Hallo Stroke

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 root SSH access.
  • Docker Engine + docker compose plugin.
  • DNS A records pointing hallostroke.org, app.hallostroke.org, and api.hallostroke.org to 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-bunny

Why 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-backend

Copy .env.production.example to .env and fill the required values. The compose file fails fast if any of these are missing:

VariableRequiredPurpose
POSTGRES_PASSWORDyesDB superuser password
JWT_ACCESS_SECRETyesaccess token signing
JWT_REFRESH_SECRETyesrefresh token signing
SUPER_ADMIN_PASSWORDyesbootstrapped admin account
AES_ENCRYPTION_KEYyesNIK encryption at rest
POSTGRES_USERno (default postgres)DB user
POSTGRES_DBno (default hallo_stroke)DB name
GOOGLE_MAPS_API_KEYno (default dummy)facility finder
YOUTUBE_API_KEYnovideo catalog
FRONTEND_URLno (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 --build

This 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 ps

What the backend image is

Two-stage Dockerfile:

  1. golang:1.24-bookworm — builds a stripped binary (-ldflags="-w -s") as server.bin.
  2. debian:bookworm-slim runtime — non-root appuser, 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 branch

The REACT_APP_* build args are set directly in docker-compose.yml, so a plain build bakes them in:

docker compose up -d --build

To 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:

  1. node:24-bookwormnpm ci, patches an ESLint comment (sed), runs npm run build (Create React App) with REACT_APP_* inlined.
  2. nginx:alpine runtime — serves /usr/share/nginx/html, React Router SPA fallback (try_files ... /index.html), long-cache hashed assets, no-cache sw.js.

Host nginx + TLS

The host runs nginx (1.24) as the public edge. Three site configs live in /etc/nginx/sites-enabled/:

ConfigDomainUpstream
hallostroke.org.confhallostroke.orglocalhost:3003 (frontend)
app.hallostroke.org.confapp.hallostroke.orglocalhost:3000 (alt frontend)
api.hallostroke.org.confapi.hallostroke.orglocalhost: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.org

Certbot 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 nginx

Cert 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 --build

No registry, no rolling deploy — docker compose rebuilds the image and recreates the single container. Expect a few seconds of downtime per service.

On this page