Docker Compose Healthchecks, Restart Policies, and Resource Limits

Teach a Compose stack to describe its own health, restart more sensibly, and stop one noisy container from eating the whole VPS.

Docker ComposeReliabilityOperations basics
What you learn

How to add health checks, choose restart behavior, and apply CPU or memory limits that fit small self-hosted systems.

Good for

VPS operators running web apps, workers, databases, and supporting services in Docker Compose.

Risk to watch

A container that is merely “running” can still be unusable, and an unlimited container can still starve the rest of the machine.

Before you begin

  • A working Compose stack you can edit and restart.
  • Basic comfort reading app logs.
  • A rough idea of which services are web apps, workers, databases, or caches.

Compose defaults are convenient, but they do not tell you whether an app is actually ready, whether it should restart after failure, or whether it is safe for one container to consume all available memory. Those decisions belong to you.

Step 1: Add healthchecks that mean something

A healthcheck should test the thing that proves the service is usable. For a web app, that is often an HTTP endpoint. For a database, that may be a client ping command. Start with a simple example:

services:
  app:
    image: nginx:alpine
    ports:
      - "8080:80"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

For Postgres, a better check looks like this:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: change-me
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

Use double dollar signs in Compose when you need a variable to reach the container shell intact. That detail trips up beginners a lot.

Expected outcome: docker compose ps shows healthy or unhealthy states that reflect reality better than “container exists.”

Step 2: Choose restart policies carefully

Restart policies answer a narrow question: what should Docker do when the container exits? The common choices are:

  • no, do not restart automatically.
  • on-failure, restart only after a non-zero exit.
  • unless-stopped, keep it running across crashes and daemon restarts unless you intentionally stopped it.
  • always, restart whenever possible, even if it was manually stopped before a daemon restart.

For most self-hosted apps, unless-stopped is a sensible default:

services:
  app:
    image: yourorg/app:1.2.3
    restart: unless-stopped

Use on-failure when you want crash recovery but do not want a deliberately stopped one-shot job to spring back to life.

Warning: Restart policies can hide repeated crash loops if you never look at logs. A container that restarts forever is not healthy. It is just noisy failure on repeat.

Step 3: Set memory and CPU limits with intent

Small VPS hosts benefit a lot from basic guardrails. One misbehaving app should not consume every byte of RAM and trigger system-wide instability. On modern Docker Compose, a common pattern is:

services:
  app:
    image: yourorg/app:1.2.3
    mem_limit: 512m
    cpus: 1.0

For a lightweight Redis cache, you might choose something smaller:

services:
  redis:
    image: redis:7-alpine
    mem_limit: 256m
    cpus: 0.50

These numbers are not magic. Start with realistic limits based on the VPS size and the service role. Then observe usage with:

docker stats

If the app dies because the limit is too low, raise it intentionally. If it never approaches the limit, that is fine. Guardrails are supposed to leave headroom.

Step 4: Verify the behavior after deployment

Render and apply the config:

docker compose config
docker compose up -d

Then inspect the result:

docker compose ps
docker inspect --format='{{json .State.Health}}' myapp-app-1
docker stats --no-stream

Recovery notes:

  • If a bad healthcheck marks a good service as unhealthy, remove or simplify the check, then redeploy.
  • If a memory limit is too aggressive, increase it before the service corrupts work or repeatedly gets killed.
  • If a restart policy keeps reviving a broken service, temporarily stop it, fix the config, then bring it back intentionally.

Troubleshooting common mistakes

The healthcheck command is not found.
Your image may not include curl or wget. Use a tool already present in the image, or choose an app-specific check.

The service is healthy, but dependent apps still fail at startup.
Healthchecks help visibility, but not every app waits for dependencies automatically. Add retry logic or app startup delay if needed.

The container keeps restarting too fast to inspect.
Check docker compose logs --tail=200 and temporarily set restart: "no" during debugging if that helps.

The app was killed unexpectedly.
Look for OOM behavior in docker inspect, docker events, or host logs. The memory limit may be too small.

What to do next

Once your services describe their own health, the next useful step is monitoring backup and scheduled-job success outside the app itself. Continue with How to Set Up Healthchecks for Backups and Cron Jobs.