# Backend — Docker Deployment Guide Operational guide for building, deploying, and observing the Halo Bestie backend as a **Docker container** on self-hosted infra (VPS, Docker Engine, or Kubernetes). **Not** Cloud Run / serverless. > Architecture / env-var reference: [../requirement/deployment.md](../requirement/deployment.md) · [.env.example](.env.example) --- ## 1. What gets deployed A single image (multi-stage [Dockerfile](Dockerfile)) running `node src/server.js`, which starts **two listeners** ([src/server.js](src/server.js)): | Listener | Bind | Port | Exposed? | |---|---|---|---| | Public API (`client_app` + `mitra_app`) | `0.0.0.0` | `PUBLIC_PORT` (default **3000**) | **Yes** — publish this | | Internal API (control center) | `INTERNAL_HOST` (default `127.0.0.1`) | `INTERNAL_PORT` (default 3001) | **No** — loopback only, never publish | Runtime image: Node 20 (bookworm-slim), prod-only deps, runs as non-root `node`, native `bcrypt` precompiled in the build stage. --- ## 2. Install Docker (one-time, on the host) ### Ubuntu / Debian VPS ```bash # Remove any old packages, then install Docker Engine from the official repo curl -fsSL https://get.docker.com | sh # Run docker as your user without sudo (log out/in afterward) sudo usermod -aG docker "$USER" # Verify docker version docker compose version # Compose v2 ships as a plugin with modern Docker ``` ### Kubernetes No Docker Engine needed on nodes — just push the image to a registry your cluster can pull from (see §4) and apply your manifests (§7). --- ## 3. Configure environment Create `backend/.env.staging` (or `.env.production`) from the template — **never commit it**: ```bash cp .env.example .env.staging ``` Fill in at minimum (full list in [.env.example](.env.example)): | Var | Notes | |---|---| | `PUBLIC_PORT` | `3000` (keep default unless your proxy expects otherwise) | | `INTERNAL_HOST` / `INTERNAL_PORT` | leave default `127.0.0.1:3001` — keeps control center private | | `DATABASE_URL` | Postgres connection string | | `VALKEY_URL` | `redis://:6379` | | `SERVER_TZ` | `UTC` | | `AUTH_JWT_SECRET` | **fresh per environment** — never reuse prod's | | `FIREBASE_SERVICE_ACCOUNT_PATH` | path to the **mounted** SA JSON, e.g. `/secrets/firebase-sa.json`. Must be from the env's Firebase project (staging = `my-bestie-876ec`) | | `XENDIT_ENABLED` | `false` until test keys + webhook are wired | | `CC_ORIGIN`, `ADMIN_EMAIL`, `ADMIN_PASSWORD` | control-center access | > Secrets (`.env`, the Firebase SA JSON) are provided at **runtime** via `--env-file` / volume mounts / k8s Secrets. They are **not** baked into the image (`.dockerignore` excludes `.env*`). --- ## 4. Build & push the image ```bash # From the repo root docker build -t /halobestie-backend:staging ./backend # Push to your registry (Docker Hub, GHCR, GCP Artifact Registry, self-hosted, …) docker push /halobestie-backend:staging ``` For a purely single-host setup you can skip the registry and build directly on the host. --- ## 5. Run database migrations (one-off) Run **before** (re)starting the service. Never auto-migrate on container boot — concurrent replicas would race. ```bash # Migrate (every deploy that includes new migrations) docker run --rm --env-file backend/.env.staging \ /halobestie-backend:staging node src/db/migrate.js # Seed (first deploy only) docker run --rm --env-file backend/.env.staging \ /halobestie-backend:staging node src/db/seed.js ``` --- ## 6. Deploy — plain Docker Engine ```bash docker run -d --name halobestie-staging \ --env-file backend/.env.staging \ -p 3000:3000 \ -v /opt/halobestie/secrets/firebase-sa.json:/secrets/firebase-sa.json:ro \ --restart unless-stopped \ --log-driver json-file --log-opt max-size=10m --log-opt max-file=5 \ /halobestie-backend:staging ``` - Publish **only** `3000`. Do **not** map `3001`. - `--log-opt` enables log rotation — see §8. - Put a TLS-terminating reverse proxy (Nginx / Traefik / Caddy) in front for `https://staging-api.halobestie.com`. WebSocket upgrade must be proxied (the apps use `/api/shared/ws`). ### Or with Docker Compose A ready-to-use [docker-compose.staging.yml](docker-compose.staging.yml) is included (backend only — Postgres/Valkey are expected via `DATABASE_URL`/`VALKEY_URL`). It publishes only `3000`, mounts the Firebase SA + log volume, and sets json-file rotation. Point it at your image via the `BACKEND_IMAGE` env var (or uncomment `build: .` to build on the host): ```bash cd backend cp .env.example .env.staging # then fill it in BACKEND_IMAGE=/halobestie-backend:staging \ docker compose -f docker-compose.staging.yml up -d ``` --- ## 7. Deploy — Kubernetes (sketch) - **Deployment** with the image, `envFrom` a Secret/ConfigMap, the Firebase SA JSON mounted from a Secret volume at `/secrets/firebase-sa.json`. - **Service** exposing only container port `3000`. Liveness/readiness probes: `tcpSocket: { port: 3000 }` (no HTTP health route — TCP probe matches the Dockerfile HEALTHCHECK). - Migrations: a one-off **Job** (`command: ["node","src/db/migrate.js"]`) run before rolling out, not an initContainer on every pod. - Logs: pods write to stdout (§8) — your cluster's node agent (Fluent Bit / Loki / Cloud Logging) collects them automatically. --- ## 8. Logs — where they go and how to map them ### 8a. Application logs → **stdout/stderr** (the Docker-native way) The backend uses Fastify's pino logger (`logger: true` in [src/app.public.js](src/app.public.js) / [src/app.internal.js](src/app.internal.js)), emitting **structured JSON to stdout**, plus a few `console.log` lifecycle lines. It does **not** write its own app-log files. So: ```bash # Tail live docker logs -f halobestie-staging # Last 200 lines docker logs --tail 200 halobestie-staging # Pretty-print the JSON (pino output is one JSON object per line) docker logs -f halobestie-staging | jq . # Compose equivalent docker compose -f docker-compose.staging.yml logs -f backend ``` **Rotation (important — default json-file logs grow unbounded):** set it per-container as in §6 (`--log-opt max-size=10m --log-opt max-file=5`), or globally in `/etc/docker/daemon.json`: ```json { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "5" } } ``` then `sudo systemctl restart docker`. **Ship logs off-host (optional):** point Docker at a log driver instead of/alongside json-file — e.g. `--log-driver=loki`, `--log-driver=fluentd`, `--log-driver=syslog`, or `--log-driver=gcplogs`. On k8s, stdout is collected by the node logging agent; no per-container config needed. **Persist raw stdout to a host file (simple VPS option):** ```bash docker run -d --name halobestie-staging ... \ > /var/log/halobestie/backend.log 2>&1 # better: use the json-file driver (above) and read /var/lib/docker/containers//-json.log, # or redirect via your reverse proxy / a sidecar. Prefer a real log driver for rotation. ``` ### 8b. Xendit webhook fallback JSONL → **needs a volume** (only if enabled) The one component that writes a **file** is the optional webhook fallback sink ([src/services/webhook-log.service.js](src/services/webhook-log.service.js)), **off by default**. When `XENDIT_WEBHOOK_FALLBACK_ENABLED=true`, it writes rolling JSONL to `XENDIT_WEBHOOK_FALLBACK_DIR` (default `./logs` → `/app/logs` in the container). To keep those across restarts, **mount a volume**: ```bash docker run -d ... \ -e XENDIT_WEBHOOK_FALLBACK_ENABLED=true \ -e XENDIT_WEBHOOK_FALLBACK_DIR=/app/logs \ -v /opt/halobestie/logs:/app/logs \ ``` (The Compose example in §6 already declares the `backend-logs` volume for this.) If the fallback stays disabled, you don't need this volume — everything is on stdout. --- ## 9. Health, upgrade, rollback ```bash # Health — the image has a built-in TCP HEALTHCHECK; check it: docker inspect --format '{{.State.Health.Status}}' halobestie-staging # Upgrade to a new image docker pull /halobestie-backend:staging docker run ... node src/db/migrate.js # if new migrations docker stop halobestie-staging && docker rm halobestie-staging docker run -d --name halobestie-staging ... # re-run with the new image # Rollback = re-run the previous tag/digest. Graceful shutdown is handled: # server.js traps SIGTERM and drains the listeners before exit. ``` --- ## 10. Quick reference | Task | Command | |---|---| | Build | `docker build -t ./backend` | | Migrate | `docker run --rm --env-file .env.staging node src/db/migrate.js` | | Run | `docker run -d --name halobestie-staging --env-file .env.staging -p 3000:3000 --restart unless-stopped ` | | Logs (live) | `docker logs -f halobestie-staging` | | Logs (pretty) | `docker logs -f halobestie-staging \| jq .` | | Health | `docker inspect --format '{{.State.Health.Status}}' halobestie-staging` | | Shell in | `docker exec -it halobestie-staging sh` |