From 524aa4d860d4d4ad1b4f28c00a3390edb884260d Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Sat, 29 Aug 2026 22:33:21 -0700 Subject: [PATCH] fix(phasefinal-web): healthcheck targeted ::1, so traefik skipped the container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The healthcheck used http://localhost/, which resolves to ::1 in nginx:alpine while nginx listens on IPv4 only — so it never passed, the container stayed unhealthy, and Traefik silently declined to create a router for it. That presents as a broken docker provider: correct labels, right network, no route, no error. Target 127.0.0.1 explicitly and add a start_period. Adds the apex router (301 phasefinal.com -> www) and drops the file-provider workaround, which was mitigating the wrong diagnosis. --- stacks/phasefinal-web/README.md | 20 +++++++++----- stacks/phasefinal-web/compose.yaml | 16 ++++++++++- .../conf/traefik-dynamic-phasefinal-web.yml | 27 ------------------- 3 files changed, 28 insertions(+), 35 deletions(-) delete mode 100644 stacks/phasefinal-web/conf/traefik-dynamic-phasefinal-web.yml diff --git a/stacks/phasefinal-web/README.md b/stacks/phasefinal-web/README.md index 1b2e765..2265629 100644 --- a/stacks/phasefinal-web/README.md +++ b/stacks/phasefinal-web/README.md @@ -21,14 +21,20 @@ requests rule. Both must be undone on import: Without these the page silently falls back to system fonts and looks wrong rather than broken, which is the failure mode you won't notice. -## Routing +## Routing — and the healthcheck trap that looks like a routing bug -`conf/traefik-dynamic-phasefinal-web.yml` deploys to -**`/opt/docker/conf/traefik-ana/config/phasefinal-web.yml`** — not into this -stack's own conf dir. It exists because Traefik's docker provider on ana-docker -was not registering newly-created containers on 2026-08-29; the compose file -still carries the correct `traefik.*` labels, so once that is fixed the file -router can be deleted. See the header comment in that file. +Routers come from the compose labels: `phasefinal-web` serves +`www.phasefinal.com`; `phasefinal-apex` 301s `phasefinal.com` to it. + +⚠ **Traefik silently skips containers Docker reports as unhealthy.** There is no +error, no log line, and no router — it looks exactly like a broken provider. The +first deploy of this stack hit that: the healthcheck used +`http://localhost/`, which resolves to `::1` in `nginx:alpine`, and nginx listens +on IPv4 only, so the check got "Connection refused" forever and the container +never left `unhealthy`. **Use `127.0.0.1`, never `localhost`, in a healthcheck +for an IPv4-only listener** — and if a correctly-labelled container never appears +in `/api/http/routers`, check `docker inspect --format '{{.State.Health.Status}}'` +*before* suspecting Traefik. ## Deploy diff --git a/stacks/phasefinal-web/compose.yaml b/stacks/phasefinal-web/compose.yaml index d000e3e..ad6c6f2 100644 --- a/stacks/phasefinal-web/compose.yaml +++ b/stacks/phasefinal-web/compose.yaml @@ -9,16 +9,30 @@ services: networks: - tnet healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost/"] + # 127.0.0.1, NOT localhost: in this image `localhost` resolves to ::1 and + # nginx listens on IPv4 only, so `localhost` gives "Connection refused" + # forever. Traefik SKIPS unhealthy containers, so a wrong healthcheck here + # silently means "no route" rather than "unhealthy service". + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"] interval: 30s timeout: 5s retries: 3 + start_period: 10s labels: - traefik.enable=true - traefik.http.routers.phasefinal-web.rule=Host(`www.phasefinal.com`) - traefik.http.routers.phasefinal-web.tls=true - traefik.http.routers.phasefinal-web.tls.certresolver=anaprod - traefik.http.services.phasefinal-web.loadbalancer.server.port=80 + # apex -> www, 301 + - traefik.http.routers.phasefinal-apex.rule=Host(`phasefinal.com`) + - traefik.http.routers.phasefinal-apex.tls=true + - traefik.http.routers.phasefinal-apex.tls.certresolver=anaprod + - traefik.http.routers.phasefinal-apex.service=phasefinal-web + - traefik.http.routers.phasefinal-apex.middlewares=phasefinal-to-www + - traefik.http.middlewares.phasefinal-to-www.redirectregex.regex=^https?://phasefinal\.com/(.*) + - traefik.http.middlewares.phasefinal-to-www.redirectregex.replacement=https://www.phasefinal.com/$${1} + - traefik.http.middlewares.phasefinal-to-www.redirectregex.permanent=true networks: tnet: diff --git a/stacks/phasefinal-web/conf/traefik-dynamic-phasefinal-web.yml b/stacks/phasefinal-web/conf/traefik-dynamic-phasefinal-web.yml deleted file mode 100644 index a12fe55..0000000 --- a/stacks/phasefinal-web/conf/traefik-dynamic-phasefinal-web.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Traefik file-provider router for phasefinal-web. -# -# DEPLOYS TO: ana-docker:/opt/docker/conf/traefik-ana/config/phasefinal-web.yml -# (NOT /opt/docker/conf/phasefinal-web/ — the file provider watches -# traefik-ana/config, so this file lives with Traefik, not the stack.) -# -# WHY A FILE ROUTER instead of container labels: as of 2026-08-29 the Traefik -# docker provider on ana-docker was not registering newly-created containers — -# phasefinal-web carried correct traefik.* labels, sat on traefik-net, and never -# appeared in /api/http/routers, with no provider activity in the logs. The file -# provider (watch: true) picks this up with no Traefik restart, so no other -# public service is interrupted. If the docker provider is fixed, this file can -# be deleted and the compose labels will take over. -http: - routers: - phasefinal-web: - rule: "Host(`www.phasefinal.com`)" - service: phasefinal-web - entryPoints: - - websecure - tls: - certResolver: anaprod - services: - phasefinal-web: - loadBalancer: - servers: - - url: "http://phasefinal-web:80"