Install
Markdown
Ask AI
Share

Docker Container Restarting in a Loop: 5 Causes, Fixed

Five real reasons a Docker container keeps restarting, each reproduced with its exit code, the command that finds it and the fix. Then the same crash on Levelrail.

Markdown
Ask AI
Share

A container stuck on Restarting (1) 5 seconds ago is the most common way a deploy fails on a single server. The status tells you almost nothing, but two numbers do: the exit code and whether the kernel killed the process.

In this guide we reproduce five distinct causes with plain Docker, show the exact command that finds each one, and fix it. Then we ship the same kind of crash to a Levelrail app and look at what the user sees. Every command and output below comes from a real run, and the rough edges we hit have their own section.

What you need#

  • Docker. Our run used Docker 28.3.0 on an arm64 Mac.
  • A tiny Node 22 image. One image, lrcrash/app:v1, is a 25 line server. It exits with a log line if DATABASE_URL is unset, writes a file to /data at startup, and listens on PORT.
  • For the last part, a running Levelrail instance. If you do not have one, follow the five minute install.

Every container below runs with --restart unless-stopped, so a crash starts it again.

Step 1: Read the three numbers first#

Whatever the cause, start with the same three commands:

bash
docker ps --filter name=lrc-env --format 'table {{.Names}}\t{{.Status}}'
docker logs --tail 4 lrc-env
docker inspect lrc-env --format 'RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Status={{.State.Status}}'

Here is cause 1 (a missing environment variable), 6 seconds after docker run -d --name lrc-env --restart unless-stopped lrcrash/app:v1:

output
NAMES     STATUS
lrc-env   Restarting (1) 2 seconds ago
fatal: DATABASE_URL is required
fatal: DATABASE_URL is required
fatal: DATABASE_URL is required
fatal: DATABASE_URL is required
RestartCount=8 ExitCode=1 OOMKilled=false Status=restarting

The number in Restarting (1) is the exit code of the last run. docker logs keeps the output of every run, so the same line repeats once per restart.

How fast does it loop?#

We recorded docker events while this container ran. These are the start times, in seconds after the first start:

output
+0s +0s +0s +1s +2s +3s +7s +13s +26s +52s +103s

The gaps roughly double. The Docker documentation describes it this way: “An increasing delay (double the previous delay, starting at 100 milliseconds) is added before each restart to prevent flooding the server” and a “maximum delay of 1 minute” (docker run reference). We did not run long enough to observe the cap, so we only confirm the doubling.

Cause 1: A required environment variable is missing#

Symptom: Restarting (1), a one line error in the logs, and the container dies within a few hundred milliseconds.

Diagnose: the three commands above. Exit code 1 plus a message means the app chose to stop, and the message names the variable.

Fix: a restart policy cannot add variables to an existing container, so recreate it:

bash
docker rm -f lrc-env
docker run -d --name lrc-env --restart unless-stopped -e DATABASE_URL=postgres://demo@db.example.com/shop -e DATA_DIR=/tmp lrcrash/app:v1
docker ps --filter name=lrc-env --format 'table {{.Names}}\t{{.Status}}'
output
NAMES     STATUS
lrc-env   Up 12 seconds

Cause 2: The command or entrypoint cannot run#

This one has several exit codes, and they tell you which kind of “cannot run” you have. We reproduced all of these:

What was wrongExit codeLog lineRestarts?
sh -c 'startapp', binary not installed127sh: startapp: not foundYes
--entrypoint /usr/local/bin/startapp, file missing127none, docker run itself failsNo
Entrypoint file without the execute bit126none, docker run itself failsNo
Script with Windows line endings in the shebang255exec /start.sh: no such file or directoryYes
Script without a shebang line255exec /start.sh: exec format errorYes
node serve.js, wrong file name1Cannot find module '/app/serve.js'Yes

Two of those never reached the restart policy. When the runtime cannot start the process at all, docker run fails with an error and the container stays in Created:

