Skip to content

Container registry ​

ReleaseDesk includes an OCI-compatible container registry served from the same origin as the dashboard under /v2/. docker, podman, skopeo, crane and Kubernetes work without extra tooling. Image data lives in the same storage backend as your artifacts (disk or S3/R2).

Repositories are named <org-slug>/<name>[/<path>]. The first segment is always the organization slug, so one organization can never read or write another one's images. Repositories are created by the first push and appear under Containers in the dashboard.

Log in ​

Create an API token under Settings → Tokens with the registry:read scope, plus registry:write if it should push. Leave App on All apps: repositories belong to the organization, so app-scoped tokens are rejected at creation (the dashboard locks the field when a registry scope is checked). Then log in with any username (it is ignored) and the token as the password:

sh
echo "$RD_TOKEN" | docker login releasedesk.example.com -u rdesk --password-stdin

With the CLI, RD_URL and RD_TOKEN are read from the environment:

sh
rdesk registry login            # runs docker login for you
rdesk registry login --print    # prints the command instead
rdesk registry login --docker podman

Docker exchanges the token for a short-lived (5 minute) registry token on every operation, so the API token itself is never sent to the registry endpoints. Members can pull; admins and CI tokens with registry:write can push and delete.

Troubleshooting ​

docker login succeeds for any valid token, so a bad token only shows up on push or pull:

SymptomCause
401 Unauthorized on the blob HEAD requestThe first path segment is not the slug of the org that owns the token, or the token is app-scoped (create an org-wide one).
insufficient_scope on a pushThe token lacks registry:write, or the org is suspended.
insufficient_scope on docker push alpine:3The image was not tagged with your registry host and org slug; it went to Docker Hub. Tag it first: docker tag alpine:3 <host>/<org-slug>/alpine:3.

To see what a token may access, decode the access claim of its registry token (an empty list means no grant):

sh
curl -s -u x:$RD_TOKEN "https://<host>/v2/token?scope=repository:<org-slug>/<name>:push,pull" | jq -r .token | cut -d. -f2 | base64 -d

Push and pull ​

sh
docker tag myapp:1.4.0 releasedesk.example.com/acme/myapp:1.4.0
docker push releasedesk.example.com/acme/myapp:1.4.0
docker pull releasedesk.example.com/acme/myapp:1.4.0

Multi-architecture images work with docker buildx build --platform linux/amd64,linux/arm64 --push. The tag list in the dashboard shows the platforms of each index.

GitHub Actions ​

yaml
- uses: docker/login-action@v3
  with:
    registry: releasedesk.example.com
    username: rdesk
    password: ${{ secrets.RD_TOKEN }}
- uses: docker/build-push-action@v6
  with:
    push: true
    tags: releasedesk.example.com/acme/myapp:${{ github.ref_name }}

Public repositories ​

A repository is private by default. An admin can switch it to public from the repository page, which allows anonymous docker pull. Pushing always requires a token. Pulls are never billed for egress.

Dedicated hostname ​

By default the registry shares the dashboard origin, so image names look like releasedesk.example.com/acme/app. For shorter names, point a second hostname such as cr.example.com at the same server and set:

sh
RD_REGISTRY_HOST=cr.example.com

On that hostname ReleaseDesk serves only /v2/ and /healthz; the dashboard, API and session cookies are unreachable there. The docker login challenge points at the same host, and the dashboard shows cr.example.com/... in its pull snippets. No extra DNS record type or certificate trick is needed beyond a normal A/CNAME record and TLS for the new name (Caddy issues it automatically when the site block lists both hostnames). Keep RD_BASE_URL set to the main origin.

Plans and quota ​

On ReleaseDesk Cloud the registry is gated by the plan's registry entitlement (and registryMaxRepos for the number of repositories). Existing images stay pullable when the entitlement is off; new pushes are rejected.

Image layers count toward the organization's storage quota. Pulls are free of egress charges; usage is recorded for statistics only. The Billing page shows the repository count and stored bytes.

Garbage collection ​

Deleting a tag only removes the tag. A daily job then removes untagged manifests and unreferenced blobs older than the grace period (7 days by default), and expires abandoned upload sessions after 24 hours. The grace period protects pushes in progress, since layers are uploaded before the manifest that references them.

S3 and CDN notes ​

With RD_STORAGE=s3, blob downloads answer 307 with a presigned URL and the client fetches the layer straight from the bucket. The bucket must therefore be reachable from clients, not just from the server. Chunked uploads are staged through the server and assembled on the final request, so very large layers use extra storage I/O on S3.

Reverse proxy ​

Layers can be several gigabytes. Do not limit the request body size on /v2/, and allow long timeouts. With Caddy:

caddyfile
releasedesk.example.com, cr.example.com {
    @registry path /v2/*
    handle @registry {
        reverse_proxy localhost:3000 {
            transport http {
                response_header_timeout 0
                read_timeout 1h
                write_timeout 1h
            }
        }
    }
    handle {
        reverse_proxy localhost:3000
    }
}

Caddy does not cap request bodies by default. If you use nginx, set client_max_body_size 0; and proxy_request_buffering off; for location /v2/.

Backups ​

Image blobs and manifests are stored under the registry/ prefix of your storage backend, while repositories, tags and manifest references live in the database. Back them up together. See Backups.