Gateherald
Docs
Docker Deployment
Architecture
Two containers share an internal Docker bridge network:
backendNode.js app in API-only mode (SERVE_UI=false,FRONTEND_ONLY_API=true). Publishes port 3000 for external webhook ingress.frontendnginx serving the static UI and proxying admin API requests to the backend over the internal network. No published port for the backend API means direct external access to/api/*is blocked at the network level.
Traffic flow:
- Webhook providers →
backend:3000(published) - Admin users →
frontendnginx → static files served directly;/api/*proxied tobackend:3000withX-Gateherald-Proxy-Secretheader injected
Files To Create
The following files are referenced throughout these steps:
Dockerfilebackend container imagedocker-compose.ymlservice definitionsdeploy/docker/nginx.confnginx config for the frontend container.dockerignoreexcludes.env.*files, the database, and other unnecessary paths from the build context so secrets are not baked into the image
All are included in the repo under their respective paths.
Step 1) Build The UI CSS
The frontend container serves static files. Build the CSS before starting the stack:
npm install
npm run build:css
This outputs ui/dist/styles.css, which the nginx container will serve directly.
Step 2) Configure The nginx Auth Snippet
The frontend nginx config includes an auth snippet at /etc/nginx/snippets/gateherald-admin-auth.conf, mounted from deploy/nginx/snippets/. Choose one:
- Basic Auth: copy
gateherald-admin-auth-basic.conf→gateherald-admin-auth.conf - OIDC: copy
gateherald-admin-auth-oidc.conf→gateherald-admin-auth.conf
copy deploy\nginx\snippets\gateherald-admin-auth-basic.conf deploy\nginx\snippets\gateherald-admin-auth.conf
For Basic Auth, create the htpasswd file that the snippet references:
htpasswd -c deploy/docker/.htpasswd-gateherald <username>
The docker-compose.yml mounts this file into the frontend container at /etc/nginx/.htpasswd-gateherald.
Step 3) Set The Shared Secret
ADMIN_PROXY_SHARED_SECRET is the shared credential between the nginx frontend and the Node backend. The frontend injects it as a request header; the backend verifies it before accepting any admin API call.
Generate a strong random value:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Set ADMIN_PROXY_SHARED_SECRET to this value in your .env.production file (see Step 4). That is the only place it needs to go.
deploy/docker/nginx.conf uses ${ADMIN_PROXY_SHARED_SECRET} as a placeholder. At startup, the nginx container reads the value from the environment and substitutes it before the config is loaded; the secret is never written into any file in the repository.
The frontend nginx container only receives ADMIN_PROXY_SHARED_SECRET from the environment. Other backend secrets (DB_PASSWORD, API_TOKEN, etc.) are passed only to the backend service via env_file: .env.production and are not exposed to the nginx container.
Step 4) Configure Backend Environment
Create a .env.production file in the project root (or pass variables directly to Compose). A minimal production config:
NODE_ENV=production
PORT=3000
SERVE_UI=false
FRONTEND_ONLY_API=true
ALLOWED_ORIGINS=http://localhost:8080
ADMIN_PROXY_SHARED_SECRET=<value from step 3>
Set ALLOWED_ORIGINS to the external URL where the frontend nginx is reachable. This controls CORS for browser-initiated API requests.
For SQLite the default database location is used; it will be persisted via the named volume defined in docker-compose.yml. If using Postgres or MySQL instead, add the DATABASE_URL / DB_* vars from .env.production.example.
Step 5) Run Migrations
Run database migrations once before starting the app for the first time, or after any upgrade that includes new migrations:
docker compose run --rm backend node scripts/db-migrate.js
Step 6) Start The Stack
docker compose up -d
- Frontend (admin UI) is available at
http://localhost:8080 - Webhook ingress is available at
http://localhost:3000/webhook/...
To follow logs:
docker compose logs -f
Step 7) TLS
The docker-compose.yml exposes the frontend on HTTP port 8080. For HTTPS, put a TLS-terminating reverse proxy (another nginx, Caddy, Traefik, etc.) in front of port 8080 and set the appropriate X-Forwarded-Proto header. The app already reads this header for cookie Secure flag decisions.
Do not expose the backend container's port 3000 through TLS termination intended for admin users; keep that path for webhook ingress only.
Optional - Postgres/MySQL Database Container
By default the backend uses SQLite, persisted via a named Docker volume. This is sufficient for a single-node deployment. If you want a proper relational database with concurrent write support and a fully independent data lifecycle, add a db service to the Compose stack.
1) Install The Driver
The Postgres or MySQL driver must be present in the image. Add it as a production dependency before building:
# Postgres
npm install pg pg-hstore
# MySQL
npm install mysql2
2) Add The db Service To docker-compose.yml
Add a db service on the same internal network, with its own named volume. Update backend to depend on it:
volumes:
gateherald-data: # remove or repurpose; no longer used for SQLite
gateherald-db: # postgres data directory
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: gateherald
POSTGRES_USER: gateherald_user
POSTGRES_PASSWORD: "${DB_PASSWORD}"
networks:
- gateherald
volumes:
- gateherald-db:/var/lib/postgresql/data
restart: unless-stopped
# No 'ports' - not reachable from outside Docker
backend:
build: .
env_file: .env.production
depends_on:
- db
ports:
- "3000:3000"
networks:
- gateherald
restart: unless-stopped
Do not publish the db container's port. It only needs to be reachable from backend on the internal network.
3) Update Backend Environment
Remove the SQLite volume mount from backend and add database connection vars to .env.production:
DB_DIALECT=postgres
DB_HOST=db
DB_PORT=5432
DB_NAME=gateherald
DB_USER=gateherald_user
DB_PASSWORD=<strong password>
DB_SSL=false
DB_HOST=db resolves to the db container via Docker's internal DNS. DB_SSL=false is appropriate here because the connection stays on the internal bridge network; enable it if your setup routes through a TLS-capable proxy.
4) Run Migrations
Wait for Postgres to be ready before migrating. A simple one-off approach:
docker compose up -d db
# wait a few seconds for Postgres to initialise, then:
docker compose run --rm backend node scripts/db-migrate.js
docker compose up -d
For a more robust solution, use a healthcheck on the db service and depends_on: condition: service_healthy on backend. See the Compose healthcheck docs for details.
Notes
ui/env.jsdefaults to an empty API base URL, which means the browser sends API requests to the same origin it loaded the UI from. The frontend nginx then proxies them internally to the backend. No changes toenv.jsare needed.- Auth snippet cutover (Basic → OIDC) follows the same steps as
docs/nginx.md. Onlydeploy/nginx/snippets/gateherald-admin-auth.confneeds to change; the nginx config and Compose file are not affected. - Seed data: to seed the database with sample data, substitute
db:seedfordb-migrate.jsin the migration command:node scripts/db-seed.js.