Skip to main content

Deploy with Docker Compose

Deploy Angos using Docker Compose with persistent storage and TLS.

Note: This guide uses filesystem storage for simplicity. For production multi-host deployments, use S3 storage instead (see the "With S3 for Multi-Replica" section below).

Prerequisites​

  • Docker with the Compose plugin installed
  • A domain name (for TLS) or self-signed certificates
  • Optional: Docker Hub credentials for pull-through cache

Basic Deployment (Development/Testing)​

This basic deployment uses filesystem storage and is suitable for development and testing only. For production, see the sections below on S3 and multi-replica setups.

Step 1: Create Configuration​

Create a directory for your deployment. The image runs as UID 65534, so that user must own the data directory:

mkdir -p registry/{config,data,certs}
cd registry
sudo chown 65534:65534 data

Create config/config.toml:

[server]
bind_address = "0.0.0.0"
port = 8000

[blob_store.fs]
root_dir = "/data"

[ui]
enabled = true
name = "My Registry"

Step 2: Create docker-compose.yml​

version: '3.8'

services:
registry:
image: ghcr.io/project-angos/angos:latest
ports:
- "8000:8000"
volumes:
- ./config:/config:ro
- ./data:/data
command: ["-c", "/config/config.toml", "server"]
restart: unless-stopped

Step 3: Start the Registry​

docker compose up -d

Step 4: Verify​

curl http://localhost:8000/v2/

Production Deployment with TLS​

Step 1: Obtain Certificates​

Place your certificates in the certs directory:

  • server.crt - Server certificate
  • server.key - Server private key

For testing, generate self-signed certificates:

openssl req -x509 -newkey rsa:4096 -keyout certs/server.key \
-out certs/server.crt -days 365 -nodes \
-subj "/CN=registry.example.com"

The registry reads the key as UID 65534, so hand the file to that user rather than loosening its mode: sudo chown 65534:65534 certs/server.key.

Step 2: Update Configuration​

Update config/config.toml:

[server]
bind_address = "0.0.0.0"
port = 8000

[server.tls]
server_certificate_bundle = "/certs/server.crt"
server_private_key = "/certs/server.key"

[global]
max_concurrent_requests = 8

[blob_store.fs]
root_dir = "/data"

[ui]
enabled = true
name = "My Registry"

Step 3: Update docker-compose.yml​

version: '3.8'

services:
registry:
image: ghcr.io/project-angos/angos:latest
ports:
- "443:8000"
volumes:
- ./config:/config:ro
- ./data:/data
- ./certs:/certs:ro
command: ["-c", "/config/config.toml", "server"]
restart: unless-stopped
healthcheck:
# /healthz reports liveness; /readyz reports readiness
test: ["CMD", "curl", "-f", "-k", "https://localhost:8000/healthz"]
interval: 30s
timeout: 10s
retries: 3

With Pull-Through Cache​

Configuration​

[server]
bind_address = "0.0.0.0"
port = 8000

[server.tls]
server_certificate_bundle = "/certs/server.crt"
server_private_key = "/certs/server.key"

[global]
max_concurrent_cache_jobs = 8

[blob_store.fs]
root_dir = "/data"

# Docker Hub
[repository."library"]
immutable_tags = true
immutable_tags_exclusions = ["^latest$"]

[[repository."library".upstream]]
url = "https://registry-1.docker.io"
# Add credentials for higher rate limits
# (see https://docs.docker.com/docker-hub/usage/)
# username = "your-dockerhub-username"
# password = "your-dockerhub-password"

# GitHub Container Registry
[repository."ghcr.io"]
immutable_tags = true

[[repository."ghcr.io".upstream]]
url = "https://ghcr.io"

[ui]
enabled = true

With a Shared Redis Cache for Multi-Replica​

Replicas need no coordination backend. Redis is optional: it shares the link, token, and key caches across replicas, so a write on one replica is visible to the others immediately instead of after the cache TTL.

docker-compose.yml​

version: '3.8'

services:
registry:
image: ghcr.io/project-angos/angos:latest
volumes:
- ./config:/config:ro
- ./data:/data
command: ["-c", "/config/config.toml", "server"]
depends_on:
- redis
restart: unless-stopped
deploy:
replicas: 2

redis:
image: redis:7-alpine
volumes:
- redis-data:/data
restart: unless-stopped

volumes:
redis-data:

A published host port can bind only one container, so the service has no ports mapping. Front the replicas with a reverse proxy (nginx, Traefik) on the same Compose network, or drop deploy.replicas and publish the port on a single replica.

Configuration​

[server]
bind_address = "0.0.0.0"
port = 8000

[blob_store.fs]
root_dir = "/data"

[metadata_store.fs]
root_dir = "/data"

[cache.redis]
url = "redis://redis:6379"
key_prefix = "angos"

With S3 for Multi-Replica​

S3 storage lets replicas run on any number of hosts with no Redis and no other coordination infrastructure.

docker-compose.yml​

version: '3.8'

services:
registry:
image: ghcr.io/project-angos/angos:latest
volumes:
- ./config:/config:ro
command: ["-c", "/config/config.toml", "server"]
restart: unless-stopped
deploy:
replicas: 2

As with the Redis setup above, the replicated service has no ports mapping; front the replicas with a reverse proxy on the same Compose network.

Configuration​

[server]
bind_address = "0.0.0.0"
port = 8000

[blob_store.s3]
bucket = "my-registry"
endpoint = "https://s3.amazonaws.com"
region = "us-east-1"
access_key_id = "YOUR_ACCESS_KEY"
secret_key = "YOUR_SECRET_KEY"

[metadata_store.s3]
bucket = "my-registry"
endpoint = "https://s3.amazonaws.com"
region = "us-east-1"
access_key_id = "YOUR_ACCESS_KEY"
secret_key = "YOUR_SECRET_KEY"

Coordination is lock-free: no lock backend needs configuring. When [global.job_queue] is enabled, a startup probe verifies the provider's atomic create-if-absent (PutObject with If-None-Match: *) before the durable queue is served.


Scheduled Storage Maintenance​

Run maintenance manually with Docker Compose:

docker compose run --rm registry -c /config/config.toml scrub
docker compose run --rm registry -c /config/config.toml prune

Schedule it with a systemd timer on the host. Create /etc/systemd/system/registry-maintenance.service:

[Unit]
Description=Registry storage maintenance
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
WorkingDirectory=/path/to/registry
ExecStart=/usr/bin/docker compose run --rm registry -c /config/config.toml scrub
ExecStart=/usr/bin/docker compose run --rm registry -c /config/config.toml prune

Create /etc/systemd/system/registry-maintenance.timer, then enable it with systemctl enable --now registry-maintenance.timer:

[Unit]
Description=Daily registry storage maintenance

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

Verification​

# Check service status
docker compose ps

# View logs
docker compose logs -f registry

# Test push
docker pull alpine:latest
docker tag alpine:latest localhost:8000/test/alpine:latest
docker push localhost:8000/test/alpine:latest

# Test pull-through cache
docker pull localhost:8000/library/nginx:latest

Troubleshooting​

Container won't start:

docker compose logs registry

Permission denied on volumes: the container runs as UID 65534.

sudo chown -R 65534:65534 data/

TLS certificate errors:

# Verify certificate
openssl x509 -in certs/server.crt -text -noout

Next Steps​