You deploy an application, Docker shows it as “Up”, and the dashboard still says “unhealthy”. Or the reverse happens: the container is healthy, but visitors get an error. Neither status tells the whole story.
A useful check answers a specific question about the service. A poor one produces noise, conceals a failure or adds load to an already struggling server. Here is how to identify which case you have and build a check you can trust.
Running, healthy and reachable are different things
Running
The container's main process is alive. It may still be starting, waiting for a dependency or unable to serve useful requests.
Healthy
The configured probe is passing. Its value depends on what it tests; a successful local request is not an end-to-end user test.
Reachable
A client can use the service through its real access path. DNS, TLS, proxy rules and network filtering all matter here.
Docker health states include starting, healthy and unhealthy. A successful probe returns exit code 0; failure uses 1, while 2 is reserved. Consecutive failures drive the unhealthy state. The Docker HEALTHCHECK reference describes the state model and timing.
1. Inspect the actual health-check failure
Work from your normal Compose directory, using the same project name and configuration files as your deployment. Replace app with your service name. These first commands are read-only:
docker compose ps
docker compose ps -q app
docker compose logs --since=15m --tail=100 app
Copy the container ID from the second command. If there are several replicas, inspect the affected one rather than assuming all are failing for the same reason.
docker inspect --format '{{json .Config.Healthcheck}}' CONTAINER_ID
docker inspect --format '{{json .State.Health}}' CONTAINER_ID
The first result shows the effective probe configuration; the second includes recent results and diagnostic output when a health check exists. If no health check is configured, there is no meaningful health history to inspect. The image may supply a default check, so inspect the running container rather than only reading your Compose file. Docker's inspect reference covers formatted output.
Do not publish the full inspection output without reviewing it. It can expose environment settings, credentials or internal hostnames. Share only the relevant, redacted failure.
Command not found
The probe expects a tool that is not in the image. Check the image contents or use the application's supported health command.
Connection refused
Check the internal port, listening address and startup logs. A host-side published port is not necessarily the port used inside the container.
Timeout or HTTP error
Check the endpoint and authentication requirements, then compare response time with resource pressure and dependency errors.
2. Try a small, working Compose health-check example
The following is an isolated demonstration, not a production application. It uses Python's built-in HTTP server and a probe implemented with the same Python runtime, avoiding an assumption that curl is installed. Put it in a new, empty test directory as compose.yaml. Ensure host port 8088 is unused.
services:
health-demo:
image: python:3.13-slim
working_dir: /tmp
command: ["python", "-m", "http.server", "8000"]
ports:
- "127.0.0.1:8088:8000"
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/', timeout=2)"
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
The server binds to a loopback-only host port. It is not exposed on every public interface. The probe runs inside the container and checks port 8000; a host-side client uses port 8088. Keep this distinction when adapting the pattern to your own service.
docker compose config --quiet
docker compose up -d --wait --wait-timeout 90
docker compose ps
curl --fail http://127.0.0.1:8088/
Run the final command on the VPS, not on your laptop. It requires curl on the host; the container probe does not. If your Compose version lacks --wait, start with up -d and inspect the status afterwards. Docker documents these startup options in the Compose up reference.
For a controlled failure test, change only the probe URL to port 8001, which the demo does not use, then run docker compose up -d again. After the grace period and enough failed checks, inspect the health output. Restore port 8000 and reapply the configuration to see it recover. Do this only in the disposable demo, not against a live service.
3. Choose sensible timing and a meaningful endpoint
In the example, interval controls the check cadence, timeout bounds each attempt and retries sets the consecutive failure threshold. start_period gives startup failures a grace period; it does not simply postpone all probes. A successful check during that period can establish the service as started, after which failures count. See the health-check timing rules.
Those values are a starting point for the demo, not a universal recommendation. A service doing a legitimate first-run migration needs a different startup budget from a lightweight API. Measure normal startup and probe response times, then allow a margin rather than extending the timeout until every failure disappears.
- Keep the check cheap: do not generate a report, crawl a media library or perform a write on every probe.
- Choose the right success condition: a static landing page may work while the API behind it is failing. Prefer a documented readiness endpoint when one exists.
- Keep diagnostic output small: return enough context to investigate without printing tokens, passwords or complete database responses.
- Be intentional about dependencies: checking a third-party API on every attempt may turn its outage or rate limit into a local failure signal.
4. Use depends_on for startup readiness
If an application starts before its database is ready, a fixed sleep is fragile. In an existing Compose stack with services named app and db, the relevant fragment is:
services:
app:
# Keep the application's existing image and settings.
depends_on:
db:
condition: service_healthy
db:
# Keep the database's existing image, storage and settings.
# Define a vendor-appropriate healthcheck here.
This is a fragment, not a complete runnable deployment. The database must have a real health check. Compose waits for the dependency to pass before starting the dependent service. The shorter depends_on: [db] controls ordering without establishing readiness. See Docker's startup-order guide.
That startup gate is not continuous supervision. Your application still needs connection retry and recovery logic when the database becomes unavailable later. In a separate container, localhost refers to that container itself; reach the database through its service name and internal port on the shared Compose network.
Be clear about what a database probe proves. A server accepting connections does not necessarily mean the application user has permission, the expected schema exists or a migration completed. Check those assumptions in your deployment validation.
Why restart: unless-stopped does not fix unhealthy status
In standalone Docker Engine, a restart policy responds to container exits, not a failed health probe while the main process remains alive. restart: unless-stopped is useful for restarting a process after it exits and for daemon restarts, while respecting a deliberate stop. It does not turn an unhealthy container into an automatically repaired service. Docker's restart-policy documentation describes that scope.
Do not confuse this with restart: true nested under a Compose dependency. That setting relates to explicit Compose-managed dependency operations; it is not a general unhealthy-container recovery rule. Nor should a standalone Compose example be taken as a description of Swarm or Kubernetes behaviour.
For the common next steps, see our guides to OOM kills and Docker exit 137, full Linux disks and safe Compose updates and rollback planning.
Check the public service as well as the container
A local probe deliberately avoids much of the network path. That makes it useful for isolating application behaviour, but it cannot prove that your domain resolves correctly, the TLS certificate is valid or the reverse proxy selects the right backend.
Use an external monitor against the real HTTPS URL, with an expected status or response where possible. Put it somewhere that can still alert you when the application VPS is unavailable. Our Uptime Kuma guide covers a self-hosted monitoring option. A monitor sharing the same failed server cannot provide an independent view of that outage.
If internal health is green but public requests fail, investigate the proxy and network path before changing the probe. The 502 troubleshooting guide and Docker port and firewall guide cover those checks.
Use the results to size and operate your VPS
On a HYEHOST Cloud VPS, you control the container configuration, probes and maintenance schedule. Choose Wolverhampton, Ashburn or Auckland for the audience and network requirements of your application. Reserve capacity for startup, updates and background jobs as well as normal requests.
Slow checks during a CPU-heavy job can be a useful clue, but a failed probe alone is not evidence that a larger plan will fix the application. Compare logs, RAM, CPU and disk behaviour first. If sustained demand warrants dedicated resources, compare VDS hosting; keep backups and external monitoring in the plan whichever server you choose.
Docker health-check FAQ
Does Docker restart an unhealthy container automatically?
Not with standalone Docker Engine and a normal restart policy. A health check updates health status; restart policies respond to container exits. Alert on persistent failures and investigate the cause.
Why is my container running but unhealthy?
Its main process is running, but its configured health command has failed enough times. The application may be broken, or the probe may use a missing tool, wrong port, wrong path or unsuitable timeout.
Does depends_on wait for a database to be ready?
The short form does not wait for readiness. A dependency with condition: service_healthy waits for its configured health check during Compose startup. Your application still needs to handle later connection failures.
Should I use localhost in a Docker health check?
Use localhost when checking a service inside that same container. To reach a separate Compose service, use its service name and internal port on the shared network.
Is a Docker health check enough for uptime monitoring?
No. An internal check cannot prove that public DNS, HTTPS, firewall rules and reverse-proxy routing work. Monitor the public service from outside the VPS as well.

