Quick answer

What this guide helps you do

Fix a Docker container stuck in a restart loop by checking logs, exit codes, OOM kills, healthchecks, environment variables, volumes and Compose dependencies.

Quick answer

A Docker restart loop means the container’s main process keeps exiting and its restart policy starts it again.

Capture the evidence before recreating anything:

docker ps -a
docker logs --timestamps --tail=200 <container-name>
docker inspect <container-name> --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} restarts={{.RestartCount}}'
docker inspect <container-name> --format '{{json .State.Health}}'

Then check the first error, exit code, memory state, configuration, mounted paths and dependencies.

Do not run docker compose down -v, delete volumes, prune Docker data or repeatedly recreate the entire stack. Those actions can remove useful evidence or persistent data without fixing the cause.


What Restarting actually means

Docker applies a restart policy when the container process exits.

Common policies are:

PolicyBehaviour
noDo not restart automatically
on-failure[:max-retries]Restart after a non-zero exit code, optionally with a retry limit
alwaysRestart whenever it stops, subject to Docker’s manual-stop behaviour
unless-stoppedRestart unless an administrator deliberately stopped it

A restart policy does not repair the application. It only decides what Docker does after the process ends.

Inspect the current policy:

docker inspect <container-name> --format '{{.HostConfig.RestartPolicy.Name}} {{.HostConfig.RestartPolicy.MaximumRetryCount}}'

Do not change the policy to always merely to hide a crash.

Step 1: identify the affected container

docker ps -a --filter status=restarting
docker ps -a --filter status=exited
docker compose ps -a

Record:

Container:
Image and tag:
Compose project:
Current status:
Restart count:
Exit code:
OOM killed:
Health status:
Last known working change:

If every container stopped together, check the Docker service, host storage and mounts before treating each container as an individual fault.

Step 2: read the earliest useful log error

Start with a bounded log view:

docker logs --timestamps --tail=200 <container-name>

For Compose:

docker compose logs --timestamps --tail=200 <service-name>

Look for the first meaningful error before the shutdown messages repeat.

Common patterns include:

  • configuration file missing or invalid
  • permission denied
  • database locked or migration failed
  • port already in use
  • dependency connection refused
  • hostname cannot be resolved
  • required environment variable missing
  • file or directory not found
  • unsupported command-line argument
  • out-of-memory termination

Follow the logs only after capturing the existing output:

docker logs --timestamps --follow <container-name>

Press Ctrl+C to stop following; this does not stop the container.

Step 3: inspect the exit state

docker inspect <container-name> --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} restarts={{.RestartCount}}'

Useful exit-code clues include:

Exit codeCommon meaningWhat to check
0Process completed normallyWrong command for a long-running service or intentional one-shot task
1General application errorApplication logs and configuration
126Command found but cannot executePermissions, architecture or mount flags
127Command not foundImage entrypoint, command and paths
137Process received SIGKILLCheck OOMKilled, host memory and manual/daemon termination
143Process received SIGTERMNormal stop, dependency shutdown or external restart

Exit code 137 does not prove an out-of-memory event. Confirm:

docker inspect <container-name> --format '{{.State.OOMKilled}}'
free -h
sudo journalctl -k --since '-30 minutes' --no-pager | grep -i -E 'oom|out of memory|killed process'

Step 4: distinguish unhealthy from restarting

A healthcheck runs a command inside the container and records starting, healthy or unhealthy.

Inspect it:

docker inspect <container-name> --format '{{json .State.Health}}'
docker inspect <container-name> --format '{{json .Config.Healthcheck}}'

An unhealthy result does not normally make the Docker Engine restart an otherwise running standalone container. Restart policies respond to the main process exiting. Other tooling may react to health status, and Compose can wait for a dependency marked service_healthy.

Check whether the healthcheck itself is valid:

  • does the image contain curl, wget or the command being called?
  • is the URL correct inside the container?
  • is the startup period long enough?
  • does the application require authentication?
  • is the dependency reachable on the correct network?

Do not disable a useful healthcheck simply to turn the status green.

Step 5: validate the Compose configuration

