This guide covers deploying the ToolHive Registry API server using Docker and Docker Compose.
Build from source:
# Using Task
task build-image
# Or using Docker directly
docker build -t ghcr.io/stacklok/toolhive/thv-registry-api:latest .docker run -d \
--name registry-api \
-p 8080:8080 \
-v $(pwd)/examples:/config:ro \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config-git.yamldocker run -d \
--name registry-api \
-p 8080:8080 \
-v $(pwd)/examples:/config:ro \
-v $(pwd)/data/registry.json:/data/registry.json:ro \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config-file.yaml# Create pgpass file with database credentials
cat > ~/.pgpass <<EOF
postgres:5432:registry:db_app:app_password
EOF
chmod 600 ~/.pgpass
docker run -d \
--name registry-api \
-p 8080:8080 \
-v $(pwd)/examples:/config:ro \
-v ~/.pgpass:/root/.pgpass:ro \
-e PGPASSFILE=/root/.pgpass \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config-database-dev.yamlCommon options:
docker run \
-d # Run in background
--name registry-api # Container name
-p 8080:8080 # Port mapping
-v $(pwd)/config:/config:ro # Config volume (read-only)
-v ~/.pgpass:/root/.pgpass:ro # Mount pgpass file
-e PGPASSFILE=/root/.pgpass # Point to pgpass file
--restart unless-stopped # Restart policy
--health-cmd='curl -f http://localhost:8080/health || exit 1' \
--health-interval=30s # Health check
--health-timeout=3s \
--health-retries=3 \
ghcr.io/stacklok/toolhive/thv-registry-api:latest \
serve --config /config/config.yamlA complete Docker Compose setup is provided that includes PostgreSQL and the API server with automatic migrations.
Recommended: Use Task commands for fresh state
# Start with fresh state (rebuilds image, clears volumes)
task docker-up
# Or detached mode
task docker-up-detached
# View logs
task docker-logs # All logs
FOLLOW=true task docker-logs # Tail logs
# Stop and clean up
task docker-downAlternative: Direct docker-compose
# Start services (may have stale state)
docker-compose up
# For fresh start, you must manually rebuild and clean:
docker-compose down -v && docker-compose up --buildImportant: The
task docker-upcommands automatically ensure fresh state by:
- Rebuilding the registry-api image from source
- Clearing the postgres_data volume (fresh database)
- Clearing the registry_data volume (no cached sync state)
This prevents subtle bugs from stale migrations, old code, or cached data.
The docker-compose.yaml includes two services:
- postgres - PostgreSQL 18 database server
- registry-api - Main API server (runs migrations automatically on startup)
Service startup flow:
postgres (healthy) → registry-api (runs migrations, then starts)
Default setup uses:
- Config file:
examples/config-docker.yaml - Sample data:
examples/registry-sample.json - Database passwords: Managed via pgpass file (
docker/pgpass) mounted into the containerPGPASSFILEenvironment variable points to the mounted pgpass file- Contains credentials for both application and migration users
The complete docker-compose.yaml in the repository root demonstrates:
- Database-backed registry storage
- Separate users for migrations and operations
- Automatic schema migrations on startup
- Limited database privileges (principle of least privilege)
- File-based data source (for demo purposes)
- Proper service dependencies and health checks
Once running, the API is available at http://localhost:8080
# List all servers
curl http://localhost:8080/registry/v0.1/servers
# Get specific server version
curl http://localhost:8080/registry/v0.1/servers/example%2Ffilesystem/versions/latest
# Health check
curl http://localhost:8080/health- Edit
examples/registry-sample.jsonwith your MCP servers - Or change the source configuration in
examples/config-docker.yaml - Restart:
docker-compose restart registry-api
Update examples/config-docker.yaml:
sources:
- name: toolhive
git:
repository: https://github.com/your-org/your-repo.git
branch: main
path: path/to/registry.json
syncPolicy:
interval: "30m"
registries:
- name: default
sources: ["toolhive"]Restart:
docker-compose restart registry-api- Update the pgpass file (
docker/pgpass) with new credentials:
# Edit docker/pgpass
postgres:5432:registry:db_app:new-app-password
postgres:5432:registry:db_migrator:new-migration-password- Update PostgreSQL superuser password in
docker-compose.yaml:
postgres:
environment:
- POSTGRES_PASSWORD=new-postgres-password- Recreate services:
docker-compose down
docker-compose up -dThe Docker Compose setup creates three database users:
registry: Superuser (for administration)db_migrator: Migration user with schema modification privilegesdb_app: Application user with limited data access privileges
# As superuser (for administration)
docker exec -it toolhive-registry-postgres psql -U registry -d registry
# As application user
docker exec -it toolhive-registry-postgres psql -U db_app -d registry
# From host machine
PGPASSWORD=registry_password psql -h localhost -U registry -d registry
PGPASSWORD=app_password psql -h localhost -U db_app -d registry# Backup database
docker exec toolhive-registry-postgres pg_dump -U registry registry > backup.sql
# Restore database
docker exec -i toolhive-registry-postgres psql -U registry registry < backup.sqlCreate docker-compose.override.yaml for local customizations:
version: '3.8'
services:
registry-api:
environment:
- DEBUG=true
ports:
- "9090:8080" # Different port
volumes:
- ./my-config.yaml:/config/config.yaml:ro
postgres:
ports:
- "5433:5432" # Expose on different portDocker Compose automatically merges override files.
Create docker-compose.prod.yaml:
version: '3.8'
services:
registry-api:
restart: always
deploy:
replicas: 2
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 40s
postgres:
restart: always
deploy:
resources:
limits:
cpus: '2'
memory: 2G
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U registry"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
driver: localRun with:
docker-compose -f docker-compose.yaml -f docker-compose.prod.yaml up -dUpdate examples/config-docker.yaml:
auth:
mode: oauth
oauth:
resourceUrl: https://registry.localhost
providers:
- name: my-idp
issuerUrl: https://auth.example.com
audience: api://registryAdd OAuth provider service to docker-compose.yaml if needed.
- Don't use default passwords in production
- Use secrets management (Docker Swarm secrets, external secret store)
- Enable TLS (use reverse proxy like Nginx or Traefik)
- Enable authentication (OAuth, not anonymous mode)
- Limit network exposure (don't expose database port)
- Use read-only volumes where possible
Important volumes to persist:
volumes:
# Database data (critical!)
postgres-data:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres
# Sync state (optional but recommended)
registry-data:
driver: localConfigure logging drivers:
services:
registry-api:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"Or use centralized logging:
logging:
driver: "syslog"
options:
syslog-address: "tcp://logs.example.com:514"
tag: "registry-api"Add monitoring services:
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=adminUse Nginx or Traefik for TLS termination:
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- registry-apiCheck logs:
docker-compose logs registry-api
docker logs registry-apiCommon issues:
- Configuration file not found or malformed
- Environment variables not set
- Database connection failure
- Port already in use
Verify database is running:
docker-compose ps postgres
docker-compose logs postgresTest connection:
docker exec toolhive-registry-postgres pg_isready -U registryThe first startup may be slow due to:
- Database migrations running
- Initial Git clone (if using Git source)
- Docker image download
Monitor progress:
docker-compose logs -f registry-apiChange port mapping in docker-compose.yaml:
ports:
- "8081:8080" # Use 8081 instead of 8080Or in docker run:
docker run -p 8081:8080 ...Check:
- Container is running:
docker ps - Port is exposed:
docker port registry-api - Firewall allows connection
- Using correct URL:
http://localhost:8080(nothttp://127.0.0.1:8080on some systems)
Run migrations manually:
docker-compose exec registry-api \
thv-registry-api migrate up --config /etc/registry/config.yamlCheck migration status:
docker-compose exec postgres \
psql -U registry -d registry -c "SELECT * FROM schema_migrations;"- Configuration Guide - Complete configuration reference
- Database Setup - Database configuration and migrations
- Authentication - OAuth and security configuration
- Kubernetes Deployment - Deploy on Kubernetes