2026-09-01

The Next.js standalone healthcheck trap on Coolify

The build succeeded. The container came up. Coolify still called it unhealthy.

The build finished clean. The container started. Coolify still marked the deployment unhealthy and rolled it back to the previous version. Nothing in the logs pointed at a crash, because nothing had crashed.

This happened four times in a row on one of my Next.js apps before I worked out what was going on. The app was a Next.js 16 build with output: 'standalone', deployed to Coolify from a Dockerfile. Two separate faults were stacked on top of each other, and either one on its own is enough to fail the healthcheck.

Where the standalone server actually binds

Next.js's standalone server.js reads process.env.HOSTNAME to decide what address to bind to. Docker sets HOSTNAME itself, and it sets it to the container's own address rather than to 127.0.0.1. So the standalone server ends up bound to the container's IP instead, which a healthcheck aimed at localhost cannot reach.

# Dockerfile, runner stage
ENV HOSTNAME=0.0.0.0

That line alone isn't quite enough. Set HOSTNAME=0.0.0.0 as a Coolify environment variable as well. Coolify's own runtime .env can re-inject HOSTNAME after the image builds, and when it does, the env var wins over whatever you baked into the Dockerfile.

How the healthcheck tool resolves the address

Coolify's healthcheck tries curl first, and falls back to busybox wget if curl isn't in the image. The node:*-alpine image I was building on did not have curl, so the fallback is the path it took. Busybox wget resolves localhost to IPv6 (::1) and doesn't retry over IPv4 if that fails. If the server is only reachable over IPv4, busybox wget reports connection refused even though the process is up and serving requests fine.

curl doesn't have this problem. It does Happy Eyeballs, trying both stacks, so once curl is available in the image the healthcheck stops caring which stack the server picked.

The fix

# Dockerfile, runner stage
RUN apk add --no-cache curl
ENV HOSTNAME=0.0.0.0

Add HOSTNAME=0.0.0.0 as a Coolify environment variable too, for the reason above. While you're in the healthcheck settings, it's worth bumping health_check_retries up to around 20. Coolify seems to ignore its own 40 second start period and fires roughly three retries earlier than that, which is enough to fail a slightly slow-starting container even after the real fix is in.

One thing worth knowing while you're debugging this: Coolify only swaps traffic to a new container once it reports healthy. A failed deploy leaves the live site running on the old container the whole time, so none of this is a production outage while you sort it out.

How to tell which one is biting you

Both faults look identical in the Coolify UI: the build succeeds, the container starts, and the healthcheck marks it unhealthy. To tell them apart, shell into the running container and try the same kind of request the healthcheck is making.

Topics

Still stuck? Send it to Emsden Studio and get a straight answer on what it is and what fixing it costs.

More notes

All notes