Container keeps restarting: find the cause from its exit code

A step-by-step check for a Docker container stuck in a restart loop: read its status and exit code, follow the branch that matches, and fix the cause with commands you run on your own server.

Published 15 October 2026

A container that restarts every few seconds hides its own error: by the time you look, it is already starting again. The fastest way in is the number Docker records each time the container stops, its exit code, because the code alone separates an application that gave up, a command that never started and a process that something else killed.

Answer one question at a time. Each step shows a command to run on your server and what its output means; the page never runs anything and sends nothing anywhere. Your answers are kept in the page's link, so you can step back, start over or send the link to someone who helps you; it holds only the answers you picked, never your output.

Find the cause, one step at a time

In the commands, replace <container>, <service> and <image> with your own names. Nothing runs from this page.

Step 1

What state is the container in?

Start from what Docker itself reports. A container in a loop shows Restarting or an Up time that never grows past a few seconds; one that is up but failing its health check shows (unhealthy).

Replace <container> with the container's name, as docker ps -a lists it. In Compose it is usually the project name, the service name and a number.

This command only reads.

Run on your server
docker ps -a --filter name=<container> --format "{{.Names}}: {{.Status}}"
What does the output show?

Every step as text

The same tree as the wizard above, all branches at once, for reading or printing.

Show every question, check and solution of the wizard

What state is the container in?

Start from what Docker itself reports. A container in a loop shows Restarting or an Up time that never grows past a few seconds; one that is up but failing its health check shows (unhealthy).

Replace <container> with the container's name, as docker ps -a lists it. In Compose it is usually the project name, the service name and a number.

Command

docker ps -a --filter name=<container> --format "{{.Names}}: {{.Status}}"

Answers

  • Restarting, or Up for only a few seconds each time: go to “Read the exit code”
  • Exited, and it stays down: go to “Read the exit code”
  • Up, but marked (unhealthy): go to “Read the health check results”
  • Up for minutes and stable right now: go to “No branch matched: collect what you need to dig further”

Read the exit code

Every time the main process of a container ends, Docker records the code it ended with. That number is the shortest way to the cause: it tells apart an application that gave up, a command that could not start, and a process that something else killed.

The command prints three values: the exit code, whether the kernel killed it for exceeding its memory limit (OOMKilled), and how many times Docker has restarted it. The reference table further down explains every code in the list.

Command

docker inspect --format "{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}" <container>

Answers

  • 0: go to “The main process finishes and exits”
  • 1, or another code from 2 to 124, or 128: go to “Read the last lines of the log”
  • 125: go to “Docker could not start the container”
  • 126: go to “The start command is not executable”
  • 127: go to “The start command does not exist in the image”
  • 137, and OOMKilled is true: go to “The container reached its memory limit”
  • 137, and OOMKilled is false: go to “When was it killed?”
  • 139: go to “The program crashed in native code”
  • 143: go to “Something outside keeps stopping it”
  • Another code above 128: go to “The process was ended by a signal”

Read the last lines of the log

With an application error, Docker knows only that the program gave up. The program usually says why just before it exits, so the last lines of its log matter more than anything earlier.

If the log is long, look for the last line that mentions an error, a failure or a refusal, and pick the answer that describes it best.

Command

docker logs --tail 50 <container>

Answers

  • A missing or invalid setting, variable or config file: go to “A setting is missing or wrong”
  • Connection refused, a timeout or an unknown host when reaching a database or another service: go to “It cannot reach a service it depends on”
  • Permission denied on a file or directory: go to “The container's user cannot access its files”
  • Address already in use: go to “The port it listens on is already taken”
  • Nothing clear, or no log at all: go to “No branch matched: collect what you need to dig further”

When was it killed?

Exit code 137 without OOMKilled means a SIGKILL that did not come from the container's own memory limit. Two sources are common: Docker itself, when a container ignores the stop request until the timeout runs out, and the kernel, when the whole server runs out of memory.

Think about when the restarts happen: right after you or a tool stopped, updated or recreated the container, or at random times while it was running.

Answers

  • While it was being stopped, updated or recreated: go to “The container does not stop in time and is killed”
  • On its own, while it was running: go to “Did the server run out of memory?”

Did the server run out of memory?

When the whole server runs out of memory, the kernel's out-of-memory killer picks a process and kills it, inside a container or not. The container's own OOMKilled flag stays false, because its limit was never reached; the kernel log is where it shows.

Reading the kernel log may need sudo. The command only reads.

Command

journalctl -k --since "1 hour ago" | grep -i -E "out of memory|killed process"

Answers

  • Lines with Out of memory or Killed process: go to “The server ran out of memory”
  • Nothing: go to “No branch matched: collect what you need to dig further”

Read the health check results

A health check is a command Docker runs inside the container on a schedule. Docker on its own does not restart a container because it is unhealthy: it only marks it. When an unhealthy container restarts, something else acts on that mark, such as Swarm, an autoheal container or a script.

So the question is why the check fails. Docker keeps the last few results with their output.

Command

docker inspect --format "{{json .State.Health}}" <container>

Answers

  • The check's output shows an error: not found, connection refused or a wrong port: go to “The health check itself is broken”
  • The check passes later, but the container is restarted before it gets there: go to “The application needs longer to start than the check allows”

The main process finishes and exits

Exit code 0 is not a crash. A container runs exactly as long as its main process; when that process returns, the container stops, and a restart policy such as always or unless-stopped starts it again, forever.

Two cases cause this. A one-off job (a migration, a backup, an init step) that was given a restart policy meant for servers. Or a server that moves itself to the background, so the foreground process that Docker watches exits right away.

What to do

  1. See what the container runs.

    Command

    docker inspect --format "{{json .Config.Entrypoint}} {{json .Config.Cmd}}" <container>
  2. For a one-off job, set restart: "no" or restart: on-failure on that service in the Compose file, so a successful run stays finished.
  3. For a server, run it in the foreground: most images document a flag for it (nginx, for example, takes -g 'daemon off;'). Do not keep the container alive with tail -f or sleep: the real process would still be unwatched.
  4. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

Docker could not start the container

Code 125 comes from Docker, not from the application: the process inside never started. The reason is in Docker's error message, which a restart loop hides because nobody watches the start.

The usual causes are a host port another process already holds, a bind mount whose source path does not exist on this server, a device or option this host does not support, or a network that was removed.

What to do

  1. Start the service once in the foreground and read the error Docker prints. Stop it with Ctrl+C afterwards.

    Changes your server

    docker compose up <service>
  2. If the message is not enough, read the Docker daemon's own log.

    Command

    journalctl -u docker --since "1 hour ago"
  3. Fix what the message names: a different host port, an existing path for the mount, or an option this host supports. Then start the service again in the background.

    Changes your server

    docker compose up -d <service>

The start command is not executable

Code 126 means the file the container starts with was found but could not be run. Most often it is a script without the executable bit: it lost it on the way through Git, an archive or a copy from Windows, or it comes from a bind mount whose file on the host was never made executable.

A bind-mounted file keeps the permissions it has on the host, whatever the image would have set.

What to do

  1. See which file the container starts with.

    Command

    docker inspect --format "{{json .Config.Entrypoint}} {{json .Config.Cmd}}" <container>
  2. If it comes from a bind mount, check its mode on the host: the first column must contain x.

    Command

    ls -l ./entrypoint.sh
  3. Make it executable on the host.

    Changes your server

    chmod +x ./entrypoint.sh
  4. If the script is built into the image, set the mode at build time with COPY --chmod=755 in the Dockerfile, then rebuild the image.
  5. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The start command does not exist in the image

Code 127 is the shell's "command not found". The command or script the container starts with is not where the configuration says, or the program it needs is not in the image.

Three causes cover most cases: a path or name typed differently in the Compose file than in the image, a tool such as bash or curl that a smaller image (Alpine, slim, distroless) does not include, and a script saved with Windows line endings, whose first line then names an interpreter that does not exist (/bin/sh followed by an invisible carriage return).

What to do

  1. See the exact command the container starts with, and compare it with the image's documentation.

    Command

    docker inspect --format "{{json .Config.Entrypoint}} {{json .Config.Cmd}}" <container>
  2. Check a mounted or copied script for Windows line endings: the output then says with CRLF line terminators.

    Command

    file ./entrypoint.sh
  3. Convert it to Unix line endings, and keep it that way in Git (.gitattributes with *.sh text eol=lf).

    Changes your server

    sed -i 's/\r$//' ./entrypoint.sh
  4. If the script needs bash and the image only has sh, start it with #!/bin/sh and avoid bash-only syntax, or use the image variant that includes bash.
  5. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The container reached its memory limit

OOMKilled: true is the clearest answer Docker gives: the container used more memory than its limit allowed, and the kernel killed its main process. The restart policy starts it again, it grows again, and the loop repeats, often at a steady interval.

