Guide · Hosting & CDN

Docker Website Down? A Step-by-Step Troubleshooting Checklist

Diagnose a dockerized website that is down: container state, exit codes, published ports, healthchecks, logs, reverse proxy upstreams, OOM kills and full disks.

By SiteDownCheck Editorial · Updated October 5, 2026 · 9 min read

When a site that runs in Docker goes down, the cause usually sits in one of a short list of places: the container isn't running, it is running but not reachable, the app inside is unhealthy, the reverse proxy points at the wrong place, or the host has run out of disk or memory. Working through them in order is faster than guessing. This checklist goes from outside to inside and gives the command for each step. It assumes a Linux host with Docker Engine and, where relevant, Docker Compose v2 (docker compose).

Step 0: Confirm what users see

Record the symptom from outside first. A refusal, a timeout, a 502 and a 404 point to different layers.

curl -sS -o /dev/null -w "%{http_code} %{time_total}s\n" https://example.com/

The HTTP Status Checker and Port Checker give an outside view. Interpret the symptom with connection refused, timed out, reset explained. If a CDN is in front, the Cloudflare Error Checker shows whether the error page is its own.

Step 1: Are the containers running?

docker ps                      # running containers
docker ps -a                   # includes stopped ones, with exit status
docker compose ps              # for Compose projects, run in the project directory

Look at the STATUS column:

  • Up 3 days (healthy): running and passing its healthcheck.
  • Up 5 minutes (unhealthy): running, failing its healthcheck.
  • Restarting (1) 20 seconds ago: a crash loop.
  • Exited (137) 2 hours ago: stopped; the code matters.

Exit codes worth knowing:

CodeUsual meaning
0Process finished normally; a web server should not do this
1Application error on startup or crash
125, 126, 127Docker could not run the command: bad options, not executable, command not found
137Killed with SIGKILL, often by the kernel out-of-memory killer or docker kill
139Segmentation fault
143Terminated with SIGTERM, normally a clean stop

Confirm an out-of-memory kill directly:

docker inspect --format '{{.State.OOMKilled}} exit={{.State.ExitCode}}' CONTAINER

If a container is stopped, restart it, but only after you read the logs (next step), or you lose the reason.

Step 2: Read the logs

docker logs --tail 100 CONTAINER
docker logs -f --since 10m CONTAINER           # follow, recent only
docker compose logs --tail 100 -f web proxy    # Compose, by service name

Look for the first error, not the last. Typical findings: a missing environment variable, a failed database connection, a port already in use inside the container, a permissions error on a mounted volume, or a migration that crashed on startup.

For host-level events such as kills and restarts:

docker events --since 1h --filter type=container
sudo dmesg -T | grep -iE "killed process|out of memory"
sudo journalctl -u docker -n 100 --no-pager

Step 3: Is the port published and the app listening correctly?

A container can run perfectly and still be unreachable. Two separate settings are involved.

Published ports map a host port to a container port. EXPOSE in a Dockerfile only documents a port; it doesn't publish it.

docker ps --format "table {{.Names}}\t{{.Ports}}"
docker port CONTAINER

You want something like 0.0.0.0:8080->80/tcp. If it shows 127.0.0.1:8080->80/tcp, the port is only available locally. This is intentional behind a reverse proxy but a problem if the world is meant to reach it. No -> means nothing is published.

In Compose:

services:
  web:
    image: myapp:latest
    ports:
      - "8080:80"              # host 8080 -> container 80
      # - "127.0.0.1:8080:80"  # local only, for use behind a proxy

The app must listen on all interfaces inside the container. An app bound to 127.0.0.1 inside the container is unreachable from outside the container, even with published ports. Set the bind address to 0.0.0.0 (for example --host 0.0.0.0 in many dev servers, or HOST=0.0.0.0). Verify from inside:

docker exec CONTAINER sh -c "ss -ltn 2>/dev/null || netstat -ltn"
docker exec CONTAINER wget -qO- http://127.0.0.1:80/ | head

Then check from the host:

curl -sI http://127.0.0.1:8080/
sudo ss -ltnp | grep 8080

If the host answers but the internet cannot reach it, the problem is the firewall, security group, or DNS. Test from outside with the Port Checker. Note that Docker publishes ports by writing its own firewall rules, and published ports can bypass host firewall tools like UFW rules. Don't assume a firewall rule blocks a published port; check how your setup behaves.

Step 4: Healthchecks

A healthcheck tells Docker whether the app is working, beyond the process being alive.

docker inspect --format '{{json .State.Health}}' CONTAINER | python3 -m json.tool

The output includes the status and the last few check results with output. Define one in your Dockerfile or Compose file:

services:
  web:
    image: myapp:latest
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/healthz || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped

