Self-Hosted Deployment

Govula on customer-controlled infrastructure: single VM with reverse proxy, or small cluster behind a load balancer.

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

Audit status: PartialSingle-VM: fully supported. Small-cluster: pattern documented; no Helm/Terraform/Ansible ships.

Run Govula entirely on customer-controlled infrastructure: a single VM for small deployments, or a small cluster behind a load balancer for higher availability. The application is stateless — every request can be served by any backend instance — so horizontal scaling is purely a matter of provisioning.

Status: Partial. Single-VM is fully supported (docker-compose.on-prem.yml exists). Small-cluster is Aspirational — pattern documented; no Helm chart, no Terraform module, no Ansible playbook ships with the repo.

Architecture — single VM

┌──────────────────────────────────────────────────────────┐
│  Single VM (Linux, 4 vCPU / 8 GB RAM minimum)            │
│                                                          │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐              │
│  │ Caddy /  │ → │ govula-  │ → │ postgres │              │
│  │ nginx    │   │ api+fe   │   │ 16       │              │
│  │ TLS term │   │ (docker) │   │ (docker) │              │
│  └──────────┘   └──────────┘   └──────────┘              │
│        ▲                              │                  │
│   :443 │                              │ /var/lib/postgres│
│        │                              ▼                  │
└────────┼──────────────────────────────────────────────────┘
         │
         ▼
   Public DNS → VM

Architecture — small cluster

┌─────────────┐  ┌──────────────┐  ┌──────────────┐
│  Load       │→ │  api-1, 2, 3 │→ │  Postgres    │
│  balancer   │  │  (stateless) │  │  primary +   │
│  (ALB / HA  │  └──────────────┘  │  read replica│
│   proxy)    │                    └──────────────┘
└─────────────┘
       │
       ▼ frontend served by same LB or static CDN

Single-VM step-by-step

  1. Provision a Linux VM (Ubuntu 22.04 LTS recommended). Open ports 80, 443. Block 5000 / 3000 / 5432 from public.
  2. Install Docker + Compose:
    curl -fsSL https://get.docker.com | sh
    
  3. Clone repo and configure:
    git clone <repo-url> /opt/govula && cd /opt/govula
    cp .env.example .env
    # Fill required vars — see "Required env vars" below
    
  4. Bring up the stack:
    docker-compose -f docker-compose.on-prem.yml up -d
    
  5. Reverse proxy — terminate TLS in Caddy or nginx (examples below) and forward to 127.0.0.1:5000 (api) and 127.0.0.1:3000 (frontend).
  6. Verify:
    curl -fsS https://your-host/health         # → {"status":"ok"}
    curl -fsS https://your-host/api/v1/health  # → success:true, db:"ok"
    

Required env vars

All enforced by src/config/validateEnv.ts at boot:

VariableRequired in prodNotes
NODE_ENV=productionyestriggers boot-envelope hardening
DATABASE_URLyespooled connection if Postgres is co-resident, single-DSN otherwise
JWT_SECRETyes (≥32 chars)hard-fail otherwise
JWT_REFRESH_SECRETyes (≥32 chars)without it, every restart silently logs out every user
CORS_ALLOWED_ORIGINSyescomma-separated, no *
PUBLIC_URLyesabsolute frontend URL — used in outbound replay links
RESEND_API_KEY + ALERT_EMAIL_TO + EMAIL_FROMall three or nonepartial transport hard-fails boot

Full list in .env.example.

Reverse proxy — Caddy (recommended)

/etc/caddy/Caddyfile:

api.example.com {
    reverse_proxy 127.0.0.1:5000
    encode gzip
    header X-Frame-Options "DENY"
}

app.example.com {
    reverse_proxy 127.0.0.1:3000
    encode gzip
}

Caddy handles TLS automatically via Let's Encrypt.

Reverse proxy — nginx

upstream govula_api { server 127.0.0.1:5000; }
upstream govula_fe  { server 127.0.0.1:3000; }

server {
    listen 443 ssl http2;
    server_name api.example.com;
    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    location / {
        proxy_pass http://govula_api;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Postgres — co-resident vs managed

OptionWhenNotes
Postgres on the same VMSmall deployments, single tenantdocker-compose.on-prem.yml already includes a postgres:16-alpine service + nightly backup sidecar
Managed Postgres (RDS / Cloud SQL / Neon)Anything customer-facing or > 100 tenantsMove DATABASE_URL to the managed endpoint; remove the postgres compose service

Small-cluster pattern (Aspirational)

The application is stateless: any backend pod can serve any request. To scale horizontally:

  1. Move Postgres off the VM (managed instance with an external DSN).
  2. Run ≥ 2 backend instances behind a load balancer with sticky-by-nothing (round-robin).
  3. Healthcheck both /health (liveness) and /api/v1/health (readiness — only this one is gated on the boot envelope).
  4. Run the frontend either as a third pod or as a static deploy on the LB.

Govula does not currently ship a Helm chart or a Terraform module — these patterns are advisory.

Rollback

Same as Docker: tag-swap the image and restart. See ../rollback-plan.md.

Troubleshooting

See troubleshooting-matrix.md.

Where to read more

Canonical source: docs/deployment/self-hosted.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 8
Hybrid & Federatednext step

Next in Deploy: Hybrid & Federated.

What should I do next?

Activation Flowprimary

continues in "deploy"

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