|
| 1 | +# Docker Deployment |
| 2 | + |
| 3 | +## Architecture |
| 4 | + |
| 5 | +Two containers share an internal Docker bridge network: |
| 6 | + |
| 7 | +- **`backend`** — Node.js app in API-only mode (`SERVE_UI=false`, `FRONTEND_ONLY_API=true`). Publishes port 3000 for external webhook ingress. |
| 8 | +- **`frontend`** — nginx 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. |
| 9 | + |
| 10 | +Traffic flow: |
| 11 | +- Webhook providers → `backend:3000` (published) |
| 12 | +- Admin users → `frontend` nginx → static files served directly; `/api/*` proxied to `backend:3000` with `X-Gateherald-Proxy-Secret` header injected |
| 13 | + |
| 14 | +## Files To Create |
| 15 | + |
| 16 | +The following files are referenced throughout these steps: |
| 17 | + |
| 18 | +- `Dockerfile` — backend container image |
| 19 | +- `docker-compose.yml` — service definitions |
| 20 | +- `deploy/docker/nginx.conf` — nginx config for the frontend container |
| 21 | +- `.dockerignore` — excludes `.env.*` files, the database, and other unnecessary paths from the build context so secrets are not baked into the image |
| 22 | + |
| 23 | +All are included in the repo under their respective paths. |
| 24 | + |
| 25 | +## Step 1 — Build The UI CSS |
| 26 | + |
| 27 | +The frontend container serves static files. Build the CSS before starting the stack: |
| 28 | + |
| 29 | +```bash |
| 30 | +npm install |
| 31 | +npm run build:css |
| 32 | +``` |
| 33 | + |
| 34 | +This outputs `ui/dist/styles.css`, which the nginx container will serve directly. |
| 35 | + |
| 36 | +## Step 2 — Configure The nginx Auth Snippet |
| 37 | + |
| 38 | +The frontend nginx config includes an auth snippet at `/etc/nginx/snippets/gateherald-admin-auth.conf`, mounted from `deploy/nginx/snippets/`. Choose one: |
| 39 | + |
| 40 | +- **Basic Auth**: copy `gateherald-admin-auth-basic.conf` → `gateherald-admin-auth.conf` |
| 41 | +- **OIDC**: copy `gateherald-admin-auth-oidc.conf` → `gateherald-admin-auth.conf` |
| 42 | + |
| 43 | +```bash |
| 44 | +copy deploy\nginx\snippets\gateherald-admin-auth-basic.conf deploy\nginx\snippets\gateherald-admin-auth.conf |
| 45 | +``` |
| 46 | + |
| 47 | +For Basic Auth, create the htpasswd file that the snippet references: |
| 48 | + |
| 49 | +```bash |
| 50 | +htpasswd -c deploy/docker/.htpasswd-gateherald <username> |
| 51 | +``` |
| 52 | + |
| 53 | +The `docker-compose.yml` mounts this file into the frontend container at `/etc/nginx/.htpasswd-gateherald`. |
| 54 | + |
| 55 | +## Step 3 — Set The Shared Secret |
| 56 | + |
| 57 | +`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. |
| 58 | + |
| 59 | +Generate a strong random value: |
| 60 | + |
| 61 | +```bash |
| 62 | +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
| 63 | +``` |
| 64 | + |
| 65 | +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. |
| 66 | + |
| 67 | +`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. |
| 68 | + |
| 69 | +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. |
| 70 | + |
| 71 | +## Step 4 — Configure Backend Environment |
| 72 | + |
| 73 | +Create a `.env.production` file in the project root (or pass variables directly to Compose). A minimal production config: |
| 74 | + |
| 75 | +```env |
| 76 | +NODE_ENV=production |
| 77 | +PORT=3000 |
| 78 | +SERVE_UI=false |
| 79 | +FRONTEND_ONLY_API=true |
| 80 | +ALLOWED_ORIGINS=http://localhost:8080 |
| 81 | +ADMIN_PROXY_SHARED_SECRET=<value from step 3> |
| 82 | +``` |
| 83 | + |
| 84 | +Set `ALLOWED_ORIGINS` to the external URL where the frontend nginx is reachable. This controls CORS for browser-initiated API requests. |
| 85 | + |
| 86 | +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`. |
| 87 | + |
| 88 | +## Step 5 — Run Migrations |
| 89 | + |
| 90 | +Run database migrations once before starting the app for the first time, or after any upgrade that includes new migrations: |
| 91 | + |
| 92 | +```bash |
| 93 | +docker compose run --rm backend node scripts/db-migrate.js |
| 94 | +``` |
| 95 | + |
| 96 | +## Step 6 — Start The Stack |
| 97 | + |
| 98 | +```bash |
| 99 | +docker compose up -d |
| 100 | +``` |
| 101 | + |
| 102 | +- Frontend (admin UI) is available at `http://localhost:8080` |
| 103 | +- Webhook ingress is available at `http://localhost:3000/webhook/...` |
| 104 | + |
| 105 | +To follow logs: |
| 106 | + |
| 107 | +```bash |
| 108 | +docker compose logs -f |
| 109 | +``` |
| 110 | + |
| 111 | +## Step 7 — TLS |
| 112 | + |
| 113 | +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. |
| 114 | + |
| 115 | +Do not expose the backend container's port 3000 through TLS termination intended for admin users — keep that path for webhook ingress only. |
| 116 | + |
| 117 | +## Optional — Postgres/MySQL Database Container |
| 118 | + |
| 119 | +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. |
| 120 | + |
| 121 | +### 1 — Install The Driver |
| 122 | + |
| 123 | +The Postgres or MySQL driver must be present in the image. Add it as a production dependency before building: |
| 124 | + |
| 125 | +```bash |
| 126 | +# Postgres |
| 127 | +npm install pg pg-hstore |
| 128 | + |
| 129 | +# MySQL |
| 130 | +npm install mysql2 |
| 131 | +``` |
| 132 | + |
| 133 | +### 2 — Add The db Service To docker-compose.yml |
| 134 | + |
| 135 | +Add a `db` service on the same internal network, with its own named volume. Update `backend` to depend on it: |
| 136 | + |
| 137 | +```yaml |
| 138 | +volumes: |
| 139 | + gateherald-data: # remove or repurpose — no longer used for SQLite |
| 140 | + gateherald-db: # postgres data directory |
| 141 | + |
| 142 | +services: |
| 143 | + db: |
| 144 | + image: postgres:17-alpine |
| 145 | + environment: |
| 146 | + POSTGRES_DB: gateherald |
| 147 | + POSTGRES_USER: gateherald_user |
| 148 | + POSTGRES_PASSWORD: "${DB_PASSWORD}" |
| 149 | + networks: |
| 150 | + - gateherald |
| 151 | + volumes: |
| 152 | + - gateherald-db:/var/lib/postgresql/data |
| 153 | + restart: unless-stopped |
| 154 | + # No 'ports' — not reachable from outside Docker |
| 155 | + |
| 156 | + backend: |
| 157 | + build: . |
| 158 | + env_file: .env.production |
| 159 | + depends_on: |
| 160 | + - db |
| 161 | + ports: |
| 162 | + - "3000:3000" |
| 163 | + networks: |
| 164 | + - gateherald |
| 165 | + restart: unless-stopped |
| 166 | +``` |
| 167 | +
|
| 168 | +Do not publish the `db` container's port. It only needs to be reachable from `backend` on the internal network. |
| 169 | + |
| 170 | +### 3 — Update Backend Environment |
| 171 | + |
| 172 | +Remove the SQLite volume mount from `backend` and add database connection vars to `.env.production`: |
| 173 | + |
| 174 | +```env |
| 175 | +DB_DIALECT=postgres |
| 176 | +DB_HOST=db |
| 177 | +DB_PORT=5432 |
| 178 | +DB_NAME=gateherald |
| 179 | +DB_USER=gateherald_user |
| 180 | +DB_PASSWORD=<strong password> |
| 181 | +DB_SSL=false |
| 182 | +``` |
| 183 | + |
| 184 | +`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. |
| 185 | + |
| 186 | +### 4 — Run Migrations |
| 187 | + |
| 188 | +Wait for Postgres to be ready before migrating. A simple one-off approach: |
| 189 | + |
| 190 | +```bash |
| 191 | +docker compose up -d db |
| 192 | +# wait a few seconds for Postgres to initialise, then: |
| 193 | +docker compose run --rm backend node scripts/db-migrate.js |
| 194 | +docker compose up -d |
| 195 | +``` |
| 196 | + |
| 197 | +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](https://docs.docker.com/compose/how-tos/startup-order/) for details. |
| 198 | + |
| 199 | +--- |
| 200 | + |
| 201 | +## Notes |
| 202 | + |
| 203 | +- **`ui/env.js`** defaults 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 to `env.js` are needed. |
| 204 | +- **Auth snippet cutover (Basic → OIDC)** follows the same steps as `docs/nginx.md`. Only `deploy/nginx/snippets/gateherald-admin-auth.conf` needs to change; the nginx config and Compose file are not affected. |
| 205 | +- **Seed data**: to seed the database with sample data, substitute `db:seed` for `db-migrate.js` in the migration command: `node scripts/db-seed.js`. |
0 commit comments