Points to remember:

  • The check command runs inside the container, so the tool it uses (curl, wget) must exist in the image.
  • start_period gives slow-starting apps time before failures count.
  • Plain Docker marks a container unhealthy, but it doesn't restart it on its own. A restart policy only reacts to the process exiting. Don't assume unhealthy triggers recovery; you need an orchestrator or an external watcher for that.
  • Use depends_on with condition: service_healthy so the app starts after its database is ready:
  web:
    depends_on:
      db:
        condition: service_healthy

Design the endpoint with the advice in load balancer health checks.

Step 5: The reverse proxy

Most Docker sites have a proxy in front (nginx, Caddy, Traefik, or a host nginx). A 502 or 504 usually means the proxy can't reach the app container.

docker compose logs --tail 50 proxy
docker exec PROXY_CONTAINER nginx -t
docker exec PROXY_CONTAINER wget -qO- http://web:3000/healthz

Common causes:

  • Wrong upstream name or port. The proxy must use the container's internal port (web:3000), not the host-published port, when both are on the same Docker network.
  • Different networks. Containers can only reach each other by service name when they share a user-defined network. Check with docker network inspect NETWORK.
  • Stale IP after recreation. nginx resolves hostnames in proxy_pass at startup. If the app container is recreated and gets a new IP, nginx keeps the old one and returns 502 until reloaded. Fix with a reload, or force runtime resolution through Docker's embedded DNS:
resolver 127.0.0.11 valid=10s;
set $upstream http://web:3000;
location / {
    proxy_pass $upstream;
}
  • Proxy started before the app and cached a failure. Add health-based depends_on or reload the proxy.
  • Host nginx proxying to a container. Make sure the container port is published to 127.0.0.1 and the proxy_pass points there.

For the proxy's own error log messages and timeout tuning, read nginx 502 and 504 timeout tuning and fix 502 bad gateway.

Step 6: Disk full

A full disk is one of the most common reasons a Docker host quietly breaks: containers fail to start, databases crash, and logs can't be written.

df -h
df -i                          # inode exhaustion looks the same as a full disk
docker system df               # space used by images, containers, volumes, build cache
sudo du -xh /var/lib/docker --max-depth=1 2>/dev/null | sort -h | tail

The usual culprits:

  • Container logs. Docker's default json-file logging driver does not rotate logs unless you configure it. A chatty container can fill a disk. Find large logs:
sudo sh -c 'ls -lhS /var/lib/docker/containers/*/*-json.log | head'

Set rotation globally in /etc/docker/daemon.json, then restart the Docker daemon (existing containers need to be recreated to pick up the new default):

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Or per service in Compose:

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
  • Unused images and build cache. After confirming nothing needed is affected:
docker image prune -a      # removes images not used by any container
docker builder prune       # removes build cache

Step 7: Memory and CPU

docker stats --no-stream
free -m

A container with too low a memory limit is OOM-killed (exit 137), and a host short on memory kills its largest process, which may be your database. Raise the limit or reduce the app's worker count.

Step 8: Restart policy and recent changes

If the whole host rebooted and the site stayed down, containers were probably not set to come back.

docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' CONTAINER
systemctl is-enabled docker

Use restart: unless-stopped (or always) for services meant to run continuously, and make sure the Docker service is enabled at boot. A restart policy does not fix a crash loop caused by bad config. Also ask what changed: a new image (avoid the latest tag in production), an edited environment file, or a changed volume mount. docker compose config shows the final resolved configuration.

Quick triage table

SymptomFirst commandLikely cause
Connection refused from outsidedocker ps, ss -ltnpContainer down, port unpublished, firewall
502 from proxydocker compose logs proxyWrong upstream, stale IP, app down
Container restartingdocker logs --tail 100Startup crash, missing config
Exit 137docker inspect ... OOMKilledOut of memory
Everything fails, writes errordf -h, df -iDisk or inodes full
Works locally, not remotelydocker port, firewall, DNSBinding to 127.0.0.1, security group
Up but unhealthydocker inspect ... .State.HealthApp-level failure, check tool missing

Prevent the next outage

Add log rotation and a restart policy to every service, alert on disk usage, and put an external check on the public URL. The free Uptime Monitor supports keyword and JSON checks, so a "200 with an error page" is caught. For scheduled jobs inside containers see cron job and heartbeat monitoring, and after an incident use the incident postmortem template.

Next steps

FAQ

Why is my container running but the site unreachable?

Usually the port is not published, the app binds to 127.0.0.1 inside the container instead of 0.0.0.0, or a firewall or proxy is in the way. Check docker port, the app's bind address, and test with curl from the host.

Does Docker restart an unhealthy container automatically?

Not by itself. A restart policy only reacts when the container's main process exits. An unhealthy status is informational unless an orchestrator or external script acts on it.

Is it safe to run docker system prune?

Without --volumes it removes stopped containers, unused networks, dangling images and build cache. Adding --volumes can delete data in unused volumes, so back up and review first.

Why do I get 502 after redeploying one container?

The reverse proxy may have cached the old container IP. Reload the proxy, or configure it to resolve the service name at request time using Docker's embedded DNS at 127.0.0.11.

Get notified when this guide updates

Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.

This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.