output
docker: Error response from daemon: failed to create task for container: ... exec: "/usr/local/bin/startapp": stat /usr/local/bin/startapp: no such file or directory: unknown
bash
docker inspect lrc-ep --format '{{.Name}} RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}} Status={{.State.Status}}'
output
/lrc-ep RestartCount=0 ExitCode=127 Status=created

This matches the Docker documentation: “A restart policy only takes effect after a container starts successfully” (start containers automatically).

The 255 cases are the sneaky ones, because the file exists. The host here is arm64 and we did not use emulation, so this exec format error is the honest kind: a script that the kernel cannot execute. Look at the first line of the file inside the image:

bash
docker run --rm --entrypoint head lrcrash/crlf:v1 -n 1 /start.sh | od -c | head -3
docker run --rm --entrypoint head lrcrash/noshebang:v1 -n 1 /start.sh
docker run --rm --entrypoint ls lrcrash/noexec:v1 -l /start.sh
output
0000000    #   !   /   b   i   n   /   s   h  \r  \n
echo hello
-rw-r--r--    1 root     root            37 Oct 24 20:39 /start.sh

The \r after /bin/sh is a Windows line ending: the kernel looks for an interpreter named /bin/sh\r. The second file has no shebang. The third has no execute bit.

Fix: convert the line endings, add the shebang, or chmod +x. We rebuilt the first two images and both ran:

bash
sed -i 's/\r$//' start.sh
docker run -d --name lrc-crlf --restart unless-stopped lrcrash/crlf:v2
docker logs lrc-crlf
output
hello

For the shell form, the fix was running the real command: --entrypoint sh lrcrash/app:v1 -c 'node server.js' came up and logged listening 0.0.0.0:3000.

Cause 3: The port or interface does not match (restarting versus unhealthy)#

This cause does not restart anything, and that is the lesson. We started the app on port 8080 while the health check and the published port both expected 3000:

bash
docker run -d --name lrc-port --restart unless-stopped -p 3000:3000 -e DATABASE_URL=postgres://demo@db.example.com/shop -e DATA_DIR=/tmp -e PORT=8080 \
  --health-interval 5s --health-timeout 2s --health-retries 2 --health-cmd 'wget -qO- http://127.0.0.1:3000/healthz' lrcrash/app:v1
docker ps --filter name=lrc-port --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker inspect lrc-port --format 'Health={{.State.Health.Status}} FailingStreak={{.State.Health.FailingStreak}} RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}} Running={{.State.Running}}'
curl -sS -m 3 -o /dev/null -w '%{http_code}\n' http://localhost:3000/
output
NAMES      STATUS                      PORTS
lrc-port   Up 25 seconds (unhealthy)   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
Health=unhealthy FailingStreak=4 RestartCount=0 ExitCode=0 Running=true
curl: (56) Recv failure: Connection reset by peer
000

The process is running, RestartCount is 0, and docker ps says Up ... (unhealthy) instead of Restarting. In our run Docker never restarted it. If you expect an unhealthy container to heal itself, it will not.

The health log says why:

bash
docker inspect lrc-port --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' | tail -2
output
1 wget: can't connect to remote host (127.0.0.1): Connection refused

A health check can also lie in the other direction. With the right port but HOST=127.0.0.1, the check inside the container passed and the host still got a reset:

output
NAMES       STATUS
lrc-iface   Up 14 seconds (healthy)
curl: (56) Recv failure: Connection reset by peer
bash
docker exec lrc-iface netstat -ltn
output
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:3000          0.0.0.0:*               LISTEN

The app listens on loopback only, so published ports cannot reach it. Fix: listen on 0.0.0.0 and make the port, the health check and -p agree. After that:

output
lrc-port   Up 12 seconds (healthy)
hello from v1 pid=1

If you want the story of a check that passes while the app is dead, read our postmortem.

Cause 4: The kernel kills it for memory (exit 137)#

Exit code 137 is 128 plus signal 9. Plenty of things send signal 9, so the field to read is OOMKilled. We ran an app that allocates 8 MiB every 100 ms with a 64 MiB limit:

