Deployment
Production stack = Postgres + API + Web, all containers, fronted by a reverse proxy of your choice (Caddy recommended for the automatic HTTPS).
Stack overview
Internet
│
▼
┌──────────────────────┐
│ Reverse proxy │ ← Caddy / Nginx / Traefik
│ (TLS termination) │ terminates HTTPS, routes by Host
└──────┬──────────┬─────┘
│ │
hyprbox.example api.hyprbox.example
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ web │ │ api │ ← docker-compose.prod.yml
│ :3000 │ │ :4000 │
└──────────┘ └────┬─────┘
│
▼
┌──────────┐
│ postgres │
│ :5432 │ (loopback-only by default)
└──────────┘
Prerequisites
- A Linux host with Docker 24+ and Docker Compose v2.
- Two DNS records pointing at the host:
hyprbox.example.com→ webapi.hyprbox.example.com→ API
- Optional: a wildcard cert if you'd rather terminate TLS at a single name.
First boot
git clone https://github.com/your-org/hyprbox.git
cd hyprbox
cp .env.production.example .env.production
Edit .env.production:
POSTGRES_USER=hyprbox
POSTGRES_PASSWORD=<openssl rand -base64 24>
POSTGRES_DB=hyprbox
JWT_SECRET=<openssl rand -base64 48> # MUST be ≥ 32 chars in prod
CORS_ORIGIN=https://hyprbox.example.com
NEXT_PUBLIC_HYPRBOX_API_URL=https://api.hyprbox.example.com
API_BIND=127.0.0.1 # reverse proxy lives on the same host
WEB_BIND=127.0.0.1
Then:
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build
The API container runs prisma db push on first boot so the schema lands
automatically. Seed the admin user once:
docker compose -f docker-compose.prod.yml exec api \
node node_modules/tsx/dist/cli.mjs prisma/seed-admin.ts
Default credentials: admin@hyprbox.local / hyprbox-admin. Change the
password immediately through the dashboard once you've logged in
(/dashboard/settings → ChangePassword landing in Phase 4; for now, register
a new user via POST /api/auth/register and stop using the default).
Reverse proxy — Caddy example
# /etc/caddy/Caddyfile
hyprbox.example.com {
reverse_proxy 127.0.0.1:3000
}
api.hyprbox.example.com {
reverse_proxy 127.0.0.1:4000
# SSE: don't buffer.
@sse path /api/stream/* /api/jobs/*/stream
reverse_proxy @sse 127.0.0.1:4000 {
flush_interval -1
}
}
Caddy issues Let's Encrypt certs automatically. Restart with caddy reload.
Nginx alternative
server {
listen 443 ssl http2;
server_name api.hyprbox.example.com;
# ssl_certificate / ssl_certificate_key here
location /api/stream/ {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
}
location / {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
server {
listen 443 ssl http2;
server_name hyprbox.example.com;
location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; }
}
Environment variables (production)
| Variable | Required | Default | Notes |
|---|---|---|---|
POSTGRES_PASSWORD |
yes | — | Hard-fails the compose validate if empty. |
JWT_SECRET |
yes | — | ≥ 32 chars or the API refuses to boot. |
CORS_ORIGIN |
recommended | http://localhost:3000 |
Set to the web's public URL. |
NEXT_PUBLIC_HYPRBOX_API_URL |
yes | — | Baked into the JS bundle at build time. |
HYPRBOX_REQUIRE_NODE_TOKEN |
implicit | true in prod |
Compose forces it on; rejects anonymous heartbeats. |
HYPRBOX_ENABLE_DOCS |
opt | false in prod |
true exposes /docs (Swagger UI). |
API_BIND / WEB_BIND |
opt | 127.0.0.1 / 0.0.0.0 |
Listen address for the host-side port binding. |
Updates
git pull
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build
Migrations apply via prisma db push on container boot. There's no rollback
mechanism yet — back up the database before any upgrade that crosses a
schema-changing release:
docker compose -f docker-compose.prod.yml exec postgres \
pg_dump -U hyprbox -d hyprbox > backup-$(date +%Y%m%d-%H%M).sql
Logs
docker compose -f docker-compose.prod.yml logs -f api
docker compose -f docker-compose.prod.yml logs -f web
docker compose -f docker-compose.prod.yml logs -f postgres
API logs are JSON in prod (pino default). Pipe through jq for inspection:
docker compose -f docker-compose.prod.yml logs api --no-log-prefix \
| jq 'select(.level >= 40)' # warn + error only
Healthchecks
Every container exposes a healthcheck:
- API:
wget -q -O- http://127.0.0.1:4000/health - Web:
wget -q -O- http://127.0.0.1:3000/login - Postgres:
pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
docker compose ps shows live status. Combine with a host-level prober (the
monitoring-only preset is designed for this — Prometheus' blackbox_exporter).
Scaling
Single-process today. To scale horizontally:
- Web: stateless, scale to N replicas behind a load balancer.
- API: each instance owns its own SSE bus, so you'd need sticky sessions
(or swap the
EventEmitterfor Redis pub/sub inapps/api/src/lib/events.ts). The/api/jobs/pendingatomic claim is safe across instances — Postgres serialises it. - Postgres: single primary is fine well past where the rest will bottleneck. Consider managed Postgres (RDS, Cloud SQL) before vertical-scaling.
Removing the stack cleanly
docker compose -f docker-compose.prod.yml down # keeps the volume
docker compose -f docker-compose.prod.yml down -v # nukes the DB too