You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/docker.md
+22-22Lines changed: 22 additions & 22 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,8 +4,8 @@
4
4
5
5
Two containers share an internal Docker bridge network:
6
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.
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
9
10
10
Traffic flow:
11
11
- Webhook providers → `backend:3000` (published)
@@ -15,14 +15,14 @@ Traffic flow:
15
15
16
16
The following files are referenced throughout these steps:
17
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
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
22
23
23
All are included in the repo under their respective paths.
24
24
25
-
## Step 1 — Build The UI CSS
25
+
## Step 1) Build The UI CSS
26
26
27
27
The frontend container serves static files. Build the CSS before starting the stack:
28
28
@@ -33,7 +33,7 @@ npm run build:css
33
33
34
34
This outputs `ui/dist/styles.css`, which the nginx container will serve directly.
35
35
36
-
## Step 2 — Configure The nginx Auth Snippet
36
+
## Step 2) Configure The nginx Auth Snippet
37
37
38
38
The frontend nginx config includes an auth snippet at `/etc/nginx/snippets/gateherald-admin-auth.conf`, mounted from `deploy/nginx/snippets/`. Choose one:
The `docker-compose.yml` mounts this file into the frontend container at `/etc/nginx/.htpasswd-gateherald`.
54
54
55
-
## Step 3 — Set The Shared Secret
55
+
## Step 3) Set The Shared Secret
56
56
57
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.
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
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.
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
68
69
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
70
71
-
## Step 4 — Configure Backend Environment
71
+
## Step 4) Configure Backend Environment
72
72
73
73
Create a `.env.production` file in the project root (or pass variables directly to Compose). A minimal production config:
74
74
@@ -85,15 +85,15 @@ Set `ALLOWED_ORIGINS` to the external URL where the frontend nginx is reachable.
85
85
86
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
87
88
-
## Step 5 — Run Migrations
88
+
## Step 5) Run Migrations
89
89
90
90
Run database migrations once before starting the app for the first time, or after any upgrade that includes new migrations:
91
91
92
92
```bash
93
93
docker compose run --rm backend node scripts/db-migrate.js
94
94
```
95
95
96
-
## Step 6 — Start The Stack
96
+
## Step 6) Start The Stack
97
97
98
98
```bash
99
99
docker compose up -d
@@ -108,17 +108,17 @@ To follow logs:
108
108
docker compose logs -f
109
109
```
110
110
111
-
## Step 7 — TLS
111
+
## Step 7) TLS
112
112
113
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
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.
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
116
117
-
## Optional — Postgres/MySQL Database Container
117
+
## Optional - Postgres/MySQL Database Container
118
118
119
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
120
121
-
### 1 — Install The Driver
121
+
### 1) Install The Driver
122
122
123
123
The Postgres or MySQL driver must be present in the image. Add it as a production dependency before building:
124
124
@@ -130,13 +130,13 @@ npm install pg pg-hstore
130
130
npm install mysql2
131
131
```
132
132
133
-
### 2 — Add The db Service To docker-compose.yml
133
+
### 2) Add The db Service To docker-compose.yml
134
134
135
135
Add a `db` service on the same internal network, with its own named volume. Update `backend` to depend on it:
136
136
137
137
```yaml
138
138
volumes:
139
-
gateherald-data: # remove or repurpose — no longer used for SQLite
139
+
gateherald-data: # remove or repurpose; no longer used for SQLite
140
140
gateherald-db: # postgres data directory
141
141
142
142
services:
@@ -151,7 +151,7 @@ services:
151
151
volumes:
152
152
- gateherald-db:/var/lib/postgresql/data
153
153
restart: unless-stopped
154
-
# No 'ports' — not reachable from outside Docker
154
+
# No 'ports' - not reachable from outside Docker
155
155
156
156
backend:
157
157
build: .
@@ -167,7 +167,7 @@ services:
167
167
168
168
Do not publish the `db` container's port. It only needs to be reachable from `backend` on the internal network.
169
169
170
-
### 3 — Update Backend Environment
170
+
### 3) Update Backend Environment
171
171
172
172
Remove the SQLite volume mount from `backend` and add database connection vars to `.env.production`:
173
173
@@ -183,7 +183,7 @@ DB_SSL=false
183
183
184
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
185
186
-
### 4 — Run Migrations
186
+
### 4) Run Migrations
187
187
188
188
Wait for Postgres to be ready before migrating. A simple one-off approach:
- To: `deploy/nginx/snippets/gateherald-admin-auth-oidc.conf`
10
-
2. Enable `/oauth2/*` location on the internal UI host and route it to oauth2-proxy.
11
-
3. Configure oauth2-proxy with your IdP issuer URL, client ID/secret, redirect URL, and session cookie settings.
12
-
4. Keep external host restrictions in place (CIDR allowlist or stronger network controls). OIDC on internal host does not replace edge network restrictions.
13
-
5. Verify behavior:
14
-
- Unauthenticated requests to `/ui/*` and admin APIs redirect to IdP.
15
-
- Authenticated user can load UI and perform template/config CRUD.
16
-
-`/webhook/*` remains publicly reachable.
17
-
- Admin APIs are unreachable from non-internal networks.
18
-
6. Rotate/remove Basic Auth credentials (`/etc/nginx/.htpasswd-gateherald`) after successful cutover.
5
+
OIDC auth is handled by [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/) running alongside nginx. The nginx OIDC snippet uses nginx's `auth_request` module to delegate every protected request to oauth2-proxy before serving it. If oauth2-proxy says the user is not authenticated, nginx redirects to the IdP login page. The user logs in on the IdP's own site (not on the app's login page) and is redirected back once authenticated.
6
+
7
+
Note: the app has a separate built-in `FRONTEND_AUTH_ENABLED` basic login flow. That is independent of OIDC. When using OIDC, leave `FRONTEND_AUTH_ENABLED=false` (the default); there is no need for both.
All protected location blocks already include this file and require no changes. The snippet swaps `auth_basic` for `auth_request /oauth2/auth`, which routes every protected request through oauth2-proxy.
19
+
20
+
### 2) Uncomment The `/oauth2/` Location Block
21
+
22
+
In your nginx config (bare-metal: `deploy/nginx/gateherald.conf` or `gateherald-split-hosts.conf` internal host; Docker: `deploy/docker/nginx.conf`), uncomment the oauth2-proxy location:
23
+
24
+
```nginx
25
+
location ^~ /oauth2/ {
26
+
proxy_pass http://127.0.0.1:4180;
27
+
proxy_set_header Host $host;
28
+
proxy_set_header X-Real-IP $remote_addr;
29
+
proxy_set_header X-Scheme $scheme;
30
+
}
31
+
```
32
+
33
+
### 3) Configure And Run oauth2-proxy
34
+
35
+
oauth2-proxy must be reachable at `127.0.0.1:4180` on the same host as nginx (or adjust the `proxy_pass` address to match). The `--upstream=static://202` flag means oauth2-proxy only performs auth. It does not proxy the traffic itself (nginx handles that).
- Set the redirect URI to `https://gateherald.internal/oauth2/callback` under "Authentication".
76
+
- Generate a client secret under "Certificates & secrets".
77
+
-`--email-domain` restricts login to users from your org's domain. Set to `*` to allow any authenticated Entra user.
78
+
- To restrict access to a specific Entra group, add `--allowed-group=<group object ID>` and grant the app registration `GroupMember.Read.All` permission (or configure the token to include group claims).
79
+
80
+
### 4) Reload Nginx
81
+
82
+
```bash
83
+
# bare-metal
84
+
nginx -s reload
85
+
86
+
# Docker
87
+
docker compose restart frontend
88
+
```
89
+
90
+
### 5) Verify
91
+
92
+
- Unauthenticated requests to `/ui/*` and admin APIs should redirect to the IdP login page.
93
+
- Authenticated user can load the UI and perform template/config CRUD.
94
+
-`/webhook/*` remains publicly reachable without auth.
95
+
- Admin APIs are unreachable from non-internal networks.
96
+
97
+
### 6) Clean Up Basic Auth
98
+
99
+
Once verified, remove the htpasswd file:
100
+
101
+
```bash
102
+
rm deploy/nginx/.htpasswd-gateherald # adjust path to your setup
103
+
```
104
+
105
+
### Notes On The OIDC Snippet
106
+
107
+
`deploy/nginx/snippets/gateherald-admin-auth-oidc.conf` forwards `X-Forwarded-User` and `X-Forwarded-Email` headers from oauth2-proxy to the upstream backend. The app does not currently consume these headers, but they will be present on proxied requests. Audit them before relying on them for any downstream logic.
108
+
109
+
Keep external host restrictions (CIDR allowlist or network controls) in place even after enabling OIDC. OIDC on the internal/frontend host does not replace edge network restrictions on the backend host.
0 commit comments