Docker & Docker Compose

Single-host path: the canonical Compose stack for evaluation and small on-prem installs, plus a raw docker run invocation against an existing Postgres. Both use the same published image.

Image characteristics

Node 20 on Alpine, non-root (uid 1001), tini as init, ~250-400 MB, multi-arch (linux/amd64 + linux/arm64). It carries the compiled API, the Next.js standalone bundle, and the license public key only, the private signing key is never shipped, and raw .ts source is not in the runtime layer. The Dockerfile also builds a dedicated migrate stage (--target=migrate) whose default command is the one-shot migration runner.

Built-in healthcheck:

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \\
  CMD wget --no-verbose --tries=1 --spider http://localhost:5002/api/health || exit 1

The 60s start period covers first-boot migration + dual-port bind; five consecutive failures flips the container to unhealthy. Note this probes :5002 (API) which returns 503 until the metadata DB is reachable, see the readiness model in the overview.

Compose stack

The shipped docker-compose.yml defines two services and requires the three secrets via :? interpolation, so Compose aborts if any is unset rather than booting a misconfigured stack:

ServiceImagePortsRole
appsimchasolutions/db-studio:latest (override via $SIMCHA_IMAGE)3000, 5002API + UI, one container
postgrespostgres:16-alpineinternal onlyMetadata store (simcha_pgdata volume)
cp .env.example .env
# populate JWT_SECRET, ENCRYPTION_KEY, DB_PASSWORD, e.g. openssl rand -hex 32
docker compose up -d
docker compose logs -f app        # STARTING -> OK once Postgres answers

On-boot migration is MIGRATE_ON_BOOT=true by default here, appropriate for a single-instance stack. For production, front the app port with a TLS-terminating reverse proxy and set CLIENT_URL to the external origin.

Lifecycle

docker compose down preserves the simcha_pgdata volume (all state survives). docker compose down -v destroys it, a full reset. Roll a new image forward without touching Postgres:

docker compose pull app
docker compose up -d app

Raw docker run against existing Postgres

docker run -d \\
  --name simcha-db-studio \\
  -p 3000:3000 -p 5002:5002 \\
  -e NODE_ENV=production \\
  -e JWT_SECRET=... \\
  -e ENCRYPTION_KEY=... \\
  -e DB_DEVOPS_DB_HOST=pg.internal \\
  -e DB_DEVOPS_DB_PORT=5432 \\
  -e DB_DEVOPS_DB_NAME=db_devops \\
  -e DB_DEVOPS_DB_USER=simcha \\
  -e DB_DEVOPS_DB_PASSWORD=... \\
  -e CLIENT_URL=https://dbstudio.example.com \\
  -e NEXT_PUBLIC_API_URL=https://dbstudio.example.com/api \\
  --restart unless-stopped \\
  simchasolutions/db-studio:1.0.1

Failure modes

SymptomCauseResolution
Container exits at bootMissing required secret in productionThe env validator names the missing var in the logs; set it and restart.
CORS errors in the browserCLIENT_URL ≠ the external originMatch scheme + host + port exactly, no trailing slash.
Health stuck at 503 STARTINGMetadata DB unreachable; the readiness poller is still retryingVerify DB_DEVOPS_DB_HOST/network path and credentials. Inside Compose the host is the service name postgres.
App boots but data decrypts to garbage / auth breaksENCRYPTION_KEY or JWT_SECRET changed against existing dataRestore the prior key from secret storage, or restore the DB from a pre-rotation backup. Encryption-key rotation is one-way.