You deployed to a VPS and docker ps keeps flipping between Restarting (1) and a few seconds of Up. The restart policy keeps bringing the process back, but it never fixes whatever kills it. Before you touch config, add memory or blame the database, find out why the process stops. Everything below is read-only; run it only on servers you're authorized to access, and replace YOUR_CONTAINER with the real name or ID. Docker access is root-equivalent, so don't expose the API or hand it to unknown helpers while debugging.
If the container exited once and stayed down, or you just want to decode a specific number, start with Docker exit codes: read the number before you restart anything. This post is about loops.
1. Check it's really a loop and write down the state
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} finished={{.State.FinishedAt}} restarts={{.RestartCount}}' YOUR_CONTAINER
docker events --since 30m --until "$(date +%s)" --filter container=YOUR_CONTAINER --filter event=die --filter event=start
Make sure it's the image and service you meant to deploy, and note the incident time and the last deploy. A restarts count that grows between runs, or die/start pairs repeating in the events, means a loop. FinishedAt is UTC.
The number in Restarting (N) is the last exit code. It points at the next check rather than giving the answer:
| Code | Usually means |
|---|---|
| 0 | Command finished; a service may have daemonized into the background |
| 1 | Application error, read the logs |
| 125 / 126 / 127 | Docker couldn't run it / not executable / not found |
| 137 | SIGKILL, not proof of OOM |
| 139 / 143 | Segfault / stopped with SIGTERM |
2. Restart policy and Docker's backoff
docker inspect --format 'policy={{.HostConfig.RestartPolicy.Name}} max_retries={{.HostConfig.RestartPolicy.MaximumRetryCount}}' YOUR_CONTAINER
| Policy | Behaviour |
|---|---|
no (default) |
Never restarts |
on-failure[:N] |
Restarts only on non-zero exit, optionally at most N times, then stays Exited
|
always |
Restarts whenever it stops; after a manual stop, only when the daemon or container is restarted |
unless-stopped |
Like always, but a container you stopped stays stopped across daemon restarts |
Docker backs off between attempts: 100 ms, doubling each time, capped at one minute. If the container stays up for at least 10 seconds, the delay resets. That's why a crashing container spends longer and longer in Restarting and only flashes Up. In Compose this is the service's restart: key.
Changing the policy doesn't fix a broken startup; it only changes how often you see it fail. Also, don't let both Docker and systemd restart the same container.
3. Read a bounded window of logs
docker logs --since 15m --tail 200 --timestamps YOUR_CONTAINER
Look for the first error before each stop, not the last line. In a loop it repeats. Empty output doesn't mean healthy: the app may log to files, or the logging driver may not expose stdout.
-
Connection refused to the database: go to step 4. Inside a container,
localhostis not the other service. -
Permission denied / missing file: check mount paths, ownership, the runtime user and startup args. Don't
chmod 777your way out. - Config or migration failure: diff recent changes and work out rollback and data compatibility before editing production config.
4. Dependencies that aren't ready yet
Classic loop: the app starts before its database, cache or API is ready, fails its first connection, exits, and gets restarted until the dependency happens to be up.
docker compose ps
docker compose logs --since 15m --timestamps YOUR_DEPENDENCY
- Plain
depends_ononly waits for the dependency's container to start, not to be ready.condition: service_healthywaits for its healthcheck, so it only helps if that service has one. - Check that the app's address matches the dependency's service name and port on the same network.
- If the app gives up after one failed connection, a short retry in the app usually beats relying on restarts.
5. Healthchecks: what they do and don't restart
docker inspect --format '{{json .State.Health}}' YOUR_CONTAINER
On plain Docker Engine, unhealthy is only a label. Docker does not restart a container because its healthcheck fails. If unhealthy containers keep getting restarted, something else is doing it (Swarm, an orchestrator, an autoheal-style helper), and that tool's logs are where to look.
A check that's too strict, or has no start period for a slow app, can keep dependents waiting on service_healthy or keep triggering that external tool. Read the check's own output before blaming the app.
6. Memory, disk and ports
docker inspect --format 'memory_limit_bytes={{.HostConfig.Memory}}' YOUR_CONTAINER
docker stats --no-stream YOUR_CONTAINER
df -h
-
Memory:
OOMKilled=trueis evidence Docker recorded an OOM kill; a 137 alone isn't.docker statsis a snapshot and won't show the earlier peak. - Disk: a full filesystem breaks databases and apps at startup. Find what filled it before deleting anything.
-
Ports: "port is already allocated" means something else holds it;
sudo ss -ltnpshows what.
7. Turn evidence into a next step
Write down the symptom, the evidence, the likely cause and the smallest next check. For production, define impact, a maintenance window and rollback first. Deleting data or containers and widening permissions are not universal fixes.
Illustrative example
Illustrative, not a real incident: after a host reboot the app shows Restarting (1) and its restart count keeps climbing while the database is Up. The app's first log line after every start is a refused connection to the database, and the database log shows it was still initializing at those times. Compose uses depends_on without a health condition. That supports "dependency not ready". The fixes, a database healthcheck plus condition: service_healthy, or a connection retry in the app, are changes to plan, followed by checking that the restart count stops growing.
A restart policy hides the failure; the exit code, events and the first log error tell you what it is.
I work on OpsMate, which puts an SSH terminal and an AI assistant on one page, so you can run the commands above yourself or ask the AI to compare the logs, restart count, policy, health and dependency status for you. Low-risk actions such as service restarts and log rotation run automatically by default, dangerous commands are blocked, and every step is logged. Each time you ask the AI for help, recent terminal output is sent to the cloud AI along with your message, so remove sensitive information first. You get 500 free AI requests per month. Read the full guide and start free on itops.sh.
Drafted with AI assistance and reviewed before publishing.
Top comments (1)
"Read the first error before you change anything" is the rule I break most often, and it's always the expensive one.
One refinement from a different domain: the first error in the log isn't always the first cause. Last week I hunted a page missing from my sitemap for two hours by reading the first symptom - the page isn't there - instead of the earliest state change: the file only regenerates on a manual save. The line I was reading sat downstream of the thing that broke.
Same trap inverted, with aggregates. My Search Console shows a 7.53% average sitting on a 16-17% peak and a 2-4% recent. The first number you read is almost never the one that explains the incident.
That's why the order of your list matters - it walks you back to the earliest observable state (exit code, OOM flag, restart count) instead of the loudest log line. The
oom={{.State.OOMKilled}}field in that inspect command is the one people skip and the one that ends the search.