Both mos-test-1 and mos-test-2 stacks (Portainer IDs 146/147) were stuck at replicas=0/1 with the gateway exiting cleanly (exit code 0) after exactly 84 seconds.
The 84s timing is not accidental — it matches the old healthcheck configuration precisely: start_period(20s) + 3 x interval(30s) = 80s → first SIGTERM at ~84s
After 3 failed wget healthchecks, Docker Swarm sends SIGTERM. NestJS/Fastify shuts down cleanly → exit 0. Since restart_policy: on-failure only restarts on non-zero exits, the container stayed dead.
Root cause candidates for wget failures
IPv6 resolution: localhost may resolve to ::1 on Alpine, but the gateway binds to 0.0.0.0. Using 127.0.0.1 explicitly eliminates this.
NestJS cold start: The GC service logs a non-fatal error during startup (~30-40s into the boot). wget may be attempting the check before the HTTP server is fully accepting connections.
Changes
1. Switch healthcheck from wget to node -e http.get
Uses 127.0.0.1 (not localhost) to force IPv4 and avoid any IPv6 resolution race. node is guaranteed available in the runtime image (it IS the runtime). retries increased from 3 to 5: gives 150s of retry window after start_period. start_period increased from 20s to 60s: covers the NestJS GC cold-start window (~40-50s).
2. Change restart policy from on-failure to any
Prevents the SIGTERM/clean-exit edge case from permanently killing the container. any restarts on exit 0, SIGTERM, and non-zero exits alike.
Test plan
CI passes on this branch
PR merged to main
Stacks 146 + 147 redeployed via Portainer API with updated stack file
Both gateway services show replicas=1/1 in Portainer
## Problem
Both `mos-test-1` and `mos-test-2` stacks (Portainer IDs 146/147) were stuck at `replicas=0/1` with the gateway exiting cleanly (exit code 0) after exactly 84 seconds.
The 84s timing is not accidental — it matches the old healthcheck configuration precisely:
`start_period(20s) + 3 x interval(30s) = 80s → first SIGTERM at ~84s`
After 3 failed `wget` healthchecks, Docker Swarm sends SIGTERM. NestJS/Fastify shuts down cleanly → exit 0. Since `restart_policy: on-failure` only restarts on non-zero exits, the container stayed dead.
## Root cause candidates for wget failures
- IPv6 resolution: `localhost` may resolve to `::1` on Alpine, but the gateway binds to `0.0.0.0`. Using `127.0.0.1` explicitly eliminates this.
- NestJS cold start: The GC service logs a non-fatal error during startup (~30-40s into the boot). wget may be attempting the check before the HTTP server is fully accepting connections.
## Changes
### 1. Switch healthcheck from wget to `node -e http.get`
Uses `127.0.0.1` (not `localhost`) to force IPv4 and avoid any IPv6 resolution race.
`node` is guaranteed available in the runtime image (it IS the runtime).
`retries` increased from 3 to 5: gives 150s of retry window after start_period.
`start_period` increased from 20s to 60s: covers the NestJS GC cold-start window (~40-50s).
### 2. Change restart policy from `on-failure` to `any`
Prevents the SIGTERM/clean-exit edge case from permanently killing the container. `any` restarts on exit 0, SIGTERM, and non-zero exits alike.
## Test plan
- CI passes on this branch
- PR merged to main
- Stacks 146 + 147 redeployed via Portainer API with updated stack file
- https://mos-test-1.woltje.com/health returns 200 {"status":"ok"}
- https://mos-test-2.woltje.com/health returns 200 {"status":"ok"}
- Both gateway services show replicas=1/1 in Portainer
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Both
mos-test-1andmos-test-2stacks (Portainer IDs 146/147) were stuck atreplicas=0/1with the gateway exiting cleanly (exit code 0) after exactly 84 seconds.The 84s timing is not accidental — it matches the old healthcheck configuration precisely:
start_period(20s) + 3 x interval(30s) = 80s → first SIGTERM at ~84sAfter 3 failed
wgethealthchecks, Docker Swarm sends SIGTERM. NestJS/Fastify shuts down cleanly → exit 0. Sincerestart_policy: on-failureonly restarts on non-zero exits, the container stayed dead.Root cause candidates for wget failures
localhostmay resolve to::1on Alpine, but the gateway binds to0.0.0.0. Using127.0.0.1explicitly eliminates this.Changes
1. Switch healthcheck from wget to
node -e http.getUses
127.0.0.1(notlocalhost) to force IPv4 and avoid any IPv6 resolution race.nodeis guaranteed available in the runtime image (it IS the runtime).retriesincreased from 3 to 5: gives 150s of retry window after start_period.start_periodincreased from 20s to 60s: covers the NestJS GC cold-start window (~40-50s).2. Change restart policy from
on-failuretoanyPrevents the SIGTERM/clean-exit edge case from permanently killing the container.
anyrestarts on exit 0, SIGTERM, and non-zero exits alike.Test plan