Skip to main content

Configure Harbor Registry

0. Scope Legend

COMMON · NO CHANGE NEEDED
Scope: Use these pills throughout the page to know whether a step is shared, repeated per GitHub organization, or repeated per private repository.
COMMON · NO CHANGE NEEDEDConfigure once and reuse.CHANGE PER GITHUB ORGRepeat for each GitHub organization.CHANGE PER REPOSITORYRepeat for each private repository inside the organization.

1. Generic Organization and Repository Inputs

CHANGE PER GITHUB ORGCHANGE PER REPOSITORY
Scope: Use one reusable, blank context set. App Short Form and GitHub Organization are organization-scoped; Service / App Name and Target Environment drive repository-specific names. The examples are placeholders only.
Cluster-wide uniqueness: choose an App Short Form that is unique inside the shared Kubernetes and Harbor platform. It becomes part of namespaces, secrets, Harbor projects, robot usernames, and working folders.

1.1. Harbor Organization and Repository Settings

CHANGE PER GITHUB ORGCHANGE PER REPOSITORY
Scope: These operational settings are reusable, but their applied resources remain organization- or repository-scoped. Sensitive robot secrets never persist in the browser.
Private GitHub Repository List for Bulk ConfigurationCHANGE PER REPOSITORY
Optional for bulk setup. Enter one private repository per line, or separate names with commas or spaces. When this list is empty, the Service / App Name is used as the repository name.
Organization Robot SecretsCHANGE PER GITHUB ORG
Generic inputs are incomplete. Enter Service / App Name, App Short Form, GitHub Organization, and Target Environment before using generated resource names or commands. Placeholder examples are never treated as entered values.
Recommended Harbor isolation: use one private Harbor project per product/GitHub organization, not one project per repository. The generated project is <<APP_SHORT_FORM>>-ci-cd. Each private GitHub repository becomes a separate Harbor repository beneath that project.
GitHub Free + private repositories: private repositories cannot consume organization-level Actions secrets or variables, and environment secrets are not available for private repositories on GitHub Free. Configure Harbor credentials as repository-level Actions secrets and non-sensitive settings as repository-level Actions variables in every private repository.
Least-privilege robot model: create one CI robot with pull and push permissions and one runtime robot with pull-only permission. Never give a deployed workload the CI robot's push credential.

2. 🧱 Shared Harbor Infrastructure Inputs

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.

3. 🧮 Computed Isolation Values

CHANGE PER GITHUB ORGCHANGE PER REPOSITORY
Scope: Organization-scoped values are derived from App Short Form and GitHub Organization. Repository-scoped values are derived from Service / App Name and Target Environment.
Service / App Name:
<<SERVICE_OR_APP_NAME>>
Application Short Form:
<<APP_SHORT_FORM>>
GitHub Organization:
<<GITHUB_ORGANIZATION>>
Private Harbor Project:
<<APP_SHORT_FORM>>-ci-cd
GitHub App Secret:
<<APP_SHORT_FORM>>-arc-ghapp-secret
Organization Working Folder:
~/arc/<<APP_SHORT_FORM>>
CI Robot Account Name:
<<APP_SHORT_FORM>>-github-ci-cd-robot-01
CI Robot Full Username:
robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01
Runtime Robot Account Name:
<<APP_SHORT_FORM>>-runtime-pull-robot-01
Runtime Robot Full Username:
robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01
ARC Namespace:
arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>
Kubernetes Pull Secret:
<<APP_SHORT_FORM>>-harbor-regcred
Kubernetes CI Credentials Secret:
<<APP_SHORT_FORM>>-harbor-credentials
Harbor HTTP URL:
http://harbor.aspireclan.com
Harbor HTTPS URL:
https://harbor.aspireclan.com
Computed Runner Image:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0
GitHub Repository URL:
https://github.com/<<GITHUB_ORGANIZATION>>/<<SERVICE_OR_APP_NAME>>
Runner Scale Set / Helm Release:
<<SERVICE_OR_APP_NAME>>-<<ENVIRONMENT>>-arc
Runner Values File:
~/arc/<<APP_SHORT_FORM>>/<<SERVICE_OR_APP_NAME>>-<<ENVIRONMENT>>-values.yaml
Example Application Image:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<<PRIVATE_REPOSITORY_NAME>>:<<ENVIRONMENT>>-<git-sha>

