HyprBox docs GitHub ↗

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 → web
    • api.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 EventEmitter for Redis pub/sub in apps/api/src/lib/events.ts). The /api/jobs/pending atomic 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