Either the limit is lower than the application really needs, or the application is configured to use more than the limit (a database cache, a Java heap or a worker count sized for a whole server).

What to do

  1. See the memory limit in bytes (0 means no limit of its own).

    Command

    docker inspect --format "{{.HostConfig.Memory}}" <container>
  2. Watch its memory use in the seconds it is up.

    Command

    docker stats --no-stream <container>
  3. Either raise the limit in the Compose file (mem_limit, or deploy.resources.limits.memory), or lower what the application is set to use, following its documentation. Keep the limit: it protects the rest of the server.
  4. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The container does not stop in time and is killed

docker stop sends SIGTERM, waits (10 seconds by default) and then sends SIGKILL. A process that does not react to SIGTERM in time ends with 137 on every stop, update or recreation, and any cleanup it should have done (closing files, finishing writes) does not happen.

The common reason is a start command in shell form: the shell becomes the main process and does not pass the signal on to the application. A slow but correct shutdown is the other reason.

What to do

  1. Write the start command in exec form, ["app", "--flag"], instead of a plain string, in the Dockerfile or in command:.
  2. If the image starts through a script, add init: true to the service, so a small init process forwards signals and reaps child processes.
  3. If the application needs longer to shut down cleanly, give it time with stop_grace_period: 30s.
  4. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The server ran out of memory

The kernel killed a process in the container because the whole server had no memory left, not because this container passed a limit of its own. The container that dies is not necessarily the one that used the most: the kernel picks by its own score.

The lasting fix is a memory limit on every container, so one of them cannot take the whole server, and a server sized for the total.

What to do

  1. See how much memory and swap the server has, and how much is free.

    Command

    free -m
  2. See which containers use the most.

    Command

    docker stats --no-stream
  3. Give every service a memory limit in the Compose file (mem_limit, or deploy.resources.limits.memory) that adds up to less than the server has, leaving room for the system itself.
  4. If the total really does not fit, move to a server with more memory, or move a service to another server. Swap can absorb short peaks but not a steady shortage.
  5. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The program crashed in native code

Code 139 is a segmentation fault: the kernel stopped the program for touching memory it should not. Configuration rarely causes this; the binary itself does. In containers the usual reasons are an image built for another processor type running under emulation, a build that needs CPU instructions this server lacks (some databases and AI runtimes need AVX or AVX2), or a bug in the exact version.

A version that ran yesterday and crashes after an update points at the update.

What to do

  1. See the server's processor type.

    Command

    uname -m
  2. See which platform the image was built for: it should match (x86_64 is amd64, aarch64 is arm64).

    Command

    docker image inspect --format "{{.Os}}/{{.Architecture}}" <image>
  3. If the application's documentation requires AVX2, check that the processor has it (no output means it does not).

    Command

    grep -o -w -m1 avx2 /proc/cpuinfo
  4. Pin the previous version's tag in the Compose file instead of latest, and report the crash to the project with its version.
  5. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

Something outside keeps stopping it

Code 143 means the process received SIGTERM and stopped as asked. That is a clean stop, not a crash. In a loop, the question is who sends it: an autoheal container acting on a failed health check, an update tool such as Watchtower, a scheduled script, a systemd unit, or a deployment that recreates the service again and again.

Docker's event log records each stop with its time, which narrows it down quickly.

What to do

  1. List the last hour's events of the container: kill, stop and die show when it was stopped.

    Command

    docker events --since 1h --until "$(date +%s)" --filter container=<container>
  2. See which other containers run on the server: autoheal, Watchtower or a similar tool is a likely sender.

    Command

    docker ps --format "{{.Names}} {{.Image}}"
  3. Match the times with scheduled jobs on the server.

    Command

    systemctl list-timers --all
  4. Fix the sender, not the container: repair the health check it reacts to, or change the tool's schedule.

The process was ended by a signal

Any exit code above 128 is 128 plus the number of the signal that ended the process. Subtract 128 and look the number up in the table further down, or in the Linux signal list.

134 (SIGABRT) is the most common one here: the program stopped itself after detecting a problem it could not recover from, and usually wrote a line about it to its log just before. 130 (SIGINT) means it was interrupted, as with Ctrl+C.

What to do

  1. Read the last lines of the log for the program's own message before the signal.

    Command

    docker logs --tail 50 <container>
  2. For SIGABRT, search the project's issue tracker for that message and your version.

