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:
| Service | Image | Ports | Role |
|---|---|---|---|
app | simchasolutions/db-studio:latest (override via $SIMCHA_IMAGE) | 3000, 5002 | API + UI, one container |
postgres | postgres:16-alpine | internal only | Metadata 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
- Both ports must be reachable through your proxy:
/api/*→ 5002, everything else → 3000. Remap host-side (-p 8080:3000) as needed and keepCLIENT_URLin sync. PORT/NEXT_PORTchange the in-container listen ports if you must avoid 5002/3000.- For a Linux host running Postgres on the host itself, attach the container to the host network or the DB's network rather than relying on
host.docker.internal. - Enforce TLS to managed Postgres with
PGSSLMODE=require(or append?sslmode=require).
Failure modes
| Symptom | Cause | Resolution |
|---|---|---|
| Container exits at boot | Missing required secret in production | The env validator names the missing var in the logs; set it and restart. |
| CORS errors in the browser | CLIENT_URL ≠ the external origin | Match scheme + host + port exactly, no trailing slash. |
Health stuck at 503 STARTING | Metadata DB unreachable; the readiness poller is still retrying | Verify 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 breaks | ENCRYPTION_KEY or JWT_SECRET changed against existing data | Restore the prior key from secret storage, or restore the DB from a pre-rotation backup. Encryption-key rotation is one-way. |