Open Gauge
Self-hosting

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, and 5432 free (or remapped — see below).

Bring the stack up

cd infrastructure/docker
docker compose up -d

This builds and starts every service defined in infrastructure/docker/docker-compose.yml.

Services

ServiceImage / buildPort(s)Purpose
dbpostgres:15-alpine5432The single source of truth for all structured data.
apiapps/api/Dockerfile8000FastAPI backend (/api/v1/...), OpenAPI schema at /openapi.json.
webapps/web/Dockerfile3000The Open Gauge user interface.
docsapps/docs/Dockerfile3002This documentation site (Knowledge Center + API Reference).
admineradminer:48080Lightweight web UI for inspecting the Postgres database directly.
miniominio/minio9000 (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:

VariableServicePurpose
HOST_IPweb, docs, minio URLsHostname/IP the app is reachable at; used to derive the URLs below.
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DBdb, apiPostgres credentials and database name.
SECRET_KEYapiSigns session/auth tokens — change this in production.
FRONTEND_URLapiPublic app URL; encoded into generated QR codes and asset labels — point this at your production domain, not localhost.
CORS_ORIGINSapiJSON array of origins allowed to call the API (must include your web URL).
MINIO_ROOT_USER / MINIO_ROOT_PASSWORDminio, apiObject storage credentials.
MINIO_BUCKETapiBucket used for calibration certificates, datasheets, and uploads.
MINIO_PUBLIC_URLapiThe externally-reachable MinIO URL used for presigned certificate/file links.
NEXT_PUBLIC_API_URLweb (build arg)The browser-reachable API URL, baked in at build time.
API_INTERNAL_URLweb, docsThe 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_URLweb (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:

  1. PostgreSQL (infrastructure/docker/data/postgres) — run pg_dump/pg_restore against the db service directly, or back up the directory itself with any regular file-level tool.
  2. MinIO (infrastructure/docker/data/minio) — back up the directory directly, or use mc mirror against 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=1 if 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.sh

It'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 head

Run 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 head

Rebuilding is safe to run even when nothing changed — Docker will reuse cached layers.

On this page