A setting is missing or wrong

Many applications check their settings at start-up and exit with an error when a required one is missing. In Compose the usual causes are a .env file that is not next to the Compose file or not read by it, a variable spelled differently in the file than the application expects, and a mounted config file that is not there.

That last one has a trap: when the source of a bind mount does not exist, Docker creates an empty directory in its place, and the application then finds a directory where it expected a file.

What to do

  1. See the Compose file as Docker reads it, with every variable filled in. The output includes your secrets: do not paste it anywhere public.

    Command

    docker compose config
  2. Compare the variable names with the application's documentation, letter by letter.
  3. Check that a mounted config file is a file: a d at the start of the line means Docker created a directory in its place.

    Command

    ls -ld ./config.yml
  4. If it is an empty directory, remove it and put the real file in its place.

    Changes your server

    rmdir ./config.yml
  5. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

It cannot reach a service it depends on

The application starts, tries to connect to its database or another service, fails, and exits; the restart policy tries again. Two causes are typical. The address is wrong: inside a container, localhost is the container itself, so another service is reached by its Compose service name. Or the order is wrong: the database container has started but is not ready to accept connections yet.

depends_on alone only waits for the other container to start. Waiting until it is ready needs a health check on the dependency and the service_healthy condition.

What to do

  1. Make sure the connection settings use the other service's Compose name (for example db:5432), not localhost or the server's address.
  2. Read the dependency's own log: it may be failing too.

    Command

    docker compose logs --tail 50 <database-service>
  3. Check that both containers are on the same network.

    Command

    docker inspect --format "{{json .NetworkSettings.Networks}}" <container>
  4. Give the dependency a healthcheck and make the application wait for it with depends_on: { db: { condition: service_healthy } }.
  5. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The container's user cannot access its files

Many images run their application as a user other than root, with a fixed numeric id. A bind-mounted directory keeps the owner it has on the host, and the container sees numbers, not names: if the directory belongs to another id, the application cannot write to it and exits.

This often appears after moving data from another server, restoring a backup, or creating the directory by hand as root.

