Skip to main content

Registry Service

The Registry Service is an OCI Distribution Specification-compliant container registry backed by GFS for blob and manifest storage, and PostgreSQL for metadata. It allows users to push, pull, and manage container images using standard Docker tooling.

Overview

  • OCI compliant: Implements the OCI Distribution Spec v1.1 at /v2/
  • Storage backend: All blobs and manifests are stored in GFS under the core-registry namespace — no external object storage required
  • Authentication: Docker Token Auth (RFC 6750) flow via auth.cloud.eddisonso.com/v2/token
  • Garbage collection: Background two-phase mark-and-sweep GC running on a 24-hour cycle

Architecture

The registry sits within the Storage grouping alongside SFS (Simple File Share), both backed by GFS:

docker push/pull


registry.cloud.eddisonso.com (edd-registry deployment, 2 pods)

├── PostgreSQL (metadata: repos, manifests, tags, blobs, upload sessions)

└── GFS (blobs + manifests stored under core-registry namespace)
├── blobs/<sha256-hex>
├── uploads/<uuid> ← in-progress chunked uploads
└── manifests/<repo>/<sha256> ← final manifests

Auth tokens are issued by auth.cloud.eddisonso.com/v2/token and carry repository-scoped access grants (pull, push, delete). The registry validates these tokens using the shared JWT_SECRET.

Configuration

Environment Variables

