2026.09 / Coolify healthcheck 052
Coolify health check fails: curl or wget not found
Your image builds, the application process starts, and its logs may even say it is listening. Then Coolify reports the container as unhealthy or the deployment fails with a line like /bin/sh: curl: not found or wget: not found. This is usually not an application crash. The command used to prove readiness is missing from the final runtime image.
Coolify’s health-check documentation says its HTTP checks require curl or wget inside the container. Installing a client on the VPS, in a CI runner, or only in a discarded Docker build stage does not satisfy that requirement. This guide fixes the tool boundary first, then separates it from the next four common faults: wrong path, wrong port, loopback binding, and insufficient startup time.
Do not disable health checks merely to turn a deployment green. First reproduce the exact probe inside the exact image that Coolify is running.
Read the failure before changing the Dockerfile
Open the failed deployment logs and the application’s health-check settings. Record the configured path, port, expected status, interval, timeout, retries, and start period. Then inspect Docker’s current view on the server:
docker inspect APP_CONTAINER --format '{{json .Config.Healthcheck}}'
docker inspect APP_CONTAINER --format '{{json .State.Health}}'
docker exec APP_CONTAINER sh -lc \
'command -v curl || command -v wget || true'
Replace APP_CONTAINER privately with the real container name. The first command reveals the command Docker actually executes. The second preserves the probe’s exit codes and recent output. Do not dump the complete container environment into a ticket: it can contain database URLs, tokens, and passwords.
| Probe output | Boundary to fix |
|---|---|
curl: not found or wget: not found | The final image lacks the requested executable |
Connection refused | The app is not ready, uses another port, or listens on another address |
HTTP 404 | The configured health path does not exist |
HTTP 301, 302, 401, or 403 | The endpoint redirects or requires authentication |
| Timeout | The app is blocked, starts too slowly, or the probe targets the wrong endpoint |
HTTP 500 or 503 | The endpoint ran but the application reports a dependency or readiness fault |
Install the probe in the final runtime stage
For an Alpine-based runtime image, install one small HTTP client in the stage that becomes the deployed image:
FROM node:22-alpine AS runtime
RUN apk add --no-cache curl
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
For Debian or Ubuntu based images:
FROM node:22-bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
The stage name is unimportant; its position is not. In a multi-stage Dockerfile, packages installed in build disappear unless that stage is also the final image. Run docker build, then test the resulting image—not the build stage:
docker build -t app-healthcheck-test .
docker run --rm app-healthcheck-test sh -lc 'command -v curl'
docker run --rm -d --name app-healthcheck-test -p 127.0.0.1:18080:3000 \
app-healthcheck-test
curl --fail --silent --show-error http://127.0.0.1:18080/health
docker rm -f app-healthcheck-test
The published test port is bound to loopback so it does not create a new public service. Adapt the internal port and path to the application. If the image is intentionally distroless or scratch, it may contain neither a shell nor a package manager. In that case, prefer an application-native probe binary or define a Docker HEALTHCHECK that invokes a binary already present. Do not copy a dynamically linked curl executable alone without its libraries and certificate store.
Choose one clear source of health truth
A health check can come from the Dockerfile, a Compose healthcheck:, or Coolify’s application settings. Multiple layers can override or disagree with one another. Inspect the effective command and keep one intentional definition wherever possible.
A Dockerfile-owned HTTP check might look like this:
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \
CMD curl --fail --silent --show-error \
http://127.0.0.1:3000/health || exit 1
Docker treats exit code 0 as healthy and 1 as unhealthy. The command runs inside the container’s own network namespace, so 127.0.0.1 means that container—not the VPS and not another Compose service. If Coolify generates the check instead, configure its path and port to match the application and leave the Dockerfile definition out. After changing or removing a Dockerfile HEALTHCHECK, force a clean rebuild so an old image layer is not mistaken for current configuration.
If curl exists, test the next boundaries in order
1. Is the application listening on the expected internal port?
docker exec APP_CONTAINER sh -lc \
'curl --fail --silent --show-error http://127.0.0.1:3000/health'
docker logs --since 10m APP_CONTAINER
Use the container port, not a host-side published port. Coolify routing also needs the application’s configured port to agree with the process. A public 200 does not prove the internal readiness command is correct, and an internal success does not prove the public router is correct; verify both.
2. Does the app bind to a reachable address?
For normal container routing, web servers should usually bind to 0.0.0.0 on their internal port. Binding only to 127.0.0.1 can prevent Traefik or other containers from reaching the service, even though a health command executed inside the same container succeeds. Fix the application’s listen setting rather than probing a host address.
3. Is the health path deliberately boring?
Use an unauthenticated endpoint such as /health that returns a small 200 response when the app is ready. Exempt it from canonical-host redirects and login middleware. Do not return environment values, database details, internal hostnames, version dumps, or stack traces. A health endpoint should answer readiness, not become a diagnostics leak.
4. Does startup need a grace period?
Migrations, cache warming, and large runtime initialization can make a correct app temporarily refuse connections. Set a realistic start period and enough retries after measuring startup logs. Do not hide a permanent dependency failure behind a ten-minute grace period. The endpoint should become healthy predictably, and a broken database or required dependency should still be visible.
Rebuild and verify the deployment
- Commit the final-stage package change or native probe with the application source.
- Trigger a force rebuild if the previous image may be cached.
- Read the deployment’s terminal status and health output—not only the queued response.
- Confirm Docker reports the replacement container as
healthy. - Run the effective health command manually inside the replacement container.
- Request the public application through HTTPS and assert unique page or API content.
- Restart or recreate once and verify the state becomes healthy again within the measured window.
docker inspect APP_CONTAINER \
--format 'status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}}'
curl --fail --silent --show-error https://app.example.com/health
curl --fail --silent --show-error https://app.example.com/ | \
grep -F 'EXPECTED PUBLIC CONTENT'
Use a placeholder hostname in shared notes and the real hostname only in your private terminal. Checking unique content matters because a proxy fallback, stale container, or SPA fallback can return HTTP 200 for the wrong thing.
Wrong turns that create a second problem
- Installing curl on the VPS: the probe executes inside the application container.
- Installing it only in the build stage: multi-stage builds discard that filesystem.
- Disabling health checks immediately: this can route traffic to a process that is not ready.
- Changing the expected code to match a redirect or login page: repair the health endpoint instead.
- Probing the public domain from inside the container: this adds DNS, TLS, Cloudflare, and router dependencies to an application-readiness test.
- Using
localhostfor another Compose service: address that service by its Compose service name; localhost is the current container. - Dumping all settings while debugging: health-check evidence does not require publishing secrets or complete environments.
Compact acceptance checklist
- The effective Docker health command is known.
- Its executable exists in the final deployed image.
- The exact command exits zero when run inside the container.
- The process listens on the configured internal port and appropriate bind address.
- The path returns the expected status without auth, redirects, or sensitive output.
- The start period covers measured startup without concealing permanent failures.
- Coolify reaches a terminal successful deployment state and Docker reports healthy.
- The public HTTPS route serves the intended application content after recreation.
Authoritative references: Coolify’s official health-check documentation states that its container-side HTTP checks need curl or wget and explains Dockerfile-defined checks. Its no available server troubleshooting guide recommends running the health command manually and checking that required tools exist. Docker’s Dockerfile HEALTHCHECK reference defines probe execution, exit codes, intervals, timeouts, start periods, and retries.