What to do

  1. See which user the container runs as (empty means the image's default; its documentation names the id).

    Command

    docker inspect --format "{{.Config.User}}" <container>
  2. See the numeric owner of the mounted directory on the host.

    Command

    ls -ln ./data
  3. Give the directory to the container's user id (1000 here is an example: use the id from the previous steps). Do not use chmod 777: it opens the data to every user on the server.

    Changes your server

    sudo chown -R 1000:1000 ./data
  4. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The port it listens on is already taken

Inside its own network namespace, a container rarely meets another listener. "Address already in use" therefore usually means one of two things: the service runs with network_mode: host, so it shares the server's ports and another program already holds one; or two processes in the same container try to open the same port.

A conflict on a published port (ports:) is different: Docker reports it when starting the container, with exit code 125.

What to do

  1. See which program listens on each port of the server (sudo shows the process names).

    Command

    sudo ss -ltnp
  2. With network_mode: host, move the application to a free port in its own settings, or stop the program that holds it. Without a reason for host networking, prefer the default network with ports:.
  3. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The health check itself is broken

The application may be fine while its check fails. The check runs inside the container, so it can only use tools the image contains (many small images have neither curl nor wget), and it must address the container's own port, not the port published on the server.

Fixing the check also stops whatever restarts the container because of it.

What to do

  1. See the check as the container runs it.

    Command

    docker inspect --format "{{json .Config.Healthcheck}}" <container>
  2. Fix healthcheck.test in the Compose file: a tool that exists in the image, localhost, and the port the application listens on inside the container. Some images ship their own check command; their documentation names it.
  3. Find what restarts unhealthy containers on this server (an autoheal container, Swarm or a script).

    Command

    docker ps --format "{{.Names}} {{.Image}}"
  4. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

The application needs longer to start than the check allows

Some applications need minutes on their first start: a database migration, an index build, a large download. Meanwhile the check fails, the retries run out, the container is marked unhealthy and is restarted, and the work starts over, so it never finishes.

start_period gives the application a grace time in which failed checks do not count.

What to do

  1. See how long the application took to become ready, from the timestamps in its log.

    Command

    docker logs --timestamps --tail 100 <container>
  2. Set start_period in the service's healthcheck longer than that (for example start_period: 5m), and keep interval and retries as they are.
  3. Recreate the container so the change takes effect.

    Changes your server

    docker compose up -d <service>

No branch matched: collect what you need to dig further

The answers so far do not point at one cause. Collect the same set of facts every time it happens, before changing anything: they let you compare one restart with the next, and they are what a project's issue tracker or forum will ask for.

All the commands below only read. docker inspect prints the container's environment variables, which often hold passwords and tokens: remove them before you share the output with anyone.

What to do

  1. The container's status.

    Command

    docker ps -a --filter name=<container>
  2. Its full configuration and state (mask secrets before sharing).

    Command

    docker inspect <container>
  3. Its log with timestamps.

    Command

    docker logs --timestamps --tail 200 <container>
  4. Its events in the last hour.

    Command

    docker events --since 1h --until "$(date +%s)" --filter container=<container>
  5. The Docker version.

    Command

    docker version
  6. Memory and disk on the server: a full disk also stops containers.

    Command

    free -m && df -h
  7. The kernel log for the same hour.

    Command

    journalctl -k --since "1 hour ago"

Exit code reference

The codes a container's main process ends with, as docker inspect reports them in .State.ExitCode. The wizard above leads to the same explanations.

CodeWhat it meansCheck first
0Finished successfully
The main process ended on its own and reported success. Nothing crashed: the program simply had nothing more to do, or it moved itself to the background and the foreground process left.
Is this a one-off job running under a restart policy, or a server started in daemon mode? A container lives only as long as its main process stays in the foreground.
1Application error
The generic failure code: the application itself decided to stop because something was wrong. Docker adds nothing here; the reason is in the application's own output.
Read the last lines of the container's log: a missing setting, a refused connection or a permission error usually shows up right before the exit.
125Docker could not run the container
The error came from Docker, not from the application: the container was never started, for example because of an invalid option, a port that is already taken or a device or mount that does not exist.
Read the error Docker printed when starting the container, and the Docker daemon's own log.
126Command cannot be executed
The command the container starts with exists but cannot be run: the file is not executable, it is a directory, or the process lacks permission to run it.
Check the entrypoint and command of the container and the permissions of the script they point to, especially when it comes from a bind mount.
127Command not found
The command the container starts with does not exist inside the image: a wrong path, a tool missing from a slimmer image, or a script whose first line names an interpreter that is not there.
Compare the command in your Compose file or Dockerfile with what the image actually contains, and look for Windows line endings in shell scripts.
128Invalid exit value
128 itself is not a signal. Some programs end with it on a fatal start-up error, and shells use it when a script calls exit with a value they cannot use. Codes above 128 are different: they mean the process was ended by a signal.
Treat it like an application error: the log shows what failed.
137Killed (SIGKILL), often out of memory 128 + signal 9 (SIGKILL)
128 + 9: the process was killed with SIGKILL and had no chance to clean up. The kernel's out-of-memory killer does this when a container passes its memory limit or the server runs out of memory; Docker does it too when a container ignores the stop request until the timeout.
Look at .State.OOMKilled in docker inspect, then at the kernel log of the server.
139Segmentation fault (SIGSEGV) 128 + signal 11 (SIGSEGV)
128 + 11: the program read or wrote memory it was not allowed to touch and the kernel stopped it. In a container this usually points at native code: a library that does not match the image, a build that needs CPU features this server lacks, or a bug in that version.
Compare the image's architecture with the server's, and try the previous version of the image.
143Stopped on request (SIGTERM) 128 + signal 15 (SIGTERM)
128 + 15: something asked the process to stop and it did. This is the normal result of docker stop or of recreating a container; in a loop it means something outside the container keeps stopping it.
Find who sends the stop: the Docker events of the container show each stop and what triggered it.

Any other code above 128 is 128 plus the number of the signal that ended the process. Common ones: 129 = SIGHUP, 130 = SIGINT, 131 = SIGQUIT, 132 = SIGILL, 134 = SIGABRT, 135 = SIGBUS, 136 = SIGFPE, 141 = SIGPIPE, 142 = SIGALRM.

What this does not cover

The wizard covers a single Docker host with Docker Engine and Compose. Kubernetes (CrashLoopBackOff), Swarm services and Podman report the same exit codes, but their commands and restart logic differ.

Commands marked as changing the server do exactly that: read them before you run them, and keep a backup of any data directory you change. When no branch matches, the last step lists what to collect; it does not guess.

Sources

How ANAROQ sources the facts on its pages is described in the methodology. Read the methodology

All troubleshooting pages