Deployment Overview

Simcha DB Studio ships as a single, self-contained, multi-arch container image (linux/amd64 + linux/arm64). You run it in your own environment; there is no Simcha-managed multi-tenant SaaS. The image is identical across Compose, Kubernetes, ECS, ACA, Cloud Run, and bare container runtimes, the orchestration manifest is the only thing that changes. The image is stateless; all durable state lives in one external PostgreSQL (the db_devops metadata store). That single fact drives every scaling, HA, and backup decision below.

Topology

                    +---------------------------+
                    |  Reverse proxy / LB / IC   |
                    |  (nginx, ALB, Traefik...)  |
                    +------+--------+-----------+
                           |        |
                       /api/*       /            <- path-based split
                       :5002      :3000
                           |        |
                    +------+--------+-----------+
                    |  Simcha DB Studio          |
                    |  one container, tini PID 1 |
                    |    node dist/server (API)  :5002
                    |    node server.js (Next)   :3000
                    +------+--------------------+
                           |
                    +------+--------------------+
                    |  PostgreSQL 13+            |
                    |  metadata store: db_devops |
                    +---------------------------+

Process model

tini is PID 1; a supervising entrypoint (docker-entrypoint.sh) forks both Node processes and blocks on wait -n. The two long-lived processes are the Express API (:5002, auth, license, queries, ChangeOps, audit) and the Next.js standalone server (:3000, built with output: 'standalone'). This is a deliberate fail-fast supervisor: if either child exits, the entrypoint tears down the other and exits the container with that child's code, so the orchestrator restarts the whole task rather than leaving a half-dead pod (API up, UI dead). A SIGTERM/SIGINT trap forwards the signal to both children and drains before exit, so rolling deploys terminate cleanly. Port bindings are configurable via PORT (API, default 5002) and NEXT_PORT (UI, default 3000).

Reverse proxy contract

The two ports are a hard split, not a fallback: route /api/* to :5002 and everything else to :3000. Terminate TLS at the proxy/LB/ingress; the container speaks plaintext HTTP internally and does not manage certificates. Set CLIENT_URL to the exact external origin (scheme + host + port, no trailing slash), it is the CORS allowlist and a mismatch is the most common post-deploy failure.

Metadata database

One external PostgreSQL 13+ holds everything durable: users, connections, scripts, releases, executions, approvals, audit logs, license record, notifications, discovery inventory. Encrypted-at-rest columns (target-DB passwords, MFA secrets, repo credentials) are sealed with ENCRYPTION_KEY and are meaningless without it. The bundled single-node Postgres in the Compose/Helm artifacts is for evaluation only; production points at managed Postgres (RDS, Cloud SQL, Azure Database for PostgreSQL Flexible Server) or your own HA cluster.

Deployment paths

PathFitArtifact
Helm chartProduction on any K8s distribution (EKS, AKS, GKE, OpenShift, k3s, on-prem). The canonical HA path; migration Job and probes are pre-wired.helm/ in the deployment package
Docker ComposeSingle host, on-prem VM, evaluation.docker-compose.yml + .env
ECS Fargate / Azure Container Apps / Cloud RunCloud-native no-K8s deploys; same image behind the vendor's LB.Vendor manifest + the published image
Bare runtimeOn-prem host without an orchestrator.Any OCI runtime (Docker, Podman, containerd)

Required secrets

The env validator refuses to boot in production if any of these is unset:

Licensing is RSA-signed and verified against the public key baked into the image, no license env var or outbound activation call is required at runtime. Apply your key via the UI (Settings → License) or POST /api/license/activate. Full reference: Environment Variables.

Startup gating and readiness

GET /api/health is always bound and returns 503 {status:"STARTING"} until the metadata DB is reachable, then 200 {status:"OK"}. The server does not hard-fail if Postgres is briefly unreachable at boot: an unref'd readiness poller retries initializeDatabase() on a 3s interval and completes initialization once the DB answers. Wire LB/ingress/orchestrator readiness to this endpoint so traffic is withheld until the store is live. GET /api/health/migrations returns the applied rows from schema_migrations (version, appliedAt) and is the deploy-time check for confirming a target DB is at the expected schema before cutting traffic over.

Migration runner

Migrations are idempotent, tracked in schema_migrations, and run by a standalone one-shot: node dist/server/database/migrate.js (exit 0 success / 1 failure, re-run on a current DB is a no-op). The image also exposes a dedicated migrate build stage whose default command is that runner, so you can build/pull a migration-only image or simply override the command on the full image. Two operating modes:

  1. On-boot (MIGRATE_ON_BOOT=true, default). The entrypoint runs the migration before starting the app. It is intentionally non-fatal, on failure it logs a warning and still starts the app (so a first-run setup wizard can provision). Acceptable for single-instance Compose; unsafe as your only path under HA because concurrent replicas would race and a silent migration failure would not stop the rollout.
  2. Explicit gate (recommended for HA). Run the one-shot as a pre-deploy step (K8s pre-install/pre-upgrade Job or initContainer, ECS run-task, ACI) that must exit 0 before the app rollout, and set MIGRATE_ON_BOOT=false on the app replicas so migration has exactly one owner. The Helm chart wires this by default.

Distribution channels

Always pin an immutable version tag (simchasolutions/db-studio:1.0.1) in production; reserve latest for evaluation.

Pick a path