Deployment Guide
Step-by-step guide to deploy frontend, backend, and database to the VPS.
This guide reproduces the live setup documented in Current Deployment. It assumes a clean Ubuntu VPS with Docker and docker compose installed.
Prerequisites
- A VPS (Ubuntu 24.04) with
rootSSH access. - Docker Engine +
docker composeplugin. - DNS
Arecords pointinghallostroke.org,app.hallostroke.org, andapi.hallostroke.orgto the VPS public IP. - Both repos cloned to
/root/projects/:
/root/projects/hallo-stroke-backend github.com/HeraldoArman/hallo-stroke-backend
/root/projects/hallo-stroke-bunny gitlab.com/mfaaach/hallo-stroke-bunnyWhy build on the VPS
The compose files build images locally (build: blocks), not from a registry. There is no CI push-to-registry step — see CI for what the workflow actually does (lint + build verification only).
Backend + database
git clone https://github.com/HeraldoArman/hallo-stroke-backend.git \
/root/projects/hallo-stroke-backend
cd /root/projects/hallo-stroke-backendCopy .env.production.example to .env and fill the required values. The compose file fails fast if any of these are missing:
| Variable | Required | Purpose |
|---|---|---|
POSTGRES_PASSWORD | yes | DB superuser password |
JWT_ACCESS_SECRET | yes | access token signing |
JWT_REFRESH_SECRET | yes | refresh token signing |
SUPER_ADMIN_PASSWORD | yes | bootstrapped admin account |
AES_ENCRYPTION_KEY | yes | NIK encryption at rest |
POSTGRES_USER | no (default postgres) | DB user |
POSTGRES_DB | no (default hallo_stroke) | DB name |
GOOGLE_MAPS_API_KEY | no (default dummy) | facility finder |
YOUTUBE_API_KEY | no | video catalog |
FRONTEND_URL | no (default http://localhost:3000) | CORS origin |
VAPID keys have compose defaults
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT fall back to values baked into docker-compose.yml. These are committed defaults — generate your own for any real deployment with npx web-push generate-vapid-keys.
docker compose up -d --buildThis starts db (postgres:18) and app (built from Dockerfile, GOARCH=amd64). The backend waits for the DB healthcheck before starting, then runs GORM AutoMigrate.
curl http://localhost:3001/health
docker compose psWhat the backend image is
Two-stage Dockerfile:
golang:1.24-bookworm— builds a stripped binary (-ldflags="-w -s") asserver.bin.debian:bookworm-slimruntime — non-rootappuser,ca-certificates+tzdata+wget(for the healthcheck),EXPOSE 3001.
DB tuning
The db service passes a tuned postgresql.conf via command args — max_connections=100, shared_buffers=128MB, effective_cache_size=256MB, wal_buffers=16MB, ssl=off. Schema is created by GORM AutoMigrate on app startup; init.sql is mounted read-only for any seed data.
Frontend (bunny)
git clone https://gitlab.com/mfaaach/hallo-stroke-bunny.git \
/root/projects/hallo-stroke-bunny
cd /root/projects/hallo-stroke-bunny
git checkout aldo # production branchThe REACT_APP_* build args are set directly in docker-compose.yml, so a plain build bakes them in:
docker compose up -d --buildTo change the API URL or VAPID key, edit the args: block in docker-compose.yml first — these are compile-time, not runtime.
curl http://localhost:3003/What the frontend image is
Two-stage Dockerfile:
node:24-bookworm—npm ci, patches an ESLint comment (sed), runsnpm run build(Create React App) withREACT_APP_*inlined.nginx:alpineruntime — serves/usr/share/nginx/html, React Router SPA fallback (try_files ... /index.html), long-cache hashed assets, no-cachesw.js.
Host nginx + TLS
The host runs nginx (1.24) as the public edge. Three site configs live in /etc/nginx/sites-enabled/:
| Config | Domain | Upstream |
|---|---|---|
hallostroke.org.conf | hallostroke.org | localhost:3003 (frontend) |
app.hallostroke.org.conf | app.hallostroke.org | localhost:3000 (alt frontend) |
api.hallostroke.org.conf | api.hallostroke.org | localhost:3001 (backend) |
Each has a port-80 server that 301s to HTTPS, and a 443 server proxying to the localhost Docker port.
certbot --nginx -d hallostroke.org -d app.hallostroke.org -d api.hallostroke.orgCertbot rewrites the configs to add the 443 server + cert paths and manages renewal.
api.hallostroke.org.conf has two location blocks:
location /backend/ { proxy_pass http://localhost:3001/; }
location / { proxy_pass http://localhost:3001/; }The trailing slash on proxy_pass strips /backend/, so https://api.hallostroke.org/backend/api/users → Fiber /api/users.
nginx -t && systemctl reload nginxCert expiry
The hallostroke.org cert expires Oct 27 2026. certbot installs a systemd timer for auto-renewal — verify it with systemctl list-timers certbot.
WhatsApp bridge
The backend reaches WhatsApp through aldinokemal2104/go-whatsapp-web-multidevice, deployed separately (not part of the Hallo Stroke compose). The backend's WHATSAPP_API_ENDPOINT points at it, and it POSTs webhook events back to api.hallostroke.org/webhook.
See WhatsApp Bot for the integration contract.
Updating a running deployment
# backend
cd /root/projects/hallo-stroke-backend
git pull
docker compose up -d --build
# frontend
cd /root/projects/hallo-stroke-bunny
git pull
docker compose up -d --buildNo registry, no rolling deploy — docker compose rebuilds the image and recreates the single container. Expect a few seconds of downtime per service.