Docker Compose deployment
Running the full Open Gauge stack with Docker Compose — services, ports, environment variables, backups, and migrations.
Docker Compose is Open Gauge's primary, first-class deployment target. Every feature is expected to work on it — from a Raspberry Pi or Intel N100 mini PC in a lab, to an internal company server. Kubernetes support is optional and not required to run Open Gauge.
Prerequisites
- Docker and Docker Compose installed on the host.
- Ports
3000,3002,8000,8080,9000,9001, and5432free (or remapped — see below).
Bring the stack up
cd infrastructure/docker
docker compose up -dThis builds and starts every service defined in infrastructure/docker/docker-compose.yml.
Services
| Service | Image / build | Port(s) | Purpose |
|---|---|---|---|
db | postgres:15-alpine | 5432 | The single source of truth for all structured data. |
api | apps/api/Dockerfile | 8000 | FastAPI backend (/api/v1/...), OpenAPI schema at /openapi.json. |
web | apps/web/Dockerfile | 3000 | The Open Gauge user interface. |
docs | apps/docs/Dockerfile | 3002 | This documentation site (Knowledge Center + API Reference). |
adminer | adminer:4 | 8080 | Lightweight web UI for inspecting the Postgres database directly. |
minio | minio/minio | 9000 (S3 API), 9001 (console) | S3-compatible object storage for certificates, datasheets, and images. |
api, web, and docs wait for db (and, for web/docs, api) to report healthy before
starting, via Docker Compose depends_on: condition: service_healthy — so a fresh docker compose up -d comes up in the right order automatically.
The api image bakes in Tectonic (a small
self-contained LaTeX engine, ~20MB) to compile certificate templates
to PDF. Its LaTeX package bundle is pre-fetched and cached into the image at build time, so
certificate generation works fully offline at runtime — no network access needed, including in
air-gapped deployments.
Key environment variables
Every credential and URL used by infrastructure/docker/docker-compose.yml is read from a
single file, infrastructure/docker/.env
— edit that file (not the compose file itself) before deploying anywhere beyond local evaluation:
| Variable | Service | Purpose |
|---|---|---|
HOST_IP | web, docs, minio URLs | Hostname/IP the app is reachable at; used to derive the URLs below. |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB | db, api | Postgres credentials and database name. |
SECRET_KEY | api | Signs session/auth tokens — change this in production. |
FRONTEND_URL | api | Public app URL; encoded into generated QR codes and asset labels — point this at your production domain, not localhost. |
CORS_ORIGINS | api | JSON array of origins allowed to call the API (must include your web URL). |
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD | minio, api | Object storage credentials. |
MINIO_BUCKET | api | Bucket used for calibration certificates, datasheets, and uploads. |
MINIO_PUBLIC_URL | api | The externally-reachable MinIO URL used for presigned certificate/file links. |
NEXT_PUBLIC_API_URL | web (build arg) | The browser-reachable API URL, baked in at build time. |
API_INTERNAL_URL | web, docs | The container-to-container API URL (e.g. http://api:8000), used for server-side requests — hardcoded in the compose file since it never varies between deployments. |
NEXT_PUBLIC_DOCS_URL | web (build arg) | The browser-reachable docs URL — used by in-app tooltips to deep-link into this documentation. |
Backups
The easiest path is the Dangerous zone at the bottom of Admin → Dashboard (superadmin
only): Export database downloads a single zip bundling a pg_dump custom-format archive of
the whole database (assets, calibrations, users, audit logs) together with every file in MinIO
(certificates, datasheets, LaTeX templates, profile pictures). The same panel's Import
database button restores both from that one file — including on a different Open Gauge
instance, since the media the restored records point to comes along with it instead of being left
behind in the original instance's MinIO.
Equivalently, the two pieces can still be backed up separately with regular infrastructure tools:
- PostgreSQL (
infrastructure/docker/data/postgres) — runpg_dump/pg_restoreagainst thedbservice directly, or back up the directory itself with any regular file-level tool. - MinIO (
infrastructure/docker/data/minio) — back up the directory directly, or usemc mirroragainst the MinIO S3 API.
Losing MinIO data doesn't corrupt calibration records (certificates can be regenerated on demand
via GET /calibrations/{id}/certificate), but user-uploaded datasheets, raw calibration files,
and custom certificate templates are not recoverable without a MinIO backup. Losing PostgreSQL
data loses everything — back it up on a real schedule, not just the volume snapshot that happens
to exist.
Import database also still accepts a bare pg_dump archive — the format Export produced
before it bundled media — for restoring older backups; in that case only the database is
restored and existing MinIO files are left untouched.
If you run pg_dump/pg_restore yourself instead of using the in-app Export/Import, use a
client version matching the db service's Postgres major version (15 — see the image: tag in
docker-compose.yml). A newer client's pg_restore emits session-setup statements the server
doesn't recognize (e.g. transaction_timeout, introduced in Postgres 17), which breaks the
restore. The api image itself always ships the matching client version, so Export/Import from
the UI is unaffected regardless of what's installed on your own machine.
PostgreSQL and MinIO data live under infrastructure/docker/data/ as bind mounts — a fixed
host path next to the compose file — rather than Docker-managed named volumes, specifically so
data can't silently disappear after a rebuild: a bind mount's identity is the literal path, so it
can't diverge the way a named volume can when Compose is invoked differently (a different working
directory, an explicit -p project override, or a legacy docker-compose v1 binary). You can
verify this on your own deployment with scripts/verify-media-persistence.sh at the repo root —
see Verifying persistence below.
If you're upgrading from a version that used named volumes (postgres_data/minio_data), copy
their contents into the new paths once before redeploying:
docker run --rm -v opengauge_postgres_data:/from -v "$(pwd)/data/postgres:/to" alpine cp -a /from/. /to/
docker run --rm -v opengauge_minio_data:/from -v "$(pwd)/data/minio:/to" alpine cp -a /from/. /to/On Windows, this command silently does nothing if run from Git Bash without precautions.
Git Bash's MSYS layer rewrites any argument containing a /-prefixed path before it reaches
docker.exe — including the /from and /to container-side paths above, not just the host side —
turning them into nonsense like C:\Program Files\Git\from. Docker then either errors or creates
an empty directory at whatever bogus location that resolves to, and the cp "succeeds" without
copying anything into the real data/postgres/data/minio — which looks exactly like the data
being lost, when what actually happened is the migration never ran against the right path. Either:
- Run the command from PowerShell or cmd.exe instead, where this rewriting doesn't happen at all, or
- Prefix it with
MSYS_NO_PATHCONV=1if you're staying in Git Bash:MSYS_NO_PATHCONV=1 docker run --rm -v opengauge_postgres_data:/from ...
A telltale sign this has already happened to you: an empty, oddly-named directory next to
data/postgres (e.g. data/postgres;C) — harmless by itself, but a sign the copy silently
no-opped. Your original data is still safe in the old named volume (docker volume ls will list
it) until you docker volume rm it yourself; re-run the migration correctly (see above) and it'll
show up in data/postgres/data/minio.
Verifying persistence
scripts/verify-media-persistence.sh (at the repo root) automates the check above: it starts
throwaway db/minio containers under an isolated Compose project name and a temp data
directory (never your real infrastructure/docker/data/), writes a marker row/object, runs a full
docker compose down + up --build, and confirms both survive. It exits non-zero if either one
doesn't, so it's safe to wire into CI or run after touching anything in docker-compose.yml:
scripts/verify-media-persistence.shIt's fully isolated from any real running deployment — safe to run alongside a live instance.
The Dangerous zone's Clear database action deletes every organization, asset, location,
procedure, and calibration, and every non-superadmin user, and empties file storage — keeping
only superadmin accounts. It's the way to take a populated demo/trial install back to the empty
state a fresh deployment starts in, without reinstalling. Requires typing RESET to confirm and
cannot be undone.
Running database migrations
Migrations use Alembic and live in apps/api/migrations/versions/:
docker compose -f infrastructure/docker/docker-compose.yml exec api alembic upgrade headRun this after pulling a new Open Gauge version that includes schema changes, before restarting the
api service on the new image.
Updating Open Gauge
git pull
docker compose -f infrastructure/docker/docker-compose.yml up -d --build
docker compose -f infrastructure/docker/docker-compose.yml exec api alembic upgrade headRebuilding is safe to run even when nothing changed — Docker will reuse cached layers.