Pages and sections
shipyard.trian.space/docs/registry
Container registry
Push, pull and deploy Docker images and static site bundles from Shipyard's built-in OCI registry.
Shipyard includes a built-in OCI-compatible container registry. Push images from your local machine or CI pipeline, then deploy them to your Swarm services with zero external dependencies. The registry speaks the standard Docker Distribution API — any tool that works with Docker Hub or GHCR works here too.
It is not limited to Docker images: the same registry stores static site bundles (and edge function bundles) as OCI artifacts, so a CI pipeline can build a site once, push it, and have Shipyard deploy it — see Static site bundles.
| Item | Value |
|---|---|
| Registry URL | registry.your-domain.com |
| Auth | API key (ship_...) |
| Protocol | OCI Distribution Spec v1.1 |
| Image path | org/project/repo:tag |
#Quick start
- Create an API key with
registry:managescope in Settings → API Keys - Run
docker login registry.your-domain.com -u anyuser -p ship_xxx - Tag your image:
docker tag myapp registry.your-domain.com/acme/myproject/myapp:latest - Push:
docker push registry.your-domain.com/acme/myproject/myapp:latest - In a Shipyard service, set the image to the full registry URL and deploy
#API keys
The registry uses Shipyard API keys for authentication — not separate registry credentials. Keys start with ship_ and are scoped to control exactly what the holder can do.
| Scope | Allows |
|---|---|
registry:view | Pull images only — read-only access to all repositories in the organization |
registry:manage | Pull and push — full read/write access; also includes registry:view |
#Creating a key
- Go to Settings → API Keys
- Click New API key
- Give the key a name (e.g. CI Push or Read-only pull)
- Select
registry:manage(push + pull) orregistry:view(pull only) - Optionally set an expiry date
- Copy the key immediately — it is shown only once
#docker login
The registry implements the Docker token authentication spec. Docker automatically negotiates a short-lived JWT when you run docker login — you never interact with the token directly.
#Login command
docker login registry.your-domain.com \
-u anyuser \
-p ship_xxxxxxxxxxxxxxxxxxxx#How it works
- Docker sends
GET /v2/to check the registry - The registry returns
401 Unauthorizedwith aWWW-Authenticateheader pointing to/auth/registry/token - Docker sends Basic auth (username + API key) to the token endpoint
- Shipyard validates the API key and returns a signed 15-minute JWT
- Docker uses the JWT for all subsequent push and pull requests
#Image naming
Images are namespaced under your organization slug, then a project slug, then a repository name. This mirrors the Shipyard project hierarchy and prevents name collisions between organizations.
# Format
registry.your-domain.com/<org-slug>/<project-slug>/<repo>:<tag>
# Examples
registry.your-domain.com/acme/backend/api:latest
registry.your-domain.com/acme/backend/api:v1.2.3
registry.your-domain.com/acme/frontend/web:sha-abc1234| Segment | What it maps to | Example |
|---|---|---|
org-slug | Your organization's URL slug | acme |
project-slug | The project containing the service | backend |
repo | Arbitrary repository name — usually the service name | api |
tag | Image tag — latest, a version, or a git SHA | v1.2.3 |
#Push & pull
# Tag a local image
docker tag myapp:latest registry.your-domain.com/acme/backend/api:latest
# Push
docker push registry.your-domain.com/acme/backend/api:latest
# Pull
docker pull registry.your-domain.com/acme/backend/api:latest#Deploying a pushed image
After pushing, point a Shipyard service at the full registry URL. The simplest way is to set the Image field in the service settings to the full registry path and click Deploy.
You can also override the image per-deployment via the special environment variable __IMAGE__:
# In a service's environment variables, override the image at deploy time
__IMAGE__=registry.your-domain.com/acme/backend/api:v1.2.3#Private registry credentials in Swarm
For services pulling from the registry at deploy time, Shipyard automatically injects the registry credentials as a Docker Swarm secret so worker nodes can pull without separate docker login calls. No extra configuration is needed on worker nodes.
#CI/CD integration
Store your API key as a CI secret and use it to authenticate during your build pipeline. Combine with a webhook trigger to redeploy after every successful push.
#GitHub Actions
Add SHIPYARD_REGISTRY_KEY to your repository secrets, then:
name: Build and push to Shipyard registry
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Shipyard registry
uses: docker/login-action@v3
with:
registry: registry.your-domain.com
username: ci
password: ${{ secrets.SHIPYARD_REGISTRY_KEY }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: registry.your-domain.com/acme/backend/api:latest#GitLab CI
variables:
IMAGE: registry.your-domain.com/acme/backend/api
before_script:
- docker login $CI_REGISTRY -u ci -p $SHIPYARD_REGISTRY_KEY
build:
script:
- docker build -t $IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $IMAGE:$CI_COMMIT_SHORT_SHA#Static site bundles
Any OCI artifact pushed with the org.shipyard.artifact.kind manifest annotation is stored as that kind instead of a Docker image. A static site is pushed as a single zip layer of your build output with kind=static_bundle; a static site service bound to that repo:tag deploys it straight from the registry — no build step on the server.
| Kind | Annotation value | Content |
|---|---|---|
| Docker image | (none — the default) | Standard image layers |
| Static site | static_bundle | One .zip layer of the built site |
| Edge function | edge_function | One JS bundle layer |
Optional annotations are recorded as metadata and shown in the registry browser:
| Annotation | Example |
|---|---|
org.shipyard.framework | vite |
org.shipyard.build_command | npm run build |
org.shipyard.output_dir | dist |
#Push with ORAS
docker push only pushes images, so use ORAS — the standard CLI for pushing arbitrary OCI artifacts. It logs in with the same ship_ API key.
# 1. Log in (username is ignored, password is a key with registry:manage)
oras login registry.your-domain.com -u ci -p ship_xxxxxxxxxxxxxxxxxxxx
# 2. Zip the *contents* of your build output (index.html at the zip root)
npm run build
(cd dist && zip -qr ../site.zip .)
# 3. Push it as a static_bundle
oras push registry.your-domain.com/acme/frontend/web:v1.2.0 \
--artifact-type application/vnd.shipyard.static-bundle \
--annotation "org.shipyard.artifact.kind=static_bundle" \
--annotation "org.shipyard.framework=vite" \
site.zip:application/zip#Deploy a bundle
- In a project, add a Static site and choose Source → Shipyard Artifactory
- Pick the bundle (
namespace/repo:tag) from the registry - Leave Auto-deploy on push checked to redeploy whenever that tag is pushed again
- Click Deploy for the first release
Each deploy fetches the newest bundle pushed to the bound tag, extracts it, and serves it — the previous version stays on disk for instant rollback. Auto-deploy can be toggled later from the service's settings.
#GitHub Actions
Add SHIPYARD_REGISTRY_KEY (a registry:manage API key) to your repository secrets. This workflow builds the site, pushes it tagged with the commit SHA (which also updates latest), and — with auto-deploy on — Shipyard rolls it out:
name: Deploy site to Shipyard
on:
push:
branches: [main]
env:
REGISTRY: registry.your-domain.com
REPO: acme/frontend/web
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci && npm run build
- name: Package bundle
run: cd dist && zip -qr ../site.zip .
- uses: oras-project/setup-oras@v1
- name: Log in to Shipyard registry
run: echo "${{ secrets.SHIPYARD_REGISTRY_KEY }}" | oras login $REGISTRY -u ci --password-stdin
- name: Push static bundle
run: |
oras push $REGISTRY/$REPO:sha-${GITHUB_SHA::7} \
--artifact-type application/vnd.shipyard.static-bundle \
--annotation "org.shipyard.artifact.kind=static_bundle" \
--annotation "org.shipyard.build_command=npm run build" \
--annotation "org.shipyard.output_dir=dist" \
site.zip:application/zipTo ship only on releases instead, trigger on push: tags: ['v*'], push $REPO:${GITHUB_REF_NAME}, and bind the site to latest.
#Deleting tags
Remove a single tag without touching the rest of the repository. In the dashboard, open Registry → namespace → repository and click the trash icon on the tag's row. Delete repository is still available to remove every tag at once.
The same action is available over the API with a session token or an API key that has registry:manage:
curl -X DELETE \
-H "Authorization: Bearer ship_xxxxxxxxxxxxxxxxxxxx" \
"https://ship.example.com/api/orgs/<org-id>/registry/namespaces/<namespace-id>/repos/web/tags/v1.2.0?kind=static_bundle"| Behaviour | Detail |
|---|---|
kind query param | Optional. A tag is unique per kind, so omit it to delete every kind under that tag name |
| Other tags | Kept — deleting v1.2.0 does not remove latest, even if both point at the same digest |
| Bound services | Returns 409 Conflict while a service's artifact source uses the tag; re-point the service first |
| Storage | Manifests and blobs are left for garbage collection, since other tags may reference them |
Platform admins can delete tags in any organization via DELETE /api/admin/registry/namespaces/<namespace-id>/repos/<repo>/tags/<tag>.
#Registry setup
The registry is bundled with Shipyard and enabled by default. To make it reachable via its own hostname (recommended), set one environment variable in your /opt/shipyard/.env file:
SHIPYARD__REGISTRY__HOSTNAME=registry.your-domain.com#DNS setup
Point your registry subdomain at the same server IP as your main Shipyard domain. An A record (or CNAME to your main domain) is sufficient:
| Record type | Name | Value |
|---|---|---|
A | registry | Your server's public IP |
TLS is provisioned automatically by Traefik via Let's Encrypt — no manual certificate management needed.
#Traefik routing
Shipyard auto-generates a Traefik dynamic configuration on every startup. The registry gets its own router rule that adds the /registry path prefix before forwarding to the backend — so the standard OCI paths (/v2/…) work transparently at the registry hostname.
http:
routers:
shipyard-registry:
rule: "Host(`registry.your-domain.com`)"
entryPoints: [websecure]
service: shipyard-backend
middlewares:
- shipyard-registry-prefix
tls:
certResolver: letsencrypt
middlewares:
shipyard-registry-prefix:
addPrefix:
prefix: "/registry"This means registry.your-domain.com/v2/… is routed to the backend as /registry/v2/…. The Traefik config file is re-written on every backend startup, so it is self-healing — if the file is lost the routes are restored automatically.