Documentation

Production compose

One host, Docker Compose, HTTPS. For a startup or a team that wants Guard Spec on a single VM without Kubernetes. The quick start (docker-compose.yml at the repository root) publishes every port over plain HTTP; this stack does not.

Quick start (root) This stack
Published ports 8080, 9090, 18081, 9091 (plain HTTP) 80 and 443 (Caddy) only
TLS none Caddy: automatic HTTPS (Let's Encrypt, ZeroSSL) or its internal CA
Limits none CPU and memory per service, sized from docs/sizing.md
Logs Docker defaults (unbounded) json-file, 10 MB x 5 per container
Restart none unless-stopped
Health checks gateway every long-running service; Caddy starts after the gateway and control plane are healthy
Missing models warn, run on L1 refuse to start (DETECTOR_REQUIRE_ML=true)
Postgres default network internal network, no route outside
internet ──443──> caddy ──> gateway:8080        (GATEWAY_HOST)
                        └─> control-plane:8081  (ADMIN_HOST: admin UI at /ui/, API at /v1)
                   edge network                  backend network (internal)
                   gateway, control-plane ─────> postgres

Start

You need a host with Docker and Compose v2, this repository (or a release source archive: the stack uses migrations/ and deploy/postgres/set-app-password.sh from it), and two DNS names pointing at the host, with ports 80 and 443 open.

cd deploy/compose/production
cp .env.example .env && chmod 600 .env
# Fill in .env: GATEWAY_HOST, ADMIN_HOST, CADDY_TLS=<your email>, and every
# secret (openssl rand -hex 24 for passwords, openssl rand -hex 32 for
# JWT_SECRET, openssl rand -base64 32 for MFA_ENC_KEY), ADMIN_EMAIL and
# ADMIN_PASSWORD.
docker compose up -d --wait

The first start pulls the images and the models (about 270 MB; 135 MB with DETECTOR_LAYERS=l1,l2), runs the migrations, and Caddy gets certificates for both names. Then:

curl -s https://$GATEWAY_HOST/readyz      # "layers":["l1","l2","l3"]
open https://$ADMIN_HOST/ui/              # log in with ADMIN_EMAIL / ADMIN_PASSWORD

Create an API key in the admin UI (or with the API, as in the README quick start, with https://$ADMIN_HOST/v1 instead of http://localhost:18081/v1), and point your applications at https://$GATEWAY_HOST/v1 (docs/integrations.md).

Testing with Caddy's internal CA

Without public DNS (a laptop, a private network, CI), let Caddy issue certificates from its own CA:

# .env
GATEWAY_HOST=gw.localhost
ADMIN_HOST=admin.localhost
CADDY_TLS=internal
HTTPS_PORT=8443        # if 443 is taken
HTTP_PORT=8480
docker compose up -d --wait
# Either skip verification ...
curl -sk --resolve gw.localhost:8443:127.0.0.1 https://gw.localhost:8443/readyz
# ... or trust Caddy's root certificate.
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-root.crt
curl -s --cacert caddy-root.crt --resolve gw.localhost:8443:127.0.0.1 https://gw.localhost:8443/readyz

Clients then need caddy-root.crt (Python: SSL_CERT_FILE or REQUESTS_CA_BUNDLE; Node: NODE_EXTRA_CA_CERTS). The root lives in the caddy_data volume and is kept by scripts/backup.sh. With a port other than 443, Caddy's HTTP-to-HTTPS redirect points at port 443; use the HTTPS port directly.

Settings

Everything is in .env (.env.example lists it):

Variable Default Notes
GATEWAY_HOST, ADMIN_HOST required Two names; the control plane is not served on the gateway's name
CADDY_TLS required Your email (ACME) or internal
ADMIN_ALLOW 0.0.0.0/0 ::/0 Client ranges allowed on ADMIN_HOST, such as your VPN. Needs Docker to pass real client addresses (Linux default; not Docker Desktop)
DETECTOR_LAYERS l1,l2,l3 l1,l2 is light mode (docs/sizing.md)
GATEWAY_CPUS, GATEWAY_MEMORY 2, 2g Measured peaks: full detector 0.9 GiB, light 0.5 GiB
INFERENCE_MAX_CONCURRENCY, INFERENCE_INTRA_OP_THREADS 2, 1 Model runs at once x threads each; keep the product at or below GATEWAY_CPUS
CONTROL_PLANE_CPUS, CONTROL_PLANE_MEMORY 1, 256m
POSTGRES_CPUS, POSTGRES_MEMORY 2, 1g
CADDY_CPUS, CADDY_MEMORY 1, 256m
LOG_MAX_SIZE, LOG_MAX_FILE 10m, 5 Per container
EDGE_SUBNET 172.30.89.0/24 The control plane trusts X-Forwarded-For from it (Caddy); change it if it collides with your networks
GUARDSPEC_VERSION 0.1.1 Image tag (docs/upgrade.md)

UPSTREAM_KEY_ORGS has no default here (the quick start lets every org use UPSTREAM_KEY_MAIN), and UPSTREAM_ALLOW_PRIVATE is false: set them deliberately, as the README's Routing section explains.

Metrics (gateway :9090, control plane :9091) are not published. Run Prometheus on the edge network (<project>_edge) and scrape gateway:9090 and control-plane:9091.

Operations

What was checked

On an Apple Silicon (arm64) Mac with Docker Desktop, with CADDY_TLS=internal: the stack starts healthy with only Caddy publishing ports; /readyz reports l1,l2,l3 over HTTPS, verified against Caddy's exported root; a benign prompt passes (200), an injection is blocked (400), the admin UI loads; a backup restored into a fresh compose project keeps the organization, users, API keys, policies, audit rows and Caddy's CA. Automatic HTTPS with a public ACME CA was not exercised (it needs public DNS); the Caddyfile adapts to an ACME policy for both names (caddy adapt).

Source: deploy/compose/production/README.md @ ca69dad29f02