Exclude health check paths from the production SSL redirect

c21a6ad02057 · Devin AI · · parent bb2226b8b5be

Exclude health check paths from the production SSL redirect

The load balancer probes GET /healthz over plain HTTP and requires 200.
Plug.SSL redirected those probes to https with a 301, so pointing the
backend service at an HTTP health check marked every node unhealthy at
once and the site served 502. Exclude /health and /healthz from
force_ssl so probes reach the health controller, and document the
health check swap procedure in the deploy runbook.

Refs #105

Co-Authored-By: Christopher David <chris@openagents.com>
Co-Authored-By
Christopher David <chris@openagents.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

built
43 modules in 98.7 s
deployed
live · 43 modules on 3 nodes · push→live —
deployed
needs_rolling_replace · 43 modules on 0 nodes · push→live —

Changed files

  • modified config/prod.exs
  • modified docs/operations/production-deploy-runbook.md

Diff

2 files changed, +46 -1

config/prod.exs modified +4 -1

@@ -17,7 +17,10 @@ config :openagents, OpenAgentsWeb.Endpoint,

17 17
  force_ssl: [
18 18
    rewrite_on: [:x_forwarded_proto],
19 19
    exclude: [
20
      # paths: ["/health"],
20
      # The load balancer health check probes these paths over plain HTTP.
21
      # A redirect counts as a failed probe, which drops every node from
22
      # rotation at once.
23
      paths: ["/health", "/healthz"],
21 24
      hosts: ["localhost", "127.0.0.1"]
22 25
    ]
23 26
  ]
docs/operations/production-deploy-runbook.md modified +42

@@ -194,6 +194,48 @@ Do not report success without all of the following:

194 194
- Issues, git clone/fetch, and a read-only computer job work.
195 195
- New configuration is live without exposing secret values.
196 196
197
## Load balancer health check
198
199
The global backend service `sarah-backend` decides which nodes receive
200
traffic. The health check it references determines whether a broken node
201
leaves rotation:
202
203
- A TCP check (`sarah-hc-tcp`) only verifies that port `8080` accepts
204
  connections. A node that answers every request with `500` still passes,
205
  so it keeps serving a third of traffic while broken. Issue #105 records
206
  an incident where this happened.
207
- The HTTP check `sarah-hc-healthz` probes `GET /healthz` on port `8080`
208
  and requires `200`. The application returns `503` from `/healthz` when
209
  the database is unreachable, boot has not converged, the deployment
210
  participant is not ready, admission is not ready, or the forge cache is
211
  not ready, so an unhealthy node drops out of rotation.
212
213
Requirements before pointing the backend at the HTTP check:
214
215
1. The running image must exclude `/health` and `/healthz` from
216
   `force_ssl` in `config/prod.exs`. GCP HTTP probes are plain HTTP;
217
   `Plug.SSL` answers them with a `301` redirect, which counts as a failed
218
   probe. If every node redirects, the backend loses all members at once
219
   and the site serves `502`. This happened on 2026-08-23.
220
2. Verify the exact probe shape on every node first. `curl
221
   http://localhost:8080/healthz` is not sufficient because `Plug.SSL`
222
   excludes the `localhost` host. Use a non-localhost host header:
223
224
   ```sh
225
   curl -s -o /dev/null -w '%{http_code}' \
226
     -H 'Host: <node-external-ip>' http://localhost:8080/healthz
227
   ```
228
229
   Require `200` on all three nodes before swapping.
230
3. Swap, then watch `gcloud compute backend-services get-health
231
   sarah-backend --global`. If any node goes `UNHEALTHY` unexpectedly,
232
   revert immediately:
233
234
   ```sh
235
   gcloud compute backend-services update sarah-backend --global \
236
     --health-checks sarah-hc-tcp --global-health-checks
237
   ```
238
197 239
## Known failure modes
198 240
199 241
| Symptom | Cause | Fix |

This page updates live while a promote is in flight · changelog