Operate
Production deployment
The quickstart gets you a feel for gmonk on one machine with insecure defaults. This is the hardened topology you actually run in production — external store, least-privilege roles, TLS, S3, an authenticated UI, and the wire proxy as a long-lived service.
Quickstart vs production
| Concern | Quickstart (demo) | Production |
|---|---|---|
| Store role | gmonk superuser — RLS bypassed | migrate as owner; services as non-superuser gmonk_app |
| Store DB | throwaway container, sslmode=disable | external / HA Postgres, sslmode=require |
| UI auth | none (open bind) | GMONK_WEB_TOKEN or SSO/OIDC, behind TLS |
| Before-images | local volume | S3 / MinIO + allowlist |
| Gate | advise | block or guarantee |
| Proxy | not running | gmonk protect as a service, client auth + TLS |
Prerequisites
- A Postgres for gmonk's store — managed/HA recommended (it's a fail-closed hard dependency). Self-hosting? Use the
localdbprofile below. - An S3 bucket (or MinIO) for before-images.
- Docker on the host that will run gmonk.
Get the files
curl -O https://gmonk.dev/docker-compose.prod.yml
curl -o .env https://gmonk.dev/gmonk.env.example
chmod 600 .env
# edit .env — fill in every CHANGE_ME 1 · Provision the store + roles
gmonk uses two roles on the same store database: an
owner (DDL, for migration + GC) and the non-superuser
gmonk_app (runtime, so tenant RLS is enforced). Create the store
database and an owner role in your managed Postgres, put its URL in
GMONK_STORE_ADMIN_URL, then apply the schema:
docker compose -f docker-compose.prod.yml --env-file .env run --rm migrate
The schema creates gmonk_app as NOLOGIN (no secret
in the schema). Grant it a login once, out of band, then set
GMONK_STORE_APP_URL to those credentials:
ALTER ROLE gmonk_app LOGIN PASSWORD 'a-strong-secret'; FORCE) is bypassed by
superusers and table owners. Pointing the running services at a superuser
silently disables tenant isolation — so the services connect as
gmonk_app, and only migration/GC use the owner/admin URL.
No managed Postgres? Add --profile localdb to
run a store container (set LOCALDB_OWNER_PASSWORD and point the
URLs at host store — see the commented block in
.env). Bring it up first: docker compose -f
docker-compose.prod.yml --profile localdb up -d store, then migrate.
2 · Configure the object store
Before-images of file/object changes go to S3 (durable, survives host loss).
Set GMONK_S3_BUCKET/_REGION and credentials in
.env, and add an allowlist as a fail-closed perimeter:
GMONK_S3_BUCKET=my-gmonk-before-images
GMONK_S3_REGION=us-east-1
GMONK_OBJECT_ALLOW_BUCKETS=my-gmonk-before-images
# MinIO instead: GMONK_S3_ENDPOINT=https://minio.internal:9000 + GMONK_S3_FORCE_PATH_STYLE=1 3 · Bring up the authenticated review UI
The UI can apply reversals, so it must never be exposed without auth. Set
either GMONK_WEB_TOKEN or the
GMONK_OIDC_* block in .env, then:
docker compose -f docker-compose.prod.yml --env-file .env up -d gmonk
It binds 127.0.0.1:4319. Put a TLS-terminating reverse proxy
(Caddy, nginx, or a cloud load balancer) in front and route your team to it
over HTTPS. With a token, open the UI as https://…/?token=<secret>;
with OIDC, users sign in with your IdP (roles map to viewer/operator/admin).
4 · Run the wire proxy in front of your database
This is the endpoint your agent connects to. Set APP_DB_URL (the
database to protect) and GMONK_PROXY_PASSWORD (what agents
present), then start it with the protect profile:
docker compose -f docker-compose.prod.yml --env-file .env --profile protect up -d proxy
# then point your agent at gmonk instead of the database:
# postgres://agent@<gmonk-host>:6432/appdb - The gate runs in
blockmode (GMONK_GATE) — unrecoverable writes are refused. Useguaranteefor prove-then-commit. - Backend TLS to a remote target is required by default — gmonk won't send its credentials in cleartext.
- For agents connecting over a network, terminate client TLS too: put certs in
./certsand append--tls-key /certs/proxy.key --tls-cert /certs/proxy.crtto the proxy command. - Protecting MySQL? Use a
mysql://APP_DB_URL— the proxy auto-detects it. A managed DB you can't proxy? Use themanagedprofile (WAL drainer) instead.
5 · Make gmonk the only way in
Capture is only complete if the agent can't sidestep the proxy. Revoke the agent account's direct DML so gmonk is the sole path:
docker compose -f docker-compose.prod.yml --env-file .env run --rm proxy enforce --agent-role agent_rw
On a protected table, even a write that bypasses the proxy is still captured,
flagged, and reversible — and fires an out_of_band alert.
6 · Operate
- Retention / GC —
--profile gcruns a daily mark-sweep of unreferenced before-images (uses the admin URL for the cross-tenant view). - Alerts — set
GMONK_ALERT_WEBHOOKfor approvals, blocks, and recoverability decay. - Backups — back up the store Postgres and the S3 bucket; an undo needs both. The store is fail-closed — run it HA.
- Upgrades — bump
GMONK_IMAGE, re-runmigrate(idempotent, as owner), thenup -d.
Production checklist
- ✓ Services connect as
gmonk_app, not a superuser - ✓ Store over TLS (
sslmode=require), HA, backed up - ✓ UI behind auth (token/OIDC) and a TLS reverse proxy
- ✓ Before-images in S3 +
GMONK_OBJECT_ALLOW_BUCKETS - ✓ Gate in
block/guarantee; client auth (+TLS) on the proxy - ✓
gmonk enforceapplied; alerts wired; GC scheduled - ✓ Demo
gmonk:gmonkcredentials never used