Problem / motivation
We want to run the Harbor app as a highly available, zone-resilient registry. Today that isn't possible.
The only replica knobs the app exposes are database.replicas and redis.replicas. Every docker pull goes through nginx → core (/service/token) → registry, and those components always run as a single pod. Even with 3 PostgreSQL and 3 Redis instances, losing the node that hosts nginx, core or registry stops all pulls and pushes until the pod is rescheduled and ready. For tenants this means new pods and replacement nodes can't pull images, and CI pipelines fail.
The replicated data tier isn't zone-aware either. packages/system/harbor/templates/database.yaml (CNPG Cluster) and redis.yaml (RedisFailover) set no affinity or topology spread. 3 instances can all land in the same zone, and CNPG's default anti-affinity only works per node.
Where it comes from:
packages/apps/harbor/templates/harbor.yaml only passes down database.replicas and redis.replicas (L147, L153 on main). The WorkloadMonitors for core, registry and portal hard-code replicas: 1 / minReplicas: 1 (L248, L266, L282).
- The vendored upstream chart (
packages/system/harbor/charts/harbor, goharbor 1.19.1) already supports replicas, podDisruptionBudget, affinity and topologySpreadConstraints for nginx, portal, core, jobservice, registry, trivy and exporter. Only the wrapper is missing them.
- Registry storage is pinned to S3 (
imageChartStorage.type: s3, COSI BucketClaim), so registry replicas share no local state. Core keeps its state in PostgreSQL and Redis.
jobservice.jobLoggers defaults to [file] on a ReadWriteOnce jobLog PVC. This is the only thing preventing jobservice from scaling out. database and stdout are supported alternatives.
| Component |
On the pull/push path |
Can it scale out? |
Exposed today |
| nginx |
yes (entry point) |
yes, stateless |
no |
| core |
yes (token, API) |
yes |
resources only |
| registry |
yes (blobs, manifests) |
yes, S3-backed |
resources only |
| portal |
no (UI) |
yes, stateless |
no |
| jobservice |
no (async jobs) |
yes, with jobLoggers: [database] |
resources only |
| trivy |
no (scans) |
yes, per-replica cache |
resources only |
| PostgreSQL / Redis |
yes |
yes |
replicas, no zone spread |
Proposed solution
-
Expose replicas for nginx, core and registry, and optionally portal. Default stays 1, so existing installs don't change. Example:
nginx: { replicas: 2 }
core: { replicas: 2, resourcesPreset: t1.medium }
registry: { replicas: 2, resourcesPreset: t1.medium }
-
When replicas > 1 for a component, render:
podDisruptionBudget: { enabled: true, maxUnavailable: 1 }
topologySpreadConstraints on topology.kubernetes.io/zone (whenUnsatisfiable: ScheduleAnyway), plus preferred hostname anti-affinity.
These are values of the vendored chart, so no template changes are needed in it.
-
Derive the WorkloadMonitor replicas / minReplicas from the new values instead of hard-coding 1.
-
Add zone spread to the CNPG Cluster (affinity.topologyKey: topology.kubernetes.io/zone) and to the RedisFailover redis and sentinel pods, so that replicas: 3 survives the loss of a zone.
-
Optional, as a follow-up: jobservice.replicas, switching jobLoggers to [database] when replicas > 1.
Expected behavior: with replicas: 2 on nginx, core and registry in a multi-zone cluster, draining or losing one node or zone keeps docker login / pull / push working.
Scope check
The changes are limited to packages/apps/harbor and packages/system/harbor (wrapper values and templates only; the vendored chart is unchanged).
Alternatives considered
Additional context
Problem / motivation
We want to run the Harbor app as a highly available, zone-resilient registry. Today that isn't possible.
The only replica knobs the app exposes are
database.replicasandredis.replicas. Everydocker pullgoes through nginx → core (/service/token) → registry, and those components always run as a single pod. Even with 3 PostgreSQL and 3 Redis instances, losing the node that hosts nginx, core or registry stops all pulls and pushes until the pod is rescheduled and ready. For tenants this means new pods and replacement nodes can't pull images, and CI pipelines fail.The replicated data tier isn't zone-aware either.
packages/system/harbor/templates/database.yaml(CNPGCluster) andredis.yaml(RedisFailover) set no affinity or topology spread. 3 instances can all land in the same zone, and CNPG's default anti-affinity only works per node.Where it comes from:
packages/apps/harbor/templates/harbor.yamlonly passes downdatabase.replicasandredis.replicas(L147, L153 on main). TheWorkloadMonitors for core, registry and portal hard-codereplicas: 1/minReplicas: 1(L248, L266, L282).packages/system/harbor/charts/harbor, goharbor 1.19.1) already supportsreplicas,podDisruptionBudget,affinityandtopologySpreadConstraintsfornginx,portal,core,jobservice,registry,trivyandexporter. Only the wrapper is missing them.imageChartStorage.type: s3, COSIBucketClaim), so registry replicas share no local state. Core keeps its state in PostgreSQL and Redis.jobservice.jobLoggersdefaults to[file]on aReadWriteOncejobLogPVC. This is the only thing preventing jobservice from scaling out.databaseandstdoutare supported alternatives.jobLoggers: [database]replicas, no zone spreadProposed solution
Expose
replicasfornginx,coreandregistry, and optionallyportal. Default stays1, so existing installs don't change. Example:When
replicas > 1for a component, render:podDisruptionBudget: { enabled: true, maxUnavailable: 1 }topologySpreadConstraintsontopology.kubernetes.io/zone(whenUnsatisfiable: ScheduleAnyway), plus preferred hostname anti-affinity.These are values of the vendored chart, so no template changes are needed in it.
Derive the
WorkloadMonitorreplicas/minReplicasfrom the new values instead of hard-coding1.Add zone spread to the CNPG
Cluster(affinity.topologyKey: topology.kubernetes.io/zone) and to theRedisFailoverredis and sentinel pods, so thatreplicas: 3survives the loss of a zone.Optional, as a follow-up:
jobservice.replicas, switchingjobLoggersto[database]whenreplicas > 1.Expected behavior: with
replicas: 2on nginx, core and registry in a multi-zone cluster, draining or losing one node or zone keepsdocker login/pull/pushworking.Scope check
The changes are limited to
packages/apps/harborandpackages/system/harbor(wrapper values and templates only; the vendored chart is unchanged).Alternatives considered
harbor-*-systemHelmRelease: reverted on every reconcile, same situation as [harbor] No way to disable registry upload purging — every surface that could is Helm-owned and reverts #3972.schedulingClass(feat(apps): per-app schedulingClass field for selective workload placement #2622): complementary. It controls where pods go, not how many, so it doesn't remove the single point of failure.Additional context
schedulingClassharbor.spec