bash
docker run -d --name lrc-oom --restart unless-stopped --memory 64m -e MODE=oom -e DATABASE_URL=postgres://demo@db.example.com/shop -e DATA_DIR=/tmp lrcrash/app:v1
docker inspect lrc-oom --format 'RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'
output
RestartCount=7 ExitCode=137 OOMKilled=true

docker events shows the sequence for each lap. The container started, the kernel reported an out of memory event about a second later, then the container died:

output
1792874607 start
1792874608 oom
1792874608 die exitCode=137
1792874609 start
1792874610 oom
1792874610 die exitCode=137

One trap we hit: docker inspect describes the current run. Our first inspect, taken while the container was between laps, showed RestartCount=4 ExitCode=0 OOMKilled=false. The 137 only shows while the container is down, so use docker events or inspect again. The logs also end mid-stream with no error message, because the process never got to write one.

Fix: find the real memory need, then raise the limit or fix the leak. With --memory 256m and no leak, the app used 11.3 MiB:

output
lrc-oom Up 8 seconds
lrc-oom 11.3MiB / 256MiB

Cause 5: It cannot write where it needs to (permission denied, read-only)#

Two variants of the same crash. First, a non-root user and a directory created by root:

bash
docker run -d --name lrc-perm --restart unless-stopped -e DATABASE_URL=postgres://demo@db.example.com/shop lrcrash/app:v1
docker logs --tail 2 lrc-perm
docker run --rm --entrypoint sh lrcrash/app:v1 -c 'id -un; ls -ld /data'
output
fatal: cannot write /data: EACCES: permission denied, open '/data/app.log'
fatal: cannot write /data: EACCES: permission denied, open '/data/app.log'
node
drwxr-xr-x    2 root     root          4096 Oct 24 20:39 /data

Exit code 1, and the answer sits in the last command: the app runs as node, the directory belongs to root. Fix: chown node:node /data in the Dockerfile. With the rebuilt lrcrash/app:v2, the container stayed Up 8 seconds.

Second, a read-only root filesystem, which is a good hardening flag until the app tries to write:

bash
docker run -d --name lrc-ro --restart unless-stopped --read-only -e DATABASE_URL=postgres://demo@db.example.com/shop -e DATA_DIR=/app lrcrash/app:v2
docker inspect lrc-ro --format 'RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}} ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}}'
output
fatal: cannot write /app: EROFS: read-only file system, open '/app/app.log'
RestartCount=6 ExitCode=1 ReadonlyRootfs=true

Fix: keep --read-only and give the app one writable place:

bash
docker run -d --name lrc-ro --restart unless-stopped --read-only --tmpfs /data:uid=1000,gid=1000 -e DATABASE_URL=postgres://demo@db.example.com/shop lrcrash/app:v2
output
lrc-ro    Up 8 seconds
listening 0.0.0.0:3000

A tmpfs is wiped on restart. If the app needs the data to survive, mount a volume instead.

The cheat sheet#

Exit codeSeen withCheck first
1missing env var, unwritable path, wrong file namedocker logs
126, 127not executable, not founddocker run error text, ls -l the file
255bad shebang, CRLF, no shebangfirst line of the script
137memory limitOOMKilled in docker inspect, docker events
none, Up (unhealthy)port or interface mismatch.State.Health.Log

The same crash on Levelrail#

On a single Docker host you find a crashloop by looking. A platform should find it for you and, more importantly, keep your users away from it. We deployed v2, an image that exits with code 1 when DATABASE_URL is missing (cause 1), over a healthy v1 with a readiness check, while a request loop sent one request every 200 ms.

bash
levelrail-cli apps deploy shop --image lrcrash/shop:v2
levelrail-cli apps deploys wait shop
output
deploy dep_YXFsc5HnqD_W of "shop": failed
failure:  container_crashed (retryable: false)
  cause:  The container exited or is restarting repeatedly.
  fix:    Read the log excerpt for what happens right after start: a bad command, a missing file or a config error is the usual cause. Fix it and redeploy, or roll back.
  docs:   /deploy-failures#container_crashed
  log excerpt:
    Container exited with code 1 after 100ms, before it became ready
    ExitedDuringReadiness

