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

ConcernQuickstart (demo)Production
Store rolegmonk superuser — RLS bypassedmigrate as owner; services as non-superuser gmonk_app
Store DBthrowaway container, sslmode=disableexternal / HA Postgres, sslmode=require
UI authnone (open bind)GMONK_WEB_TOKEN or SSO/OIDC, behind TLS
Before-imageslocal volumeS3 / MinIO + allowlist
Gateadviseblock or guarantee
Proxynot runninggmonk 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 localdb profile below.
  • An S3 bucket (or MinIO) for before-images.
  • Docker on the host that will run gmonk.

Get the files

bash
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:

bash
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:

sql
ALTER ROLE gmonk_app LOGIN PASSWORD 'a-strong-secret';
Why two roles? RLS (even 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:

bash
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:

bash
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:

bash
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 block mode (GMONK_GATE) — unrecoverable writes are refused. Use guarantee for 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 ./certs and append --tls-key /certs/proxy.key --tls-cert /certs/proxy.crt to the proxy command.
  • Protecting MySQL? Use a mysql:// APP_DB_URL — the proxy auto-detects it. A managed DB you can't proxy? Use the managed profile (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:

bash
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 gc runs a daily mark-sweep of unreferenced before-images (uses the admin URL for the cross-tenant view).
  • Alerts — set GMONK_ALERT_WEBHOOK for 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-run migrate (idempotent, as owner), then up -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 enforce applied; alerts wired; GC scheduled
  • ✓ Demo gmonk:gmonk credentials never used
Verified vs not: this compose is config-validated and its commands match the engine's entrypoint and documented flags. It has not been run end-to-end against a real managed store + S3 + target here — validate it in a staging environment before production, and tell us what breaks.