VariableRequiredSourceDescription
DATABASE_URLYesK8s Secret registry-db-credentialsPostgreSQL connection string
JWT_SECRETYesK8s Secret edd-cloud-authShared HMAC secret for JWT validation
NATS_URLNoManifest envNATS server URL (e.g. nats://nats:4222). When set, push and delete events are published via the notification publisher.

Command-line Flags

FlagDefaultDescription
-addr0.0.0.0:8080Listen address
-mastergfs-master:9000GFS master gRPC address

Endpoints

All OCI Distribution API endpoints are served under https://registry.cloud.eddisonso.com/v2/.

Version Check

MethodPathAuthDescription
GET/v2/OptionalReturns {} with 200 if authenticated, 401 challenge if not

Catalog

MethodPathAuthDescription
GET/v2/_catalogRequiredList the caller's own repositories (paginated with ?n=&last=). Anonymous callers receive 401. No cross-user catalog browsing.

Blobs

MethodPathAuthDescription
HEAD/v2/{name}/blobs/{digest}pullCheck blob existence and size
GET/v2/{name}/blobs/{digest}pullDownload a blob
DELETE/v2/{name}/blobs/{digest}deleteMark blob for GC
POST/v2/{name}/blobs/uploads/pushStart a chunked or monolithic upload
PATCH/v2/{name}/blobs/uploads/{uuid}pushAppend a chunk to an in-progress upload
PUT/v2/{name}/blobs/uploads/{uuid}pushFinalize an upload (with ?digest=sha256:...)

Manifests

MethodPathAuthDescription
HEAD/v2/{name}/manifests/{ref}pullCheck manifest by tag or digest
GET/v2/{name}/manifests/{ref}pullFetch manifest by tag or digest
PUT/v2/{name}/manifests/{ref}pushPush a manifest (creates or updates a tag)
DELETE/v2/{name}/manifests/{ref}deleteDelete a manifest or untag a tag

Tags

MethodPathAuthDescription
GET/v2/{name}/tags/listpullList tags for a repository (paginated)

Management API

Separate from the OCI /v2/ API, a JSON REST API under /api/ powers the dashboard's registry views. These endpoints are authenticated with the caller's auth-service session token (see Authentication). Repo names may contain slashes and are parsed positionally.

MethodPathAuthDescription
GET/api/reposRequired (session)List the caller's own repositories. Unauthenticated callers receive 401.
GET/api/repos/{name}Required (owner or SA)Repo detail (name, visibility, owner, tag count, total size, last pushed).
GET/api/repos/{name}/tagsRequired (owner or SA)List tags with digest, size, and push time.
DELETE/api/repos/{name}/tags/{tag}OwnerDelete a tag (and clean up its manifest if unreferenced). Owner-only — session tokens do not bypass the owner check.

CORS is enabled for *.cloud.eddisonso.com origins (and https://cloud.eddisonso.com), allowing GET, PUT, DELETE, and OPTIONS.

Authentication

The registry uses the Docker Token Auth flow:

  1. Client sends an unauthenticated request to /v2/.
  2. Registry returns 401 Unauthorized with a WWW-Authenticate header pointing to https://auth.cloud.eddisonso.com/v2/token.
  3. Docker client sends its credentials to the token endpoint and receives a short-lived JWT scoped to the requested repository and actions.
  4. Client retries the original request with Authorization: Bearer <token>.

The JWT encodes the scope as repository:<name>:<actions> (e.g. repository:myuser/myimage:pull,push).

Token Types

The registry accepts two kinds of bearer token, both signed with the shared JWT_SECRET:

  1. OCI registry tokens — short-lived, repository-scoped tokens issued by auth.cloud.eddisonso.com/v2/token for docker push/pull. Access is limited to the repository:<name>:<actions> grants encoded in the token.
  2. Session tokens — standard auth-service session JWTs (the user_id/type claims used by the frontend dashboard). A session token grants the caller full access to their own repositories (hasAccess returns true for any action). It carries no per-repo scope.

Because both token types share the same signing key, authenticate tries the session token first: a session JWT would also parse as a registry token, but registry parsing would wrongly use the Subject (username) as the user ID instead of the user_id claim. Order therefore matters — session validation must run before OCI validation. The resulting authResult sets IsSession so downstream checks can distinguish the two.

Note: session-token "full access to own repos" applies to the OCI /v2/ actions via hasAccess. The owner-only management endpoint (DELETE .../tags/{tag}) is not bypassed by IsSession — it compares UserID against the repo owner directly.

Repository Visibility

All repositories are private (visibility=0). There is no public repository level. All pull, push, delete, and catalog operations require a valid token scoped to the owner.

warning

The PUT /api/repos/{name}/visibility endpoint has been removed. Changing a repository's visibility is no longer supported. All repositories are private-only.

Storage Layout

GFS namespace: core-registry

Path patternContents
blobs/<sha256-hex>Finalized layer and config blobs
uploads/<uuid>In-progress chunked upload data
manifests/<repo-name>/<sha256-hex>Finalized manifest JSON

Blob data is stored exactly once in GFS regardless of how many repositories reference it. The repository_blobs and manifest_blobs tables track reference relationships in PostgreSQL.

Garbage Collection

GC runs as a background goroutine on a 24-hour interval using a two-phase mark-and-sweep strategy:

  1. Sweep — delete blobs that were marked for GC (gc_marked_at IS NOT NULL) more than one interval ago and remove the corresponding GFS objects.
  2. Mark — set gc_marked_at on blobs not referenced by any manifest (i.e. absent from manifest_blobs).
  3. Clean — delete upload sessions older than 24 hours to reclaim GFS space for abandoned uploads.

The API DELETE /v2/{name}/blobs/{digest} only sets the GC mark; actual GFS object removal happens asynchronously during the next sweep phase. This ensures concurrent pulls are not interrupted.

Deployment

  • Replicas: 2 pods spread across backend nodes (backend: "true")
  • Topology: topologySpreadConstraints with maxSkew: 1 ensures one pod per node
  • Manifest: manifests/edd-registry/edd-registry.yaml
  • Liveness/readiness: HTTP GET /healthz on port 8080 (an unauthenticated 200 OK handler — /v2/ is not used for probes because it requires auth and would return 401)

Database Schema

TablePurpose
repositoriesRegistry repositories — owner ID, name, visibility
manifestsManifest records — digest, media type, size — per repository
tagsTag-to-digest mappings per repository
repository_blobsBlobs tracked per repository with optional gc_marked_at timestamp
manifest_blobsJoin table linking manifests to their referenced blob digests
upload_sessionsIn-progress chunked upload state: UUID, hash checkpoint, byte count