ADR-0001 — Daytona sandbox mutable-image registry runs on Harbor
ADR-0001 — Daytona sandbox mutable-image registry runs on Harbor
Section titled “ADR-0001 — Daytona sandbox mutable-image registry runs on Harbor”Status: Accepted (2026-07-06)
Context
Section titled “Context”Daytona CREATE_SANDBOX builds a per-sandbox derived image daytona-<contenthash> from a base workspace snapshot, PUSHES the derived image to its INTERNAL registry, and the runner PULLS it back. For ~5h across one session this failed with Error response from daemon: manifest unknown.
Root cause (live-verified): DockerRegistryService.getRegistryPushAccess mints push access via a Harbor-style token/robot service (/service/token). ghcr.io returns 405 for that flow, so daytona never obtains a push token → the derived image never lands → runner pull = manifest unknown. The stored ghcr PAT is valid (token_match=true); the failure is upstream in token minting, not the credential.
Empirically proven this session (do not re-test):
- Flipping only the
transientdocker_registry row to Harbor while leavinginternal=ghcr → stillmanifest unknown. So daytona’s derived push uses the INTERNAL registry, not transient. - daytona resolves the base snapshot’s registry via the DEFAULT
internalrow too. Settinginternal=Harbor while the base snapshot image is stillghcr.io/...producesNo registry found for snapshot. So base-pull and derived-push are COUPLED on the internal registry.
Decision
Section titled “Decision”- The daytona mutable sandbox-image (internal/transient) registry is a chart-managed Harbor in namespace
daytona. Daytona validates and authorizes the registry through the in-cluster TLS endpoint (https://harbor.daytona.svc.cluster.local), so the chart-manageddaytona-harbor-nginxCA is mounted into both daytona-api and the runner Docker daemon trust path (/etc/docker/certs.d/harbor.daytona.svc.cluster.local). Build/push image refs use the same canonical TLS host (harbor.daytona.svc.cluster.local/...). Harbor supports the token/robot service ghcr lacks. - Harbor’s dependencies are centralized: blobs on SeaweedFS S3, DB on shared CNPG, cache on a non-cluster Redis (
daytona-redis-master, standalone — the shared cluster-modefractalops-cacheValkey only exposes db0 and cannot host Harbor’s multi-DB layout). Admin secret via OpenBao/ESOdaytona-harbor-admin. - The base daytona-workspace image must be published INTO Harbor (
harbor.daytona.svc.cluster.local/library/fractalops-api:...) byruntime-release(build_daytona_workspace), and the daytonasnapshot.imageNameset to that Harbor path. A manualskopeo/one-off copy is forbidden — it drifts back to ghcr on the next build (violates the settings-reproducibility law). - The
docker_registryDB rows (runtime DB state, NOT GitOps — Argo never touches them) forinternal+transientpoint at the same Harbor host and use the OpenBao/ESO-managed Harbor admin secret (daytona-harbor-admin/HARBOR_ADMIN_PASSWORD). Repository ownership follows ADR-0013.
Consequences
Section titled “Consequences”- Harbor is a hard runtime dependency for studio sandboxes; its 6 bring-up traps are catalogued in the daytona-registry keystone memory (RFC1123 Job name, nginx TLS secret, redis cluster-mode, admin-secret hook-GC prune, generated-secret checksum drift, jobservice/core startup race).
- The
docker_registryrow values, the daytona-apiINTERNAL/TRANSIENT_REGISTRY_URLenv, the Harbor admin password env refs, and the snapshotimageNamemust all name ONE consistent Harbor authority — reconcile them together, never one in isolation. TheinternalandtransientDB rows must useharbor.daytona.svc.cluster.local(no scheme, no port). If they drift to the oldharbor.daytona.svc.cluster.local:80authority, Harbor redirects Docker’s blob-upload POSTs and the client resumes them as GETs, ending inblob upload unknown. - A daytona-api registry-config change requires a pod roll to take effect.
- Under the native Kubernetes provider (ADR-0007) there is no runner DaemonSet:
daytona-api pulls base snapshots and pushes derived sandbox images to Harbor
directly, authenticated by the
daytona-internal-registrydockerconfig ExternalSecret (rendered by thedaytona-bootstrapchart from the Harbor admin credential). Harbor’s TLS CA reaches daytona-api via thedaytona-harbor-nginxvolume mount indaytona-values.yaml, not a hook-injected runner CA mount.
Registry topology & consolidation (Nexus vs Harbor)
Section titled “Registry topology & consolidation (Nexus vs Harbor)”Operator question: can we keep only ONE of Nexus/Harbor? Analysis across our actual components (Daytona, DevPod, BuildKit):
- BuildKit — registry-agnostic; pushes to any OCI registry. Not a constraint.
- DevPod — pulls devcontainer OCI images from any registry. Not a constraint.
- Daytona — the ONLY constraint. v0.184
getRegistryPushAccessexpects a Harbor-style token/robot registry API; ghcr and Nexus’s docker registry (standard Docker v2 bearer-token) do not implement it. - Artifact types — docker/OCI: both can host. npm/pypi/maven: Nexus ONLY — Harbor is a container+Helm registry and cannot host language packages.
Therefore:
- Harbor-only is NOT viable — it cannot replace Nexus’s npm/pypi/maven hosting.
- Nexus-only IS viable IFF daytona pushes via basic-auth to Nexus (skipping the Harbor token flow). Nexus is the superset (docker + language artifacts + proxy-cache); Harbor exists solely to satisfy daytona’s token-API quirk.
Role split (operator law, until/unless consolidated): Nexus = DEV artifact hub (npm/pypi/maven + docker dev images); Harbor = PROD/operations image hub (daytona runtime sandbox images). Do not mix roles.
Open decision (record, don’t silently keep Harbor): verify whether daytona v0.184 can basic-auth push to Nexus. If yes → consolidate to Nexus-only and retire Harbor + its 6-trap bring-up (leanest). If no → the two-registry split stands, justified purely by daytona’s hard requirement. Record the result in the Assembly-owned ADR corpus before changing topology.
Alternatives rejected
Section titled “Alternatives rejected”- ghcr basic-auth push (patch daytona to skip the token service): requires forking/patching daytona; rejected — no-fork constraint. NOTE: the same basic-auth mechanism against Nexus is the consolidation path above — revisit it, since it would retire Harbor.
- Object-storage / MinIO push path:
ObjectStorageService.getPushAccessis for build-context (context.tar) upload, NOT snapshot image propagation, which stays Docker-registry based. SeaweedFS also fails daytona’s MinIO-STS assumption. Rejected as not a registry replacement. - Keep ghcr, bump credentials/scope/rows: necessary-but-insufficient — the 405 is architectural (ghcr has no Harbor token service), so no ghcr-side fix ever lands the push.
- Harbor bundled internal Redis: rejected in favour of reusing the existing standalone
daytona-redis-master(centralization law).