The wait returned exit code 7 after 2 seconds. Status and logs agree with it:

bash
levelrail-cli apps status shop
levelrail-cli apps logs shop
output
TYPE               STATUS   REASON                 MESSAGE                                                           LAST TRANSITION
Ready              False    ExitedDuringReadiness  Container exited with code 1 after 100ms, before it became ready  2026-10-24T20:46:17Z
2026-10-24T20:45:47Z stdout starting v1 pid=1
2026-10-24T20:46:16Z stderr fatal: DATABASE_URL is required
2026-10-24T20:46:17Z stderr fatal: DATABASE_URL is required

The dashboard shows the crashloop banner with the last lines the container wrote:

The dashboard app overview with a red banner reading This app is crashlooping, the exit code 1 message and five lines of fatal: DATABASE_URL is required
The crashloop banner shows the exit code and the last log lines, the same evidence docker logs gave you.

Users never saw it#

Throughout the bad deploy, the diagnosis and the fix, the old release kept answering. The request loop sent 3491 requests with 0 failures, all served by v1 or the fixed v2. A container that exits before it passes readiness never gets traffic.

Retries and backoff#

Levelrail kept retrying the crashing container. The control plane log says keeps exiting (1 restarts), next restart in 3s, then (2 restarts), next restart in 10s. The crash lines in apps logs were stamped 20:46:16, 20:46:17, 20:46:29, 20:46:49, 20:47:49, 20:49:19, 20:51:49 and 20:56:19. The gaps were 1, 12, 20, 60, 90, 150 and 270 seconds.

Let it name the cause#

bash
levelrail-cli apps diagnose shop
output
confidence:  high
The app exited because it expected environment variables that are not configured: DATABASE_URL.

suggested next step:
  Set DATABASE_URL

causes:
  MISSING_ENV (high): Required environment variable is not set
    fix 1 [input]: Set DATABASE_URL
      env.DATABASE_URL: needs a value (--input env.DATABASE_URL=VALUE)

It reads the same deploy error, condition and log line you would have read by hand, with fixed rules. It does not call a model.

The fix path has bumps#

We applied fix 1 and asked for a redeploy:

bash
levelrail-cli apps diagnose shop --apply-fix 1 --input env.DATABASE_URL=postgres://demo@db.example.com/shop --redeploy
levelrail-cli apps apply shop
output
applied fix 1 to shop: [env.DATABASE_URL]
applying 1 pending change(s) to app "shop": restarting it (env DATABASE_URL)

Neither fixed the app. The redeploy still failed with the old environment, and apps status kept printing pending changes: env DATABASE_URL. A plain apps deploy shop --image lrcrash/shop:v2 worked, but only after the retry timer fired: we issued it at 15:54:17 and the container came up at 15:56:19, so deploys wait returned healthy after 2 minutes 14 seconds. Plan for a couple of minutes, not seconds, when you fix a crashloop that has been going for a while.

A crash after the readiness check passes#

The harder case is an app that is healthy for a while, then dies. v3 passes readiness and exits with code 1 forty seconds after it starts.

bash
levelrail-cli apps deploy shop --image lrcrash/shop:v3
levelrail-cli apps deploys wait shop
output
deploy dep_hxWfutKBr-4T of "shop": healthy

That is exit code 0 after 12 seconds, and it was correct at that moment. Forty seconds later the container crashed. The request loop shows what users got during the next 100 seconds:

output
15:57:58.425 200 v2
15:58:10.432 200 v3
15:58:39.924 503
15:58:40.128 200 v2
15:58:50.704 200 v3
15:59:20.514 0 TimeoutError   (5 timeouts, 2 s apart)
15:59:30.549 503
15:59:32.144 200 v3
15:59:43.505 200 v2

