Skip to content

Commit 23a4c9a

Browse files
committed
add docker deployment docs and supported files
1 parent 67b9173 commit 23a4c9a

8 files changed

Lines changed: 371 additions & 3 deletions

File tree

.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.git
2+
node_modules
3+
.env.*
4+
*.db
5+
data/
6+
.vscode
7+
.site
8+
deploy/docker/.htpasswd-gateherald

Dockerfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM node:20-alpine
2+
3+
RUN addgroup -S gateherald && adduser -S -G gateherald gateherald
4+
5+
WORKDIR /app
6+
7+
COPY package*.json ./
8+
RUN npm ci --omit=dev
9+
10+
COPY . .
11+
12+
RUN chown -R gateherald:gateherald /app
13+
USER gateherald
14+
15+
EXPOSE 3000
16+
17+
CMD ["node", "index.js"]

deploy/docker/nginx.conf

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
upstream gateherald_backend {
2+
server backend:3000;
3+
keepalive 32;
4+
}
5+
6+
server {
7+
listen 80;
8+
server_name _;
9+
10+
root /usr/share/nginx/html;
11+
12+
proxy_http_version 1.1;
13+
proxy_set_header Host $host;
14+
proxy_set_header X-Real-IP $remote_addr;
15+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
16+
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
17+
18+
location = / {
19+
return 302 /ui/config-builder;
20+
}
21+
22+
location = /ui {
23+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
24+
return 302 /ui/config-builder;
25+
}
26+
27+
location = /ui/config-builder {
28+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
29+
try_files /config-builder.html =404;
30+
}
31+
32+
location = /ui/template-builder {
33+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
34+
try_files /template-builder.html =404;
35+
}
36+
37+
location = /ui/docs {
38+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
39+
try_files /docs.html =404;
40+
}
41+
42+
location ~ ^/ui/docs/[^/]+$ {
43+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
44+
try_files /docs.html =404;
45+
}
46+
47+
location = /ui/docs.js {
48+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
49+
try_files /docs.js =404;
50+
}
51+
52+
location = /ui/env.js {
53+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
54+
try_files /env.js =404;
55+
}
56+
57+
location ^~ /ui/dist/ {
58+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
59+
alias /usr/share/nginx/html/dist/;
60+
}
61+
62+
location = /api {
63+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
64+
proxy_set_header X-Gateherald-Proxy-Secret "${ADMIN_PROXY_SHARED_SECRET}";
65+
proxy_pass http://gateherald_backend;
66+
}
67+
68+
location ~ ^/api/(templates|configs)(/|$) {
69+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
70+
proxy_set_header X-Gateherald-Proxy-Secret "${ADMIN_PROXY_SHARED_SECRET}";
71+
proxy_pass http://gateherald_backend;
72+
}
73+
74+
location = /api/ui/templates {
75+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
76+
proxy_set_header X-Gateherald-Proxy-Secret "${ADMIN_PROXY_SHARED_SECRET}";
77+
proxy_pass http://gateherald_backend;
78+
}
79+
80+
location = /api/ui/docs-index {
81+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
82+
proxy_set_header X-Gateherald-Proxy-Secret "${ADMIN_PROXY_SHARED_SECRET}";
83+
proxy_pass http://gateherald_backend;
84+
}
85+
86+
location ~ ^/api/ui/docs/[^/]+$ {
87+
include /etc/nginx/snippets/gateherald-admin-auth.conf;
88+
proxy_set_header X-Gateherald-Proxy-Secret "${ADMIN_PROXY_SHARED_SECRET}";
89+
proxy_pass http://gateherald_backend;
90+
}
91+
92+
# OIDC callback path when using oauth2-proxy on the frontend container.
93+
# location ^~ /oauth2/ {
94+
# proxy_pass http://127.0.0.1:4180;
95+
# proxy_set_header Host $host;
96+
# proxy_set_header X-Real-IP $remote_addr;
97+
# proxy_set_header X-Scheme $scheme;
98+
# }
99+
}

docker-compose.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
networks:
2+
gateherald:
3+
driver: bridge
4+
5+
volumes:
6+
gateherald-data:
7+
8+
services:
9+
backend:
10+
build: .
11+
env_file: .env.production
12+
ports:
13+
- "3000:3000"
14+
networks:
15+
- gateherald
16+
volumes:
17+
- gateherald-data:/app/data
18+
restart: unless-stopped
19+
20+
frontend:
21+
image: nginx:alpine
22+
depends_on:
23+
- backend
24+
networks:
25+
- gateherald
26+
ports:
27+
- "8080:80"
28+
environment:
29+
ADMIN_PROXY_SHARED_SECRET: "${ADMIN_PROXY_SHARED_SECRET}"
30+
# Scope envsubst to only this variable so nginx's own $host, $remote_addr etc. are not touched.
31+
NGINX_ENVSUBST_TEMPLATE_VARS: ADMIN_PROXY_SHARED_SECRET
32+
volumes:
33+
- ./ui:/usr/share/nginx/html:ro
34+
- ./deploy/docker/nginx.conf:/etc/nginx/templates/default.conf.template:ro
35+
- ./deploy/nginx/snippets/gateherald-admin-auth.conf:/etc/nginx/snippets/gateherald-admin-auth.conf:ro
36+
- ./deploy/docker/.htpasswd-gateherald:/etc/nginx/.htpasswd-gateherald:ro
37+
restart: unless-stopped

docs/docker.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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`.

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ It includes:
3030

3131
- [Getting Started](getting-started.md)
3232
- [API Reference](api-reference.md)
33-
- [Deployment](deployment.md)
33+
- [Split-Host Deployment](split-host-deployment.md)
34+
- [Docker Deployment](docker.md)
3435
- [Nginx Config Notes](nginx.md)
3536
- [Database In Production](database-production.md)
3637
- [Troubleshooting](troubleshooting.md)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Deployment
1+
# Split-Host Deployment
22

33
## Internal Frontend + External Backend
44

scripts/build-docs-pages.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const preferredOrder = [
1515
'index',
1616
'getting-started',
1717
'api-reference',
18-
'deployment',
18+
'split-host-deployment',
19+
'docker',
1920
'nginx',
2021
'database-production',
2122
'troubleshooting'

0 commit comments

Comments
 (0)