Deployment
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 buildmake 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 migrateormake devtarget. Migrations run via thepugCLI or the standalone migrate binaries (below); local all-in-one runs via thepug devcommand. 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>— whichmainto 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-clickhouseAll 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` imageListens 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 |
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-eventsRunning a single small node?
pug dev(aliaspug 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— ensurenofile≥ 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 | Liveness — 200 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 | Liveness — 200 while its consume loops run, 503 if wedged → restart. |
| Each worker | GET /readyz | :8090 | Readiness — 200 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
- Dashboard — build and serve the UI
- Connect your SDKs — point your apps at this instance
- CLI reference — every
pugcommand and flag - Configuration — all environment variables