Skip to content

Deployment

Deploy the Pug stack with Docker Compose or Kubernetes, including migrations and health checks.

Updated View as Markdown

Production runbook for the backend — build it, migrate the stores, run each process, and harden for production traffic. The dashboard has its own build and deploy; see Dashboard.

Before deploying: meet all Requirements and set every variable in Configuration. The make commands below run from the backend repo root.

1. Build

You can ship the backend as binaries (systemd, bare processes) or container images (Kubernetes, Compose). Pick one.

Option A — binaries

make build

make build compiles the following into bin/. bin/pug is the unified CLI; every other binary is a single-purpose build of one process — an alternative to the matching pug sub-command for deployment tools that prefer one binary per unit.

Binary Equivalent to Role
bin/pug Unified CLI — all sub-commands in one binary
bin/pug-server pug server API server
bin/pug-migrate-postgres pug postgres migrate Postgres migration runner
bin/pug-migrate-nats pug nats migrate NATS stream initialiser
bin/pug-migrate-clickhouse pug clickhouse migrate ClickHouse migration runner
bin/pug-worker-events pug worker events Events worker — writes events to ClickHouse
bin/pug-worker-email pug worker email Transactional email worker
bin/pug-worker-compliance pug worker compliance Compliance worker (GDPR/DPDP)
bin/pug-worker-profile-identify pug worker profile identify Profile identify worker
bin/pug-worker-profile-alias pug worker profile alias Profile alias worker
bin/pug-worker-profile-upsert pug worker profile upsert Profile upsert worker
bin/pug-worker-demo pug worker demo Demo-traffic generator (only for public demos)

There is no make migrate or make dev target. Migrations run via the pug CLI or the standalone migrate binaries (below); local all-in-one runs via the pug dev command. See the CLI reference.

Option B — container images

The repo ships one parameterized Dockerfile that builds a right-sized image for any role. Two build args select the role:

  • --build-arg CMD=<path under ./cmd> — which main to compile.
  • --target <app|worker|migrate> — the runtime shape (what, besides the binary, lands in the image).

The three targets exist because the roles need different files at runtime:

Target Contains Used by
app distroless + the binary only The server (reads nothing from schema/)
worker binary + schema/nats Workers (read schema/nats/consumers.yaml at startup)
migrate binary + schema/ migration assets The migrate runners

Each image’s entrypoint is that single-purpose binary, so you run the container with no arguments. Images are multi-arch (amd64 + arm64, cross-compiled — no QEMU) and run as distroless nonroot.

# API server
docker build --build-arg CMD=server            --target app     -t pug-server .

# a worker (swap the CMD path for any worker below)
docker build --build-arg CMD=workers/events    --target worker  -t pug-worker-events .

# a migration runner
docker build --build-arg CMD=migrate/postgres  --target migrate -t pug-migrate-postgres .

CMD paths, relative to ./cmd:

Role CMD Target
Server server app
Events worker workers/events worker
Email worker workers/email worker
Compliance worker workers/compliance worker
Profile identify / alias / upsert workers/profile/identify · workers/profile/alias · workers/profile/upsert worker
Demo worker workers/demo worker
Postgres / NATS / ClickHouse migrate migrate/postgres · migrate/nats · migrate/clickhouse migrate

2. Migrations

Run migrations once against each store before the first boot, and again on every upgrade. They are idempotent — re-running them on an up-to-date store is safe.

# unified CLI
./bin/pug postgres migrate
./bin/pug nats migrate
./bin/pug clickhouse migrate

# or the standalone binaries / migrate images
./bin/pug-migrate-postgres
./bin/pug-migrate-nats
./bin/pug-migrate-clickhouse

All migration commands read connection details from the environment (DATABASE_URL, NATS_URL, CLICKHOUSE_URL). postgres migrate and clickhouse migrate default to applying all pending migrations up (--direction up); pass --direction down --num N to roll back N steps. nats migrate initializes the JetStream streams and consumers.

3. Run

Production topology: one process per role, each as its own replica. Do not use pug dev in production — it runs every component in a single process with no independent scaling or restart boundaries.

API server

./bin/pug server        # or ./bin/pug-server, or the `app` image

Listens on :3000 (override with PUG_SERVER_PORT). Place a TLS-terminating load balancer in front — see Production hardening.

Workers

There are 6 worker processes in a normal deployment (the demo worker is only for public demos). Run each as its own replica, using whichever form fits your tooling:

