---
title: "Deployment"
description: "Deploy the Pug stack with Docker Compose or Kubernetes, including migrations and health checks."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.pug.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/self-hosting/dashboard).

Before deploying: meet all [Requirements](/self-hosting/requirements) and set every variable in [Configuration](/self-hosting/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

```bash
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](/self-hosting/cli).

### 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`.

```bash
# 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.

```bash
# 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

```bash
./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](#4-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](#horizontal-scaling)):

```bash
./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](/self-hosting/cli#dev).

## 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](/self-hosting/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](/self-hosting/dashboard) — build and serve the UI
- [Connect your SDKs](/self-hosting/connect-sdks) — point your apps at this instance
- [CLI reference](/self-hosting/cli) — every `pug` command and flag
- [Configuration](/self-hosting/configuration) — all environment variables

Source: https://docs.pug.sh/self-hosting/deployment/index.mdx
