Skip to content
Blog

Monitor a Coolify app from outside the server

Coolify health checks confirm a container answers on localhost, not that your app is reachable. Where they fall short and how to check from outside the box.

Coolify makes self-hosting a Docker deployment pleasant enough that it is easy to assume the platform is also watching whether your app stays up. Coolify is watching something, and it is worth being precise about what. The gap between "Coolify says healthy" and "the app is reachable" is where an outage sits unnoticed until a customer emails you.

What do Coolify's health checks actually check?

Coolify's health checks confirm that a process inside the container answers on localhost. They are Docker healthchecks, either the ones you configure in Coolify's UI or the ones baked into your image's Dockerfile with a HEALTHCHECK instruction. Docker runs a command inside the container on an interval and marks it healthy or unhealthy based on the exit code. For an HTTP-based check, the command is typically a curl or wget against localhost from inside the container.

The mechanism has real, specific requirements and real, specific limits.

The image needs curl or wget in it, or the check cannot run. This trips people up constantly: installing curl on the Coolify host does nothing, because the check runs inside the application container, not on the host. A minimal image built FROM scratch or a slim distroless base has neither binary by default, and the healthcheck fails immediately, for a reason that has nothing to do with whether the app works.

Docker Compose deployments do not get Coolify's healthcheck UI at all. A Compose stack needs the healthcheck defined in the Dockerfile or in the healthcheck block of docker-compose.yaml, for each service individually. Coolify's dashboard toggle does not apply. It is easy to deploy a Compose app, never touch this, and have no health check running for it whatsoever.

A container-level Dockerfile HEALTHCHECK silently wins over Coolify's UI setting. If your Dockerfile already declares one, Coolify's settings page detects it and shows a warning. The page still lets you flip on a "custom" healthcheck, but the override has no actual effect, because Docker keeps running the Dockerfile's original check on its original interval. Whatever you configured in the UI is decorative until you change the Dockerfile itself.

The healthcheck only ever asks the container about itself, from inside itself. Even a correctly configured healthcheck answers one question: does a process inside this container respond on localhost. It says nothing about four things:

  • Whether the box the container runs on is reachable from the internet.
  • Whether DNS for your domain resolves.
  • Whether the reverse proxy in front of it (Traefik, in Coolify's default setup) is routing correctly.
  • Whether a firewall change upstream just cut off every path in.

A container can be perfectly healthy by Docker's definition while nobody outside the server can reach it at all.

Why can a healthy container still mean a down app?

Because most outages happen outside the container, where the healthcheck cannot look. Picture the failure modes side by side.

  • A deploy that breaks your app's /health endpoint but leaves the process running. Docker's healthcheck can catch this, if you built one and the image has curl.
  • A deploy that works fine but breaks Traefik's routing config for the domain. The container stays healthy the entire time, because nothing about the container changed. A visitor gets a 502 from the proxy and Coolify's dashboard shows green.
  • A DNS record pointed somewhere else, an expired TLS certificate, or a host firewall rule that starts dropping inbound traffic on 443. None of these touch the container or its healthcheck, and all of them take your site down for every visitor while Coolify reports nothing wrong.
Failure Container healthcheck sees it Outside HTTP check sees it
App process dead Yes, if a healthcheck is configured Yes
/health broken, process running Yes, if the check hits /health Yes, if the check hits the same path
Traefik routing broken No Yes
DNS record changed No Yes
TLS certificate expired No Yes
Host firewall dropping port 443 No Yes

Coolify's health checks live inside the boundary of the container. Everything that can break your app for an actual visitor without touching the container lives outside that boundary, by definition, and needs to be checked from outside it too.

How do you add a check from outside the server?

Point a check at the public URL and run it from somewhere that is not the box. The fix does not touch Coolify's configuration at all.

An HTTP check against the real, public URL. On RealUptime Monitor, point a check at https://yourapp.example.com, the address a visitor actually types or clicks, not an internal container port. Run from more than one region, the check catches the Traefik-misroute case, the DNS case, and the firewall case, all of which a container-internal healthcheck cannot see by construction. A single region already beats nothing. Multiple regions tell you whether a failure is global or a routing problem specific to one path into your server.

A status change that actually reaches you. RealUptime's alerting fans a state change out to email, Slack, PagerDuty, or a webhook the moment a region flips from up to down. The same two-consecutive-failure pattern keeps a slow blip from paging you at 3am for nothing.

The Monitor agent, if you also want host-level visibility. Some checks only make sense from the Coolify host itself: disk space, memory, or whether a service bound only to localhost is listening. The Monitor agent installs alongside Coolify's own containers, with network_mode: host so it sees the host's own network, not a container's. It reports next to your outside HTTP checks in the same dashboard. The full walkthrough, including the install command and a working docker-compose.yaml entry, is in the Coolify docs.

Why does a Coolify migration container show as unhealthy?

Because Coolify's healthcheck machinery treats a one-off job like a long-running service. Database migrations run at deploy time are the most common example. They show as "unhealthy" once they exit, even though exiting is exactly what a one-off job is supposed to do. A migration container that flashes red in Coolify's dashboard right after a deploy that otherwise worked fine is this pattern.

Coolify's Compose support has an exclude_from_hc: true flag you can set on a one-off service to pull it out of healthcheck monitoring entirely. Set the flag rather than learning to ignore the alert, since ignoring alerts is how a real failure eventually gets ignored too.

The problem differs from the one this post is mainly about, checks that never fire when they should, but the lesson is the same. A healthcheck's scope, what it watches and what triggers it, has to match what you want to know about. Otherwise it produces false confidence or false alarms, and either one erodes trust in the alert faster than having no check at all.

How do you verify the outside check works?

Break something on purpose and confirm the check catches it, rather than trusting it blind until the day it matters. Three cheap ways to do this on a Coolify deployment:

  • Stop the container (docker stop on the app's container, or pause the deployment from Coolify's UI) and confirm the outside check flips to down within your configured interval plus the failure threshold. This also tells you your alert channels (email, Slack, webhook) actually fire, not just that the dashboard changes color.
  • Temporarily break the Traefik label or domain routing for the service and confirm the outside check catches a 502 or timeout even though the container itself stays healthy the whole time. This is the specific gap a container-internal healthcheck cannot see, and it is the one worth proving to yourself directly.
  • Let the check recover by restoring the container or the routing and confirm the alert clears on its own, with no manual resolution needed, the same recovery behavior described for heartbeat monitors in our cron job monitoring guide.

Five minutes doing this once is cheaper than discovering during a real incident that the alert channel was misconfigured, or that the check interval was too loose to matter.