Docker Deployment

Local-dev and single-host production using the shipped compose files. Multi-host orchestration is advisory.

This section is intended for: Technical Team. Unauthorised access is restricted.

Audit status: MixedLocal + single-host: Implemented. Swarm/K8s: Aspirational.

Local-development and production-grade Docker recipes for Govula's backend (Express) and frontend (Next.js). Compose files already exist on main (docker-compose.yml, docker-compose.single-tenant.yml, docker-compose.on-prem.yml) — see docs/ENTERPRISE-DEPLOYMENT.md for the deployment-mode flags they consume.

Status: Implemented for local dev and single-host production. Multi-host orchestration (Swarm / Kubernetes) is Aspirational — pattern only.

Architecture

┌─────────────────┐     ┌────────────────────┐     ┌─────────────────┐
│  govula-fe      │ →   │  govula-api        │ →   │  postgres       │
│  Next.js (3000) │     │  Express (5000)    │     │  16-alpine      │
│  NEXT_PUBLIC_   │     │  NODE_ENV=prod     │     │  named volume   │
│  API_URL=...    │     │  PORT=5000         │     │  pg_data        │
└─────────────────┘     └────────────────────┘     └─────────────────┘
        ▲                       ▲
        │ host:80/443           │ host:5000 (internal only behind reverse proxy)
        ▼                       ▼
              reverse proxy (Caddy / nginx / Traefik)

Local dev

cp .env.example .env
docker-compose up -d
# Wait for health
docker-compose ps
curl -fsS http://localhost:5000/health         # → {"status":"ok"}
curl -fsS http://localhost:3000                # → 200

The compose file mounts a pg_data named volume so a docker-compose down does not drop your dev DB.

Production single-host

  1. Build with explicit version + hash (matches the deterministic build pattern in docs/ENTERPRISE-DEPLOYMENT.md §"Deterministic Builds"):
    docker build \
      --build-arg BUILD_HASH=$(git rev-parse --short HEAD) \
      --build-arg BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
      -t govula-api:$(git rev-parse --short HEAD) .
    
  2. Run with hardened env (every variable below is enforced at boot by src/config/validateEnv.ts):
    • NODE_ENV=production
    • JWT_SECRET (≥32 chars) and JWT_REFRESH_SECRET (≥32 chars) — both required, both hard-fail
    • DATABASE_URL (pooled Postgres connection)
    • CORS_ALLOWED_ORIGINS (comma-separated, no *)
    • PUBLIC_URL (absolute frontend URL)
    • RESEND_API_KEY + ALERT_EMAIL_TO + EMAIL_FROM (all three or none)
  3. Healthcheck (the unconditional liveness probe at src/app.ts):
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:5000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    
  4. Log driver — Govula already emits structured JSON via Pino (src/utils/logger.ts). Pipe stdout to your aggregator:
    logging:
      driver: "json-file"   # or "fluentd", "awslogs", "gcplogs"
      options: { max-size: "50m", max-file: "5" }
    

Multi-stage Dockerfile pattern

Govula's repo root contains a single Node image. For tighter prod images:

# ── builder ──────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build           # tsc → dist/

# ── runtime ──────────────────────────────────────────────
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget --spider -q http://localhost:5000/health || exit 1
USER node
CMD ["node", "dist/index.js"]

Docker Compose modes

The repo ships three compose files (see docs/ENTERPRISE-DEPLOYMENT.md):

FileMode flagNotes
docker-compose.ymlDEPLOYMENT_MODE=saasDefault multi-tenant
docker-compose.single-tenant.ymlDEPLOYMENT_MODE=single-tenantDedicated DB
docker-compose.on-prem.ymlDEPLOYMENT_MODE=on-premIncludes backup sidecar

Rollback

docker-compose rollback is image-tag swap:

docker-compose pull govula-api:<previous-tag>
docker-compose up -d --no-deps govula-api

Schema is forward-only and additive (every CREATE TABLE and ADD COLUMN is IF NOT EXISTS per src/scripts/ensureSchema.ts), so rolling the image back is safe — older code simply doesn't read newer columns. See ../rollback-plan.md for full procedure.

Troubleshooting

SymptomLikely causeFix
govula-api exits immediately, log says Production environment validation failedA required env var is missing/shortInspect log for the variable name; set it; restart
/health 200, /api/v1/health 503 with db:"down"Postgres container not readyWait for postgres healthcheck; check DATABASE_URL
Frontend can't reach backendWrong NEXT_PUBLIC_API_URL (build-time!)Rebuild frontend image after env change
CORS 403 from your domainOrigin not in CORS_ALLOWED_ORIGINSAdd origin, restart api container

Where to read more

Canonical source: docs/deployment/docker.md

This page mirrors the markdown deployment hub on disk. The full markdown source includes additional code blocks, command examples, and embedded reference tables.

Hub index: /docs/deployment

You are here · Deploy · step 7
Production Hardeningnext step

Harden the container deployment before going live.

What should I do next?

Activation Flowprimary

continues in "deploy"

Ranked using IA v1 graph + intent map + glossary density (deterministic; no AI inference).