Detecting and Systematically Debugging Container Restart Loops
AI generated
FROM
RUN
Docker · Debugging · Operations
Debugging Container Restart Loops Systematically
When a container keeps killing itself over and over

A container that starts, runs briefly, and then crashes again in an endless loop is one of the most frustrating everyday problems in Docker operations, since the logs often disappear faster than you can read them. A systematic approach narrows down the cause within minutes in most cases, instead of guessing wildly at configuration values.

17 min read Restart policy Logs before the crash Healthcheck pitfalls

1. What a Restart Loop Actually Means

A restart loop happens when a container runs with a restart policy such as always, on-failure, or unless-stopped, but the main process exits with an error immediately after starting. Docker enforces that restart policy strictly and starts the container again according to the configuration, which lets it crash again with the same error, repeating the cycle indefinitely.

In this state, the container status typically flips constantly between Restarting and briefly Up, which is easy to observe with docker ps. Unlike Kubernetes, plain Docker has no explicit CrashLoopBackOff status with exponential backoff, but the Docker Engine internally increases the wait time between restart attempts over time to limit system load.


# Spot a restart loop from the status column
docker ps -a --filter "name=webshop_app"
# STATUS: Restarting (1) 3 seconds ago

# Show the number of restarts so far
docker inspect webshop_app --format '{{.RestartCount}}'

2. Step 1: Capture Logs Before the Last Crash

The single most important first step is to capture the container's log output before another restart overwrites the relevant error messages in the terminal. The plain docker logs command only shows the current run, while combining the --since option with a redirect into a file lets you permanently save the relevant excerpt instead of losing it in a scrolling terminal.

Especially valuable is the fact that Docker keeps the logs of a container's previous run available under the same container ID, as long as the container has not been fully removed, effectively giving you previous-run behavior without any extra flag. A simple redirect into a file also prevents output from getting lost in the terminal buffer during a very fast crash cycle.


# Save all logs so far with timestamps
docker logs -t webshop_app > /tmp/webshop_app_crash.log 2>&1

# Only the last 200 lines before the current restart
docker logs -t --tail 200 webshop_app

# Capture live while the next crash happens
docker logs -f webshop_app | tee /tmp/webshop_live.log

3. Step 2: Temporarily Disable the Restart Policy

As long as the restart policy stays active, Docker automatically restarts the container after every crash, which makes targeted debugging harder, since the container may already be restarting again while you are still analyzing the previous failure. The most reliable next step is therefore to temporarily set the restart policy to no, so the container stays in a stopped state after a crash instead of immediately restarting.

With the container stopped, you can then read the last exit code, which often already provides a decisive clue: exit code 1 usually points to a general application error, exit code 137 to a kernel-enforced SIGKILL, frequently triggered by an out-of-memory event, and exit code 143 to a regular SIGTERM that the application failed to handle cleanly within its grace period.


# Temporarily disable the restart policy
docker update --restart=no webshop_app

# Check the exit code of the last run
docker inspect webshop_app --format '{{.State.ExitCode}}'

# After analysis: restore the policy
docker update --restart=unless-stopped webshop_app

4. Common Cause 1: Missing or Wrong Environment Variable

One of the most common causes of restart loops is a missing, empty, or misnamed environment variable that the application strictly depends on at startup, such as database credentials or an API secret. Many application frameworks deliberately abort startup with a clear error when a required variable is missing, which shows up as a fast, repeated crash sequence, especially once the actual error message gets buried in a flooded log.

Comparing the environment variables actually set inside the container against what the application expects, for instance via a .env.example file, usually uncovers such problems within seconds. Particularly tricky are silent typos in a variable name, which the application does not recognize as missing but simply ignores, falling back to a default value that only causes an error later on.


# Check the environment variables actually set in the container
docker exec webshop_app env | sort

# Compare against the expected configuration
docker exec webshop_app env | sort > /tmp/actual.env
sort .env.example > /tmp/expected.env
diff /tmp/expected.env /tmp/actual.env

5. Common Cause 2: Database Not Ready Yet

Another classic pattern occurs in multi-container setups where an application tries to connect to a database immediately on startup, while the database itself is still initializing. The database container already reports Up, but does not yet accept connections, since internal initialization steps such as creating system tables are still running, which can take several seconds for MySQL and PostgreSQL on the very first start.

The application then receives a connection error and exits, after which the restart policy starts it again, often faster than the database finishes initializing, leading to several failed attempts in a row. The reliable fix is a healthcheck on the database container combined with depends_on and the service_healthy condition in docker-compose, instead of relying on plain process uptime.


# docker-compose.yml: correct dependency on a healthcheck
services:
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10
  app:
    depends_on:
      db:
        condition: service_healthy

6. Common Cause 3: An Overly Aggressive Healthcheck

Paradoxically, an overly strict healthcheck can itself trigger a restart loop, when combined with an external orchestrator or restart script configured to actively restart a container on a failed health status. A too-short interval combined with an application that needs slightly longer to start than expected, for instance due to a cache warmup, causes the container to be marked unhealthy while it is still in a normal startup phase.

The start_period healthcheck option is exactly the right tool for this: it defines a grace period during which failed healthchecks are not yet counted as failures, giving the application time for its actual initialization. If this option is forgotten or set too tight, an otherwise working container is falsely flagged as persistently unhealthy and gets unnecessarily restarted by higher-level systems.


