Skip to content

Fix Ghost healthcheck to tolerate http→https redirect#7

Merged
lunarthegrey merged 1 commit into
mainfrom
fix-healthcheck-hairpin-nat
Apr 24, 2026
Merged

Fix Ghost healthcheck to tolerate http→https redirect#7
lunarthegrey merged 1 commit into
mainfrom
fix-healthcheck-hairpin-nat

Conversation

@lunarthegrey
Copy link
Copy Markdown

Summary

Change Ghost's healthcheck from:

wget -qO- http://localhost:2368/ghost/api/admin/site/

to:

wget --spider -S --max-redirect=0 -T 3 http://localhost:2368/ghost/api/admin/site/ 2>&1 | grep -q 'HTTP/1'

Why

Ghost redirects any request whose Host: header doesn't match its configured url. From inside the container http://localhost:2368/… returns a 301 to https://tower.unredacted.org/… — wget by default follows the redirect, which requires hairpin NAT (routing to your own public IP from inside a container). Many VPS setups and firewall configs block hairpin, so the follow fails, wget exits non-zero, Docker marks the container unhealthy, and Traefik stops routing to it. Ghost itself is fine — the site just looks down.

The new check uses --max-redirect=0 so wget stops at the 301 instead of following, then greps for any HTTP/1 response line. Any Ghost response (2xx/3xx/4xx/5xx) counts as healthy; a dead port emits no header → still fails correctly.

Spotted on a live Coolify deploy — Ghost booted clean but repeated GET /ghost/api/admin/site/ 301 lines every 30s in the logs, site returning 503 through Traefik.

Test plan

  • bash .github/scripts/test-patch.sh passes (existing assertions still match)
  • docker compose config --quiet parses the new CMD-SHELL healthcheck
  • After merge + sync + redeploy: docker compose ps shows ghost (healthy), site responds through Traefik

🤖 Generated with Claude Code

Ghost redirects any request whose Host header doesn't match its
configured url — so `wget http://localhost:2368/ghost/api/admin/site/`
from inside the container gets a 301 to the public URL, and wget
follows it back out through Traefik. That only works if the host
supports hairpin NAT (routing to its own public IP from inside a
container), which is commonly blocked on VPS and firewall setups.

On such hosts the healthcheck fails silently, Docker marks the
container unhealthy, and Traefik stops routing to it — Ghost is
running fine but the site shows as down.

New healthcheck uses `--max-redirect=0` so wget stops at the 301
instead of trying to follow, plus `-S --spider` to emit the server
response headers, then greps for any `HTTP/1` status line. Ghost
responding at all (any 2xx/3xx/4xx/5xx) counts as healthy — a dead
container won't emit headers, so the check still fails correctly.

Spotted on a live Coolify deploy where Ghost booted successfully
but Traefik returned 503.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lunarthegrey lunarthegrey merged commit f0dcfd5 into main Apr 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant