Docker Deployment
This guide covers deploying Rack Gateway using Docker, suitable for development, testing, and simple production deployments.
Docker Image
Section titled “Docker Image”The official Docker image is available on Docker Hub:
docker pull docker.io/docspringcom/rack-gateway:latestImage Tags
Section titled “Image Tags”| Tag | Description |
|---|---|
v0.x.x | Specific release (use this in production) |
<short-sha> | The commit a release was built from |
latest | Most recent release (mutable, avoid in production) |
Verifying an Image
Section titled “Verifying an Image”Every release image has a signed build provenance attestation generated by GitHub Actions. Verify it before deploying:
gh attestation verify oci://docker.io/docspringcom/rack-gateway:v0.x.x --repo DocSpring/rack-gatewayQuick Start
Section titled “Quick Start”1. Create a Docker Network
Section titled “1. Create a Docker Network”docker network create rack-gateway-net2. Start PostgreSQL
Section titled “2. Start PostgreSQL”docker run -d \ --name postgres \ --network rack-gateway-net \ -e POSTGRES_USER=rack_gateway \ -e POSTGRES_PASSWORD=your-secure-password \ -e POSTGRES_DB=rack_gateway \ -v pgdata:/var/lib/postgresql/data \ postgres:173. Create the audit roles
Section titled “3. Create the audit roles”The audit log lives in its own schema, owned by roles the migrations expect to exist. Create them once per PostgreSQL cluster:
docker exec -i postgres psql -U rack_gateway -d rack_gateway <<'SQL'CREATE ROLE audit_owner NOLOGIN;CREATE ROLE audit_writer NOLOGIN;CREATE ROLE audit_reader NOLOGIN;SQLHere rack_gateway is a superuser (the postgres image makes POSTGRES_USER one), so it can use these roles
directly. With separate, non-superuser roles, grant audit_owner to the role that runs migrations, and
audit_writer and audit_reader to the role the gateway connects as.
4. Apply migrations
Section titled “4. Apply migrations”The image never migrates on startup. Run the migrations with the same configuration as the gateway:
docker run --rm \ --network rack-gateway-net \ -e DATABASE_URL=postgres://rack_gateway:your-secure-password@postgres:5432/rack_gateway \ -e COLUMN_ENCRYPTION_KEY=your-column-encryption-key \ docker.io/docspringcom/rack-gateway:latest ./rack-gateway-api migrate5. Start Rack Gateway
Section titled “5. Start Rack Gateway”docker run -d \ --name rack-gateway \ --network rack-gateway-net \ -p 8080:8080 \ -e DATABASE_URL=postgres://rack_gateway:your-secure-password@postgres:5432/rack_gateway \ -e DOMAIN=gateway.example.com \ -e APP_SECRET_KEY=$(openssl rand -base64 32) \ -e AUDIT_HMAC_SECRET=$(openssl rand -base64 32) \ -e COLUMN_ENCRYPTION_KEY=your-column-encryption-key \ -e GOOGLE_CLIENT_ID=your-client-id \ -e GOOGLE_CLIENT_SECRET=your-client-secret \ -e GOOGLE_ALLOWED_DOMAIN=example.com \ -e RACK_TOKEN=your-convox-rack-token \ -e RACK_HOST=https://your-rack.convox.cloud \ -e ADMIN_USERS=admin@example.com \ docker.io/docspringcom/rack-gateway:latestKeep APP_SECRET_KEY, AUDIT_HMAC_SECRET and COLUMN_ENCRYPTION_KEY somewhere safe and pass the same values
on every start: a new AUDIT_HMAC_SECRET breaks the audit log’s hash chain, and a new COLUMN_ENCRYPTION_KEY
makes stored TOTP seeds and the Slack token unreadable. Generate the column key once with
openssl rand -base64 32; see Column Encryption.
6. Verify
Section titled “6. Verify”curl http://localhost:8080/api/v1/healthDocker Compose
Section titled “Docker Compose”To try the published image locally with Docker Compose:
services: postgres: image: postgres:17 environment: POSTGRES_USER: rack_gateway POSTGRES_PASSWORD: development POSTGRES_DB: rack_gateway volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U rack_gateway"] interval: 5s timeout: 5s retries: 5
gateway: image: docker.io/docspringcom/rack-gateway:latest depends_on: postgres: condition: service_healthy ports: - "8080:8080" environment: DATABASE_URL: postgres://rack_gateway:development@postgres:5432/rack_gateway DOMAIN: localhost:8080 APP_SECRET_KEY: dev-secret-key-32-bytes-minimum!! AUDIT_HMAC_SECRET: dev-audit-secret-32-bytes-minimum COLUMN_ENCRYPTION_KEY: ${COLUMN_ENCRYPTION_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} GOOGLE_ALLOWED_DOMAIN: ${GOOGLE_ALLOWED_DOMAIN} RACK_TOKEN: ${RACK_TOKEN} RACK_HOST: ${RACK_HOST} ADMIN_USERS: ${ADMIN_USERS} healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8080/api/v1/health"] interval: 10s timeout: 5s retries: 5
volumes: pgdata:The image never runs migrations on startup. Create the audit roles, apply the migrations, then start:
docker compose up -d postgresdocker compose exec -T postgres psql -U rack_gateway -d rack_gateway \ -c 'CREATE ROLE audit_owner NOLOGIN' -c 'CREATE ROLE audit_writer NOLOGIN' -c 'CREATE ROLE audit_reader NOLOGIN'docker compose run --rm gateway ./rack-gateway-api migratedocker compose up -dEnvironment Variables
Section titled “Environment Variables”Required Variables
Section titled “Required Variables”| Variable | Description | Example |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | postgres://user:pass@host:5432/db |
DOMAIN | Gateway domain | gateway.example.com |
APP_SECRET_KEY | 256-bit secret for sessions | openssl rand -base64 32 |
AUDIT_HMAC_SECRET | Key for the audit log’s hash chain; never change it | openssl rand -base64 32 |
COLUMN_ENCRYPTION_KEY | Encrypts stored TOTP seeds and the Slack token; generate once and keep it | openssl rand -base64 32 |
GOOGLE_CLIENT_ID | OAuth client ID | From Google Cloud Console |
GOOGLE_CLIENT_SECRET | OAuth client secret | From Google Cloud Console |
GOOGLE_ALLOWED_DOMAIN | Allowed email domain | example.com |
RACK_TOKEN | Convox rack API token | From Convox |
RACK_HOST | Convox rack URL | https://rack.convox.cloud |
Optional Variables
Section titled “Optional Variables”| Variable | Default | Description |
|---|---|---|
PORT | 8080 | HTTP listen port |
ADMIN_USERS | - | Comma-separated admin emails |
RACK_ALIAS | - | Short rack name (e.g., staging) |
See Environment Variables for the complete list.
Database Migrations
Section titled “Database Migrations”The image never migrates on startup: run migrate with the new image before starting it, after every upgrade.
docker run --rm --network rack-gateway-net -e DATABASE_URL=... -e COLUMN_ENCRYPTION_KEY=... \ docker.io/docspringcom/rack-gateway:<version> ./rack-gateway-api migrateHealth Checks
Section titled “Health Checks”The gateway exposes health endpoints:
| Endpoint | Purpose |
|---|---|
/api/v1/health | Basic health check |
Example health check configuration:
healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8080/api/v1/health"] interval: 30s timeout: 10s retries: 3 start_period: 10sProduction Considerations
Section titled “Production Considerations”Use Specific Version Tags
Section titled “Use Specific Version Tags”docker pull docker.io/docspringcom/rack-gateway:vX.Y.ZConfigure Resource Limits
Section titled “Configure Resource Limits”services: gateway: deploy: resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.25' memory: 256MEnable TLS
Section titled “Enable TLS”For production, run behind a reverse proxy with TLS:
server { listen 443 ssl http2; server_name gateway.example.com;
ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem;
# Build uploads stream through the gateway: don't cap the body size here client_max_body_size 0;
location / { proxy_pass http://gateway:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# exec, run and logs use WebSockets proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 1h; proxy_request_buffering off; }}services: traefik: image: traefik:v3.0 command: - "--providers.docker=true" - "--entrypoints.websecure.address=:443" - "--certificatesresolvers.letsencrypt.acme.email=admin@example.com" - "--certificatesresolvers.letsencrypt.acme.storage=/acme/acme.json" - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" ports: - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - acme:/acme
gateway: labels: - "traefik.enable=true" - "traefik.http.routers.gateway.rule=Host(`gateway.example.com`)" - "traefik.http.routers.gateway.tls.certresolver=letsencrypt"# Caddyfilegateway.example.com { reverse_proxy gateway:8080}Behind any reverse proxy, set TRUSTED_PROXY_CIDRS to the proxy’s addresses (for example the Docker network’s
subnet). The gateway only believes X-Forwarded-For from those, so without it the audit log, rate limits and
security emails all see the proxy’s IP. The proxy must pass WebSocket upgrades through and must not limit request
body sizes (the gateway caps API bodies itself and streams build archives).
Database Backups
Section titled “Database Backups”Configure regular PostgreSQL backups:
# Add to cron0 * * * * docker exec postgres pg_dump -U rack_gateway rack_gateway | gzip > /backups/gateway-$(date +\%Y\%m\%d\%H).sql.gzLogging
Section titled “Logging”Configure Docker logging driver for production:
services: gateway: logging: driver: "json-file" options: max-size: "10m" max-file: "3"Or use a centralized logging solution:
services: gateway: logging: driver: "awslogs" options: awslogs-group: "rack-gateway" awslogs-region: "us-east-1" awslogs-stream: "gateway"Development Mode
Section titled “Development Mode”The published image is a release build: it refuses to start with DEV_MODE, E2E_TEST_MODE or any other
test bypass set, and ./rack-gateway-api build-info reports
test bypasses: compiled out.
Development mode (non-secure cookies for HTTP, relaxed security checks, migrations on startup, email logging) needs
a build from source with the testbypass Go build tag. task dev and task docker:up build one, or pass
--build-arg GO_BUILD_TAGS=testbypass to docker build. Never deploy such a build.
Troubleshooting
Section titled “Troubleshooting”Container won’t start
Section titled “Container won’t start”Check logs:
docker logs rack-gatewayCommon issues:
| Error | Cause | Solution |
|---|---|---|
failed to ping postgres | PostgreSQL not reachable | Check DATABASE_URL and network |
APP_SECRET_KEY is required in production | Missing secret | Generate with openssl rand -base64 32 |
AUDIT_HMAC_SECRET must be set in non-dev environments | Missing secret | Generate with openssl rand -base64 32 and never change it |
COLUMN_ENCRYPTION_KEY is required | Missing key | Generate once with openssl rand -base64 32 and keep it |
DOMAIN is required in production / GOOGLE_ALLOWED_DOMAIN is required in production | Missing config | Set them |
Database connection issues
Section titled “Database connection issues”Verify PostgreSQL is accessible:
docker exec -it postgres psql -U rack_gateway -d rack_gateway -c "SELECT 1"Check running containers
Section titled “Check running containers”docker ps -adocker network inspect rack-gateway-netNext Steps
Section titled “Next Steps”- Convox Deployment - Production deployment on Convox
- Database Setup - PostgreSQL configuration
- Production Checklist - Security hardening