Appearance
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-stdinWith 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 podmanDocker 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:
| Symptom | Cause |
|---|---|
401 Unauthorized on the blob HEAD request | The 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 push | The token lacks registry:write, or the org is suspended. |
insufficient_scope on docker push alpine:3 | The 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 -dPush 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.0Multi-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.comOn 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.