The app service healthcheck probes http://localhost:3000/api/health. Inside the container, localhost resolves to ::1 (IPv6) first per /etc/hosts, but the Next.js standalone server listens only on 0.0.0.0 (IPv4). The probe hits ::1:3000, gets Connection refused, and the container is reported unhealthy — even though the app serves HTTP 200 on both / and /api/health.
This is why shared-memory-app-1 showed (unhealthy) for weeks while functioning normally.
Fix
One-line change: probe http://127.0.0.1:3000/api/health instead of localhost, matching the interface the app actually binds. The db and embedder healthchecks already avoid localhost.
No runtime/app code changes; healthcheck only. Takes effect on next docker compose up -d (container recreate).
## What
The `app` service healthcheck probes `http://localhost:3000/api/health`. Inside the container, `localhost` resolves to `::1` (IPv6) first per `/etc/hosts`, but the Next.js standalone server listens only on `0.0.0.0` (IPv4). The probe hits `::1:3000`, gets `Connection refused`, and the container is reported **unhealthy** — even though the app serves HTTP 200 on both `/` and `/api/health`.
This is why `shared-memory-app-1` showed `(unhealthy)` for weeks while functioning normally.
## Fix
One-line change: probe `http://127.0.0.1:3000/api/health` instead of `localhost`, matching the interface the app actually binds. The `db` and `embedder` healthchecks already avoid `localhost`.
## Verification (on production)
- `docker exec ... wget http://localhost:3000/api/health` -> Connection refused (`::1`)
- `docker exec ... wget http://127.0.0.1:3000/api/health` -> HTTP 200
- container `/etc/hosts`: `::1 localhost`
- app listen socket: `0.0.0.0:3000` (IPv4 only)
No runtime/app code changes; healthcheck only. Takes effect on next `docker compose up -d` (container recreate).
Inside the app container localhost resolves to ::1 (IPv6) first, but the
Next.js standalone server listens only on 0.0.0.0 (IPv4). The healthcheck
probed http://localhost:3000/api/health and got Connection refused on ::1,
so the container reported unhealthy for weeks despite serving 200 on both
/ and /api/health. Switch the probe to 127.0.0.1 to match the bound iface.
The db and embedder healthchecks already avoid localhost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No issues found. Checked for correctness and consistency.
Verified the IPv4/IPv6 reasoning is sound: the Dockerfile sets HOSTNAME=0.0.0.0, so the Next.js standalone server binds IPv4-only; an in-container localhost probe hits ::1 first and is refused. 127.0.0.1 is the correct fix. Confirmed APP_BIND only affects the host-side port publish, not the in-container bind, so the probe is always reachable. The change is consistent with the existing embedder healthcheck (also 127.0.0.1), and the CMD-SHELL string is syntactically clean.
### Code review
No issues found. Checked for correctness and consistency.
Verified the IPv4/IPv6 reasoning is sound: the Dockerfile sets `HOSTNAME=0.0.0.0`, so the Next.js standalone server binds IPv4-only; an in-container `localhost` probe hits `::1` first and is refused. `127.0.0.1` is the correct fix. Confirmed `APP_BIND` only affects the host-side port publish, not the in-container bind, so the probe is always reachable. The change is consistent with the existing `embedder` healthcheck (also `127.0.0.1`), and the CMD-SHELL string is syntactically clean.
🤖 Generated with [Claude Code](https://claude.ai/code)
jknapp
merged commit 41149fe709 into main2026-06-12 19:19:47 +00:00
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.
What
The
appservice healthcheck probeshttp://localhost:3000/api/health. Inside the container,localhostresolves to::1(IPv6) first per/etc/hosts, but the Next.js standalone server listens only on0.0.0.0(IPv4). The probe hits::1:3000, getsConnection refused, and the container is reported unhealthy — even though the app serves HTTP 200 on both/and/api/health.This is why
shared-memory-app-1showed(unhealthy)for weeks while functioning normally.Fix
One-line change: probe
http://127.0.0.1:3000/api/healthinstead oflocalhost, matching the interface the app actually binds. Thedbandembedderhealthchecks already avoidlocalhost.Verification (on production)
docker exec ... wget http://localhost:3000/api/health-> Connection refused (::1)docker exec ... wget http://127.0.0.1:3000/api/health-> HTTP 200/etc/hosts:::1 localhost0.0.0.0:3000(IPv4 only)No runtime/app code changes; healthcheck only. Takes effect on next
docker compose up -d(container recreate).Code review
No issues found. Checked for correctness and consistency.
Verified the IPv4/IPv6 reasoning is sound: the Dockerfile sets
HOSTNAME=0.0.0.0, so the Next.js standalone server binds IPv4-only; an in-containerlocalhostprobe hits::1first and is refused.127.0.0.1is the correct fix. ConfirmedAPP_BINDonly affects the host-side port publish, not the in-container bind, so the probe is always reachable. The change is consistent with the existingembedderhealthcheck (also127.0.0.1), and the CMD-SHELL string is syntactically clean.🤖 Generated with Claude Code