Skip to content
Blog

Monitor a Coolify app from outside the server

Coolify's built-in health checks watch whether a container process is alive, not whether your app is reachable from the internet. What Coolify's health checks cover, where they fall short, and how to add a check that runs 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. It is watching something, and it is worth being precise about what, because the gap between "Coolify says healthy" and "the app is reachable" is where an outage sits unnoticed until a customer emails you.

What Coolify's health checks actually do

Coolify's built-in health checks 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. Under the hood, 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, that command is typically a curl or wget against localhost from inside the container.

That 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. If your app is deployed as a Compose stack, the healthcheck has to be defined in the Dockerfile or the docker-compose.yaml's healthcheck block 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, shows a warning, and lets you flip on a "custom" healthcheck anyway, but the override has no actual effect: 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.

It 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 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, or 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 "the container is healthy" and "the app is up" are different claims

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 Traefik's routing config for the domain silently breaks: 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 that gets pointed somewhere else, a TLS certificate that expires, a firewall rule on the host that starts dropping inbound traffic on 443: none of these touch the container, so none of them touch its healthcheck, and all of them take your site down for every visitor on earth while Coolify reports nothing wrong.

The pattern across all of these: 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.

Adding a check from outside

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

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 it from more than one region and it 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, on the same two-consecutive-failure pattern that keeps a slow blip from paging you at 3am for nothing.

The Monitor agent, if you also want host-level visibility. For checks that make sense to run from the Coolify host itself, disk space, memory, whether a specific service only bound to localhost is listening, the Monitor agent installs alongside Coolify's own containers (network_mode: host so it sees the host's own network, not a container's), and 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.

One-off jobs and the false alarms they cause

A related, narrower problem worth knowing about even if you are not chasing an outage: Coolify has a documented pattern where one-off jobs, database migrations run at deploy time being the most common example, get picked up by the same healthcheck machinery as long-running services and show as "unhealthy" once they exit, even though exiting is exactly what a one-off job is supposed to do. If you have seen a migration container flash red in Coolify's dashboard right after a deploy that otherwise worked fine, this is why. 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, which is worth doing rather than learning to ignore the alert, since that is how a real failure eventually gets ignored too.

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

Verifying the outside check actually works

Once an HTTP check against your public URL is running, it is worth deliberately breaking something to confirm the check catches it, rather than trusting it blind until the day it matters. A few 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.

The short version

Coolify's health checks, correctly configured, tell you a container process is alive and answering on localhost. That is real signal and worth keeping. It cannot tell you whether the proxy in front of it is routing correctly, whether DNS still points where it should, or whether the app is reachable from the actual internet at all, because none of that lives inside the container boundary the healthcheck operates in. An HTTP check against the public URL, run from outside the server, closes exactly that gap, and it takes a few minutes to add on top of whatever health check Coolify already runs.

Check your own site the same way

One account covers RealUptime Status, Monitor, Errors and Outages. The free tier includes 10 monitors, checked from every live region, and it stays free.

Start free: 10 monitors, no card

No spam. Unsubscribe anytime with one click.