From the correct project directory:

docker compose config
docker compose config --images
docker compose ps -a

The rendered configuration reveals substituted variables and merged files.

Protect the output if secrets appear. Check:

  • image and tag
  • command and entrypoint overrides
  • environment-variable names
  • bind-mount source paths
  • named volumes
  • published ports
  • networks and aliases
  • user, group and device mappings
  • restart policy
  • healthcheck
  • dependency conditions

Compare the current Compose file with the last known working version.

Step 6: check environment variables and secrets

An empty or renamed variable can make an application exit immediately.

List the environment keys without publishing secrets:

docker inspect <container-name> --format '{{range .Config.Env}}{{println .}}{{end}}'

Run that only in a private terminal because values may be sensitive.

Check that:

  • the expected .env file is in the Compose project directory
  • variable names match the image documentation
  • passwords containing special characters were quoted correctly
  • secret files exist and are readable
  • an update did not rename or remove an option

Do not paste complete environment output into a public support post.

Step 7: verify volumes and permissions

Inspect mounts:

docker inspect <container-name> --format '{{range .Mounts}}{{println .Type .Source "->" .Destination .Mode}}{{end}}'

For every bind mount:

findmnt -T /host/path
namei -l /host/path
ls -ld /host/path

Confirm:

  • the host source exists
  • the underlying disk or network share is mounted
  • Docker did not start against an empty mount-point directory
  • the container user can read or write as required
  • the mount is not unexpectedly read-only
  • free disk space and inodes remain available
df -hT
df -ih

Use Docker Container Cannot Access a Mounted Folder for the permission path. Do not use chmod 777 as a general repair.

Step 8: check ports and dependencies

For a port error:

docker port <container-name>
sudo ss -lntup

For dependencies:

docker network ls
docker inspect <container-name> --format '{{json .NetworkSettings.Networks}}'
docker compose logs --tail=100 <dependency-service>

Starting first is not the same as being ready. A database may need time to become healthy before the application connects.

Where the image provides a meaningful healthcheck, Compose can wait for:

depends_on:
  database:
    condition: service_healthy

Use the service names and healthcheck supported by the actual stack. Do not paste this fragment without adapting it to a complete valid Compose file.

Step 9: check the image and architecture

docker inspect <container-name> --format '{{.Config.Image}}'
docker image inspect <image-name> --format '{{.Id}} {{.Architecture}} {{.Os}}'
uname -m

If the problem started after pulling an image:

  • read that image’s release notes
  • confirm the tag did not move to a breaking release
  • verify the CPU architecture
  • check required migrations and variables
  • retain the previous image ID or digest
  • change only the affected service during testing

Do not update every container while diagnosing one failure.

Step 10: recreate only after finding the cause

Once configuration and data are protected:

docker compose up -d --no-deps --force-recreate <service-name>
docker compose ps <service-name>
docker compose logs --timestamps --tail=100 <service-name>

Recreating is appropriate when the container definition changed or its replaceable writable layer is damaged. It does not fix a broken database, missing mount or invalid environment value.

Never add -v to docker compose down during routine troubleshooting unless deleting named volumes is explicitly intended and independently recoverable.

Safe diagnostic order

identify restarting container
→ capture logs and inspect state
→ interpret exit code and OOM status
→ inspect healthcheck
→ render Compose configuration
→ verify variables, mounts and free space
→ test ports and dependencies
→ check image changes
→ recreate only the affected service

Verification checklist

  • Container remains running for longer than the previous loop.
  • Restart count stops increasing.
  • Health status becomes healthy where configured.
  • Logs contain no repeating startup error.
  • Persistent data is present.
  • Required mounts are active.
  • Dependencies are reachable.
  • Application works through its intended local address.
  • Service survives a controlled Docker restart.
  • Recovery notes and configuration are updated.

Official Docker references


Recap

A restart policy is the mechanism repeating the failure, not usually the cause. Capture logs, exit state, OOM status and healthcheck output before recreating anything. Then verify Compose configuration, environment values, mounted data, dependencies and the image change that preceded the loop.