# Healthcheck with a sufficient grace period
docker run -d \
  --health-cmd="curl -f http://localhost/health || exit 1" \
  --health-interval=10s \
  --health-timeout=3s \
  --health-retries=3 \
  --health-start-period=45s \
  webshop_app

7. Memory Problems: When the Kernel Kills the Container Itself

If a container was started with a fixed memory limit and the application exceeds it, the Linux kernel's out-of-memory killer steps in and forcibly terminates the main process with SIGKILL. This case reliably shows up as exit code 137, along with a kernel warning that appears outside the container in the host system log, while the container itself often cannot emit a meaningful error message anymore, since the process is terminated without warning.

To confirm this cause, it is worth checking the OOMKilled field in the Docker inspect output, which explicitly indicates whether the last crash was triggered by the out-of-memory killer. The fix lies either in raising the memory limit, or, more often the right call, in analyzing and fixing an actual memory leak in the application itself.


# Check whether the last crash was caused by an OOM kill
docker inspect webshop_app --format '{{.State.OOMKilled}}'

# Compare the current memory limit against actual usage
docker inspect webshop_app --format '{{.HostConfig.Memory}}'
docker stats webshop_app --no-stream

8. Preventing Restart Loops Instead of Debugging Them

Most of the causes described here can already be defused before the first crash, by writing applications so that a missing required configuration produces a clear, immediately recognizable error message instead of failing cryptically. An equally useful pattern is a built-in retry mechanism with a bounded number of attempts for external dependencies such as database connections, instead of terminating the entire process on the very first connection failure.

In Compose setups, it is also worth deliberately avoiding an overly aggressive restart policy during active development, for instance by temporarily using on-failure:3 instead of always, so a broken container stops after a few attempts and leaves room for analysis, instead of running indefinitely and flooding the logs with identical error messages.


# Bounded restart policy during development
docker run -d --restart=on-failure:3 webshop_app

# Equivalent in docker-compose.yml
# restart: on-failure:3

9. The Systematic Debugging Flow at a Glance

Instead of checking causes in random order for every restart loop, a fixed sequence pays off: first capture the logs, then disable the restart policy, then read the exit code, then narrow down between environment variables, dependencies, healthcheck configuration, and memory limits based on that exit code. This flow can be completed entirely within a few minutes and covers the vast majority of cases seen in practice.

The table below maps the most common exit codes and symptoms to their most likely causes, to steer troubleshooting from the very first observation instead of starting from zero for every new incident.

Symptom / Exit Code Most Likely Cause First Check Typical Fix
Exit code 1 General application error, e.g. missing environment variable docker logs -t --tail 200 Fix configuration or environment variables
Exit code 137 Out-of-memory kill by the kernel docker inspect --format '{{.State.OOMKilled}}' Raise the memory limit or fix a memory leak
Exit code 143 Regular SIGTERM, no clean shutdown Review the application's shutdown handling Implement graceful shutdown logic
Immediate crash after connecting to the database Database not ready yet docker logs of the database container Healthcheck plus depends_on: service_healthy
Container marked unhealthy despite the app running Healthcheck too aggressive or too early docker inspect --format '{{.State.Health}}' Increase start_period in the healthcheck

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

Debugging Restart Loops: The Essentials at a Glance

First step

Always capture logs immediately, before the next automatic restart overwrites them.

Restart policy

Temporarily set to 'no' so the container can be analyzed in a stopped state.

Exit codes

137 points to an OOM kill, 1 usually to an application error like a missing environment variable.

Most common cause

Missing environment variables and applications starting before their dependencies are ready.

11. FAQ: Debugging Restart Loops: The Essentials at a Glance

1What is a restart loop in Docker containers?
A state where a container with an active restart policy crashes immediately after starting, causing Docker to restart it automatically, which triggers the same error again and repeats the cycle indefinitely.
2How do I spot a restart loop with docker ps?
The status column repeatedly shows 'Restarting' followed by a brief 'Up'. The number of restarts so far can also be read with docker inspect --format '{{.RestartCount}}'.
3Why should I capture the logs first before debugging further?
Because another automatic restart can overwrite the error messages from the previous run visible in the terminal. Redirecting docker logs -t into a file permanently saves the relevant excerpt.
4How do I temporarily disable the restart policy?
With docker update --restart=no . The container stays stopped after the next crash, allowing a focused analysis of the last exit code and the logs.
5What does exit code 137 mean for a crashed container?
Exit code 137 almost always indicates a kernel-enforced SIGKILL, usually triggered by the out-of-memory killer when the container exceeded its configured memory limit.
6How do I find out whether a crash was caused by an out-of-memory event?
With docker inspect --format '{{.State.OOMKilled}}'. A return value of true confirms that the kernel's out-of-memory killer terminated the container.
7Why does my application crash even though the database shows as running in docker ps?
The Up status only means the database process has started, not that it already accepts connections. Internal initialization steps can still take several seconds after that.
8How do I prevent an application from starting before a ready database?
With a healthcheck on the database container combined with depends_on and the service_healthy condition in docker-compose, instead of relying on plain process uptime alone.
9Can a healthcheck itself trigger a restart loop?
Yes, if interval and start_period are set too tight and the application needs longer to initialize, it gets falsely marked unhealthy and unnecessarily restarted by higher-level systems.
10What is the purpose of the start_period healthcheck option?
It defines a grace period during which failed healthchecks are not yet counted as failures, giving the application enough time for its regular startup phase.