4. 📦 VM Specs

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
CPU: 4 vCPU
Memory: 8192 MB
Disk: 200 GB thin

5. 🖥️ Prepare Base VM Before Templating

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.

Log in to the VM and complete the base OS preparation.

lsblk
sudo growpart /dev/sda 3
sudo pvresize /dev/sda3
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
df -h

HDD resize reference: Resize HDD conversation

sudo apt update && sudo apt full-upgrade -y
sudo reboot now
sudo apt install -y \
  ca-certificates \
  curl \
  wget \
  gnupg \
  lsb-release \
  jq \
  unzip \
  zip \
  tar \
  vim \
  nano \
  htop \
  net-tools \
  dnsutils \
  openssl \
  apt-transport-https \
  software-properties-common \
  qemu-guest-agent
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker --version
docker compose version
sudo systemctl enable docker
sudo systemctl status docker --no-pager
sudo usermod -aG docker $USER
timedatectl
sudo timedatectl set-timezone America/New_York
sudo systemctl restart systemd-timesyncd
timedatectl
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-harbor.conf
sudo sysctl --system
sudo mkdir -p /srv
sudo mkdir -p /opt
sudo mkdir -p /var/local
sudo hostnamectl set-hostname tmpl-ac-harbor-00
hostnamectl
sudo nano /etc/hosts
sudo apt update
sudo apt install -y iputils-ping
ping -c 4 192.168.8.1
ping -c 4 1.1.1.1
ping -c 4 google.com

6. 🧹 Clean the VM Before Templating

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
sudo apt autoremove -y
sudo apt clean
sudo journalctl --vacuum-time=3d
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-id

Optional cleanup:

sudo rm -rf /tmp/*
sudo rm -rf /var/tmp/*

Then shut it down:

sudo shutdown -h now

7. 📀 Create Template

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
Template Name:
tmpl-ac-harbor-00

Create the Proxmox/VM template only after all base configuration and cleanup steps are complete.

8. 🧬 Clone Template for Production Harbor VM

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
sudo hostnamectl set-hostname ac-harbor-prod-01
hostnamectl

Set static IP using: Configure VMs

Prod Hostname:
ac-harbor-prod-01
Prod VM IP:
192.168.8.8

Add an A record in db.aspireclan.com in production DNS and assign the production proxy IP.

Prod Proxy IP:
192.168.8.5

9. 📥 Download and Extract Harbor

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
sudo mkdir -p /opt/harbor
cd /opt

sudo wget https://github.com/goharbor/harbor/releases/download/v2.15.0/harbor-online-installer-v2.15.0.tgz
sudo tar xzf harbor-online-installer-v2.15.0.tgz
cd /opt/harbor
ls -la
sudo mkdir -p /data
sudo chmod 700 /data

10. ⚙️ Configure harbor.yml

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
cd /opt/harbor

sudo cp harbor.yml harbor.yml.bak
sudo cp harbor.yml.tmpl harbor.yml

sudo nano /opt/harbor/harbor.yml

Only change these kinds of values:

Nano tip: press Ctrl + W to search.
hostname:
harbor.aspireclan.com
harbor_admin_password:
<<ENTER_ADMIN_PASSWORD>>
database.password:
<<ENTER_DATABASE_PASSWORD>>

Comment the complete HTTPS section because TLS terminates at the reverse proxy:

#https:
#  # https port for harbor, default is 443
#  # port: 443
#  # The path of cert and key files for nginx
#  # certificate: /your/certificate/path
#  # private_key: /your/private/key/path

11. 🚀 Install Harbor

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.
cd /opt/harbor
sudo ./install.sh
docker ps
curl -I http://127.0.0.1
curl -I http://192.168.8.8
sudo ufw allow from 192.168.8.5 to any port 80 proto tcp
sudo ufw reload
sudo ufw status

12. 🌐 Configure Reverse Proxy

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.

Configure the reverse proxy using: prod-proxy-01 instructions

Access URL:
https://harbor.aspireclan.com

13. 🔐 Create the Product-Isolated Harbor Project

CHANGE PER GITHUB ORG
Scope: Repeat for each GitHub organization/product. Derive names from the generic App Short Form and GitHub Organization inputs.

Log in to Harbor as an administrator and create one private project for <<SERVICE_OR_APP_NAME>>.

Private Project Name:
<<APP_SHORT_FORM>>-ci-cd
  • Public: disabled
  • Automatically scan images on push: enabled
  • Project quota: set according to available Harbor storage
  • Content trust/signature enforcement: introduce later after Cosign is implemented
Do not create a separate Harbor project for every microservice repository by default. A Harbor project is the product security boundary. Repositories such as <service-one> and <service-two> should live beneath <<APP_SHORT_FORM>>-ci-cd. Create a separate project only when a repository requires different administrators, retention, replication, legal/compliance controls, or a stricter trust boundary.

14. 🤖 Create the CI Push/Pull Robot

CHANGE PER GITHUB ORG
Scope: Repeat for each GitHub organization/product. Derive names from the generic App Short Form and GitHub Organization inputs.

Go to project <<APP_SHORT_FORM>>-ci-cdRobot Accounts New Robot Account.

Robot Account Name:
<<APP_SHORT_FORM>>-github-ci-cd-robot-01
Expected Full Username:
robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01
Expiration:
365 days
  • Repository: Pull Repository
  • Repository: Push Repository
  • Do not grant delete, project administration, member management, or robot management.

Export or copy the generated robot secret immediately and save it in NordPass. Harbor cannot display the original secret again; rotate the secret when needed.

15. 🔽 Create the Runtime Pull-Only Robot

CHANGE PER GITHUB ORG
Scope: Repeat for each GitHub organization/product. Derive names from the generic App Short Form and GitHub Organization inputs.

Create a second robot account for Kubernetes and deployment-time image pulls.

Robot Account Name:
<<APP_SHORT_FORM>>-runtime-pull-robot-01
Expected Full Username:
robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01
Expiration:
365 days
  • Repository: Pull Repository only
  • Do not grant Push Repository.
  • Do not grant delete or project administration permissions.

Save the generated runtime robot secret in NordPass.

16. 🧹 Configure Tag Retention and Immutability

CHANGE PER GITHUB ORG
Scope: Repeat for each GitHub organization/product. Derive names from the generic App Short Form and GitHub Organization inputs.

Configure a project-level retention policy that keeps approximately the latest 20 development/build tags per repository. Preserve production releases according to your release and rollback requirements.

  • Allow mutable development tags only when the workflow requires them.
  • Prefer immutable Git SHA and release tags.
  • Apply immutability to production, release, and semantic-version tags.
  • Do not make a moving tag such as latest immutable unless that is intentional.
  • Schedule garbage collection only after validating retention behavior.

17. 🔑 Configure GitHub Free Private Repository Secrets

CHANGE PER REPOSITORY
Scope: Repeat for each private repository inside the selected GitHub organization. GitHub Free requires repository-level Actions secrets and variables.
Do not create organization-level Harbor Actions secrets for this workflow while the GitHub organizations remain on GitHub Free. Private repositories cannot consume organization-level Actions secrets or variables on that plan. Also do not depend on environment secrets or environment variables for private repositories. Use repository-level secrets and variables in every listed private repository.

Selected private repositories:

https://github.com/<<GITHUB_ORGANIZATION>>/<<PRIVATE_REPOSITORY_NAME>>

Repository-level values:

Secret: HARBOR_USERNAME:
robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01
Secret: HARBOR_PASSWORD:
<<ENTER_CI_ROBOT_SECRET>>
Variable: HARBOR_REGISTRY:
harbor.aspireclan.com
Variable: HARBOR_PROJECT:
<<APP_SHORT_FORM>>-ci-cd

Run the following from a trusted admin workstation with GitHub CLI authenticated to .

gh auth status

read -s -p "Harbor CI robot secret: " HARBOR_CI_PASSWORD
echo

HARBOR_CI_USERNAME='robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01'
HARBOR_REGISTRY='harbor.aspireclan.com'
HARBOR_PROJECT='<<APP_SHORT_FORM>>-ci-cd'

REPOSITORIES=(
  "<<ENTER_PRIVATE_REPOSITORY_NAME>>"
)

for REPOSITORY in "${REPOSITORIES[@]}"; do
  FULL_REPOSITORY='<<GITHUB_ORGANIZATION>>/'"${REPOSITORY}"

  echo "Configuring ${FULL_REPOSITORY}..."

  printf '%s' "${HARBOR_CI_USERNAME}" | \
    gh secret set HARBOR_USERNAME --repo "${FULL_REPOSITORY}"

  printf '%s' "${HARBOR_CI_PASSWORD}" | \
    gh secret set HARBOR_PASSWORD --repo "${FULL_REPOSITORY}"

  gh variable set HARBOR_REGISTRY \
    --repo "${FULL_REPOSITORY}" \
    --body "${HARBOR_REGISTRY}"

  gh variable set HARBOR_PROJECT \
    --repo "${FULL_REPOSITORY}" \
    --body "${HARBOR_PROJECT}"

  gh secret list --repo "${FULL_REPOSITORY}"
  gh variable list --repo "${FULL_REPOSITORY}"
  echo
done

unset HARBOR_CI_PASSWORD
unset HARBOR_CI_USERNAME
unset HARBOR_REGISTRY
unset HARBOR_PROJECT
GitHub permits up to 100 repository secrets per repository. This Harbor design uses only two Harbor secrets per repository. Self-hosted runner execution does not consume GitHub-hosted runner minutes, but Actions artifacts and caches still count toward the organization's applicable storage allowance. Keep workflow artifact retention intentionally short.

18. ☸️ Create Product-Isolated Kubernetes Harbor Secrets

CHANGE PER GITHUB ORG
Scope: Repeat for each GitHub organization/product. Derive names from the generic App Short Form and GitHub Organization inputs.

Create the pull-only Docker registry secret in the ARC runner namespace. This secret is used by imagePullSecrets.

read -s -p "Harbor runtime pull-only robot secret: " HARBOR_RUNTIME_PASSWORD
echo

kubectl create namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
  --dry-run=client \
  -o yaml | kubectl apply -f -

kubectl create secret docker-registry <<APP_SHORT_FORM>>-harbor-regcred \
  --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
  --docker-server='harbor.aspireclan.com' \
  --docker-username='robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01' \
  --docker-password="${HARBOR_RUNTIME_PASSWORD}" \
  --docker-email='noreply@aspireclan.com' \
  --dry-run=client \
  -o yaml | kubectl apply -f -

unset HARBOR_RUNTIME_PASSWORD

kubectl get secret <<APP_SHORT_FORM>>-harbor-regcred \
  --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>

Create the CI push/pull credentials secret separately. The runner container can expose these values to workflows that must authenticate to Harbor.

read -s -p "Harbor CI push/pull robot secret: " HARBOR_CI_PASSWORD
echo

kubectl create secret generic <<APP_SHORT_FORM>>-harbor-credentials \
  --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
  --from-literal=HARBOR_USERNAME='robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01' \
  --from-literal=HARBOR_PASSWORD="${HARBOR_CI_PASSWORD}" \
  --dry-run=client \
  -o yaml | kubectl apply -f -

unset HARBOR_CI_PASSWORD

kubectl get secret <<APP_SHORT_FORM>>-harbor-credentials \
  --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>
The Kubernetes secret names intentionally match the multi-organization ARC convention: <<APP_SHORT_FORM>>-harbor-regcred for pull-only image access and <<APP_SHORT_FORM>>-harbor-credentials for CI push/pull credentials. Never reuse one GitHub organization's Harbor credentials in another organization's runner namespace.

19. 🧪 Build the Product Runner Image on the Build VM

CHANGE PER GITHUB ORG
Scope: Repeat for each GitHub organization/product. Derive names from the generic App Short Form and GitHub Organization inputs.

Log in to the build VM.

Build VM:
prod-build-02
mkdir -p ~/arc-runner-azcli/<<APP_SHORT_FORM>>
cd ~/arc-runner-azcli/<<APP_SHORT_FORM>>

Create a Dockerfile with the following content:

FROM ghcr.io/actions/actions-runner:latest

USER root

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       ca-certificates curl apt-transport-https lsb-release gnupg \
    && curl -sL https://aka.ms/InstallAzureCLIDeb | bash \
    && az version \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

USER runner
docker build -t harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0 .

Log in with the selected product's CI robot:

docker login harbor.aspireclan.com -u 'robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01'

Paste the CI robot secret when prompted.

Push the image:

docker push harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/actions-runner-azcli:v1.0.0

20. 🏷️ Application Image Naming Convention

CHANGE PER REPOSITORY
Scope: Repeat for each private repository inside the selected GitHub organization. GitHub Free requires repository-level Actions secrets and variables.

Store every service image from beneath the selected product project. Use the GitHub repository name as the Harbor repository name.

# Recommended immutable image:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<github-repository-name>:<environment>-<git-sha>

# Example:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<<PRIVATE_REPOSITORY_NAME>>:<<ENVIRONMENT>>-<git-sha>

# Optional human-friendly release tag:
harbor.aspireclan.com/<<APP_SHORT_FORM>>-ci-cd/<github-repository-name>:v1.2.3
The product project is the isolation boundary; the Harbor repository is the individual service/image boundary. This layout is easy to browse, matches GitHub repository names, and allows project-level robot accounts, vulnerability scanning, quotas, retention, and audit review.

21. ✅ Final Isolation Verification

CHANGE PER GITHUB ORGCHANGE PER REPOSITORY
Scope: Verify the organization Harbor boundary and every repository-level GitHub configuration before enabling production workflows.
echo "Service / App: <<SERVICE_OR_APP_NAME>>"
echo "GitHub organization: <<GITHUB_ORGANIZATION>>"
echo "Harbor project: <<APP_SHORT_FORM>>-ci-cd"
echo "ARC namespace: arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>>"
echo "Kubernetes pull secret: <<APP_SHORT_FORM>>-harbor-regcred"
echo "Kubernetes CI secret: <<APP_SHORT_FORM>>-harbor-credentials"
echo "CI robot: robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-github-ci-cd-robot-01"
echo "Runtime robot: robot$<<APP_SHORT_FORM>>-ci-cd+<<APP_SHORT_FORM>>-runtime-pull-robot-01"

kubectl get secrets \
  --namespace arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>> \
  | grep -E '<<APP_SHORT_FORM>>-harbor-regcred|<<APP_SHORT_FORM>>-harbor-credentials'
  • The Harbor project is private.
  • The CI robot belongs only to the selected product project.
  • The runtime robot has pull-only permission.
  • Every private GitHub repository has repository-level Harbor secrets and variables.
  • No workflow depends on GitHub Free organization-level secrets or private-repository environment secrets.
  • The ARC namespace and Kubernetes secrets use the selected app short form.

22. 📚 Official References

COMMON · NO CHANGE NEEDED
Scope: Shared across all GitHub organizations. Configure once and reuse unless the shared Harbor platform itself changes.