Traffic flipped between v3 and the old release, and 7 of 646 requests failed (two 503s and five timeouts, about 10 seconds in total) before the post-deploy watch gave up. After the second restart the watch rolled back:

bash
levelrail-cli apps deploys list shop
output
ID                IMAGE                                     STATUS     ROLLOUT  REASON
dep_X3Qoi_qPVtFt  levelrail-rollback/shop:dep_yVUW930rPhfr  succeeded  serving  AutoRolledBack
dep_hxWfutKBr-4T  lrcrash/shop:v3                           failed     failed   PostDeployCrashloop

The failed row carries the reason the new release restarted 2 times after cutover (limit 2), and apps timeline shows both events.

The deploy history with a failed v3 row marked PostDeployCrashloop and the auto rollback row above it
The watch tripped after two restarts and put the previous release back.

What to know before you rely on it#

We found these while recording this guide:

  • A crash after readiness reaches users. In our run 7 requests failed before the automatic rollback, even though the deploy had passed its readiness check. The gate only guards the cutover, not what happens after it.
  • deploys wait reports healthy for v3 and never changes its mind. Check apps status or apps deploys list after a deploy if the app can crash later.
  • The dashboard banner stays after recovery. After the fix, the app showed Healthy and This app is crashlooping together, with lines from the healthy container underneath. It was also still showing after the automatic rollback.
  • The failure excerpt can include lines from the old container, such as starting v1 and listening 3000.
  • The CLI never says “crashloop” or when the next retry happens. That text is in the control plane log only.
  • The retries have no cap that we saw: eight attempts in ten minutes, with gaps growing to 270 seconds.
  • The fix path in the section above, diagnose --apply-fix --redeploy and apps apply, did not apply the new environment while the crashloop timer was running.

We track these as open issues and will update this guide as they are fixed.

The app showing a Healthy badge in the sidebar while the crashlooping banner is still visible
Healthy and crashlooping at the same time: the banner outlived the problem.

Recap#

  1. Read docker ps, docker logs and docker inspect (ExitCode, OOMKilled, RestartCount) before changing anything.
  2. Exit 1 is the app stopping itself, 126, 127 and 255 are an entrypoint that cannot run, and 137 with OOMKilled=true is the memory limit.
  3. Up (unhealthy) is not a restart loop. Docker did not restart it, so fix the port, the interface and the health command.
  4. A platform that gates the cutover keeps users on the old release when a new one crashes at start. When a release crashes after the gate, expect a short burst of errors until the watch rolls back.

To see how a gated deploy fails safely, read the rollback guide.

Frequently asked questions#

Why does my Docker container keep restarting?

The main process exits and your restart policy starts it again. Run docker inspect and read ExitCode, then docker logs. In our runs exit 1 meant the app stopped itself (missing env var, unwritable path), 127 meant a command was not found, 255 meant the entrypoint could not be executed, and 137 with OOMKilled true meant the memory limit killed it.

What does Restarting (1) mean in docker ps?

The container is between runs, waiting to be started again by its restart policy, and the number is the exit code of the last run. Restarting (1) means the last run exited with code 1. RestartCount in docker inspect tells you how many restarts have happened.

Does Docker restart an unhealthy container?

Not in our run. A container with a failing health check showed Up (unhealthy) and RestartCount stayed 0. Only a process exit triggers the restart policy, so an unhealthy container needs something else to act on the health status.

Next steps

Keep reading

A bad release your users never see A bad release your users never see

tutorial 3 min read

Roll Back a Failed Docker Deploy Without Downtime

Ship a broken release on purpose, watch your users stay on the old version, then roll back with one command. Real commands, real output, self-hosted.

  • Docker
  • Deployments
  • Rollback
Restarts that never drop a request Restarts that never drop a request

tutorial 6 min read

Restart a Docker Container Without Downtime

Restart a Docker app with one command and zero failed requests, then see what a plain stop and start costs. Real output from a self-hosted run.

  • Docker
  • Deployments
  • Zero downtime