Process Unified command Standalone binary Docker CMD
Events pug worker events pug-worker-events workers/events
Email pug worker email pug-worker-email workers/email
Compliance pug worker compliance pug-worker-compliance workers/compliance
Profile identify pug worker profile identify pug-worker-profile-identify workers/profile/identify
Profile alias pug worker profile alias pug-worker-profile-alias workers/profile/alias
Profile upsert pug worker profile upsert pug-worker-profile-upsert workers/profile/upsert
Demo (optional) pug worker demo pug-worker-demo workers/demo

Scale the events worker horizontally for high ingest — run multiple replicas (see Horizontal scaling):

./bin/pug worker events        # or ./bin/pug-worker-events

Running a single small node? pug dev (alias pug start) runs the server and all workers in one process. It’s built for local dev, but it’s a legitimate way to stand up a low-volume single-box instance — you just give up independent scaling and restarts. See the CLI reference.

4. Production hardening

TLS termination

pug server listens on plain HTTP (:3000). Terminate TLS at a load balancer (nginx, Caddy, AWS ALB, Cloudflare) and proxy to port 3000. Do not expose port 3000 directly to the internet — the SDKs and dashboard send credentials (x-api-key, Authorization JWT), so TLS is required.

Secrets management

Do not put secrets in unencrypted files. Inject the following via your secrets manager (Vault, AWS Secrets Manager, Kubernetes Secrets, etc.):

Secret Variable Notes
JWT signing key PUG_JWT_SECRET_KEY openssl rand -hex 32. Rotating it invalidates all dashboard sessions.
Email provider encryption key PUG_EMAIL_PROVIDER_SECRET_KEY openssl rand -base64 32. Required for per-tenant email providers.
Postgres credentials DATABASE_URL Use a dedicated Pug database user with restricted privileges.
ClickHouse credentials CLICKHOUSE_URL Include in the connection URL (clickhouse://user:pass@host:9000/pug).
NATS credentials NATS_URL Secure NATS with credentials/NKeys or TLS per your NATS deployment.

CORS origins

Set PUG_CORS_ORIGINS to the exact origin(s) your dashboard runs on (e.g. https://app.example.com). Using * disables credentialed requests, which breaks dashboard sign-in.

NATS JetStream HA

Run a 3-node NATS cluster with JetStream on each node — a single node is a single point of failure for all ingestion. Start each with --jetstream --store_dir /data/nats, configure --cluster routes, and use a stream replication factor ≥ 3 so one node’s loss doesn’t lose data.

ClickHouse HA and storage

A single ClickHouse node is fine for low-to-medium volumes. For higher volumes or fault tolerance:

  • Replication — run replicated tables backed by ClickHouse Keeper (or ZooKeeper) so a node loss doesn’t lose data.
  • Tiered storage — an S3-compatible object store as the “cold” tier for high-retention deployments.
  • ulimits — ensure nofile ≥ 262144 on all ClickHouse nodes (required).

Horizontal scaling

The events worker is the primary bottleneck at high ingest rates. Each replica is a stateless NATS JetStream consumer and scales linearly — run multiple. Other workers process lower-volume streams; start with one replica each and scale as needed.

Monitoring

Point OTEL_EXPORTER_OTLP_ENDPOINT at your collector (Grafana, Datadog, Honeycomb, …) to export traces, metrics, and logs. With it unset, Pug logs to stdout as text. See Configuration → Telemetry.

Health checks

Both the server and every worker expose Kubernetes-style probes. Wire your orchestrator’s liveness/readiness probes (or load-balancer health checks) to these:

Process Endpoint Port Behavior
pug server GET /healthz :3000 Liveness200 ok while the process is serving. Does not check dependencies (a dependency blip must not restart-cascade the fleet).
pug server GET /readyz :3000 Readiness — pings Postgres (reader + writer), ClickHouse, Dragonfly, and NATS. 200 JSON when all are reachable, 503 otherwise (pod is pulled from rotation, not killed).
Each worker GET /healthz :8090 Liveness200 while its consume loops run, 503 if wedged → restart.
Each worker GET /readyz :8090 Readiness200 once started and connected to NATS, 503 otherwise → gate rollouts.

The worker probe address is PUG_WORKER_HEALTH_ADDR (default :8090; set to off to disable). When several worker binaries share a host, give each a distinct port to avoid a bind clash. pug dev disables worker probes (it’s for local use).

Backing services keep their own health endpoints — probe those for infrastructure liveness:

Service Health endpoint
NATS GET http://<host>:8222/healthz
ClickHouse GET http://<host>:8123/ping
PostgreSQL pg_isready / TCP connect to :5432
Dragonfly redis-cli ping

Further reading

Navigation

Type to search…

↑↓ navigate↵ selectEsc close