Updated
Docker exit codes explained: why your container exits right after deploy
CI says the deploy succeeded, but a few seconds later docker ps -a shows Exited (1) or Exited (137), or the container keeps flipping to Restarting. Users can't reach the app. This guide is for developers and founders who just shipped to a VPS and want a first read of the Docker exit code: confirm the container's state, look up what the code usually means and what it does not prove, then read a bounded window of logs to find the first error. For a container stuck in a restart loop, continue with Docker container keeps restarting once you have the exit code.
Every command here is read-only. Run them only on a server you are authorized to access, and replace YOUR_CONTAINER with the real container name or ID.
1. Confirm the state and the exit code
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
docker inspect --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}' YOUR_CONTAINER
If you deploy with Compose, docker compose ps -a lists the project's containers, including stopped ones; the container name is usually <project>-<service>-1.
Exited (1) 8 seconds ago: the main process exited once with code 1.Restarting (1) 3 seconds ago: a restart policy keeps bringing it back, and the number in brackets is the code from the last exit.The inspect line prints three values: the exit code, whether Docker recorded an out-of-memory kill (
true/false), and Docker's own error message. Keep them apart; they answer different questions.State.Erroris filled mostly when Docker itself couldn't start the process (bad command, port already allocated, and so on). It is empty for most application crashes, so empty does not mean healthy.A status of
Createdmeans the container was created but never ran. The problem is usually in the run command or image, not in your code.
To see how long the process actually ran and whether a restart policy is involved:
docker inspect --format 'started={{.State.StartedAt}} finished={{.State.FinishedAt}} restarts={{.RestartCount}} policy={{.HostConfig.RestartPolicy.Name}}' YOUR_CONTAINER
A few seconds between started and finished is the "exits right after deploy" pattern. These timestamps are in UTC (they end in Z); convert before comparing them with your deploy time or with host logs in local time.
What this does not tell you: why the process exited. It also doesn't confirm you're looking at the right thing, so check that the name and image tag match the deploy you just shipped.
2. Docker exit code table: what to run next, and what each code does not prove
| Code | Usual meaning | What to run next | What it does not prove |
|---|---|---|---|
| 0 | The main process finished successfully | Check what the container is told to run (command below). A web service that exits 0 usually started itself in the background, or its command was a one-shot task. For a migration or backup job, 0 is the expected result. | That anything is wrong with your code, or that the service is fine. It only says the main process ended without an error. |
| 1 | Generic application error | Read the logs (step 3) and find the first error. Compare with what changed in this deploy: image tag, environment variables, config, migrations. | Which error it was. Many runtimes exit 1 for any uncaught exception or failed startup check. |
| 125 | Docker itself failed: the docker run command or the daemon rejected the request | Read the error printed by docker run / docker compose up in the CI or deploy log, and State.Error. Typical messages: unknown flag, image pull denied, port already allocated (then sudo ss -ltnp shows who holds the port). | An application bug. Usually your process never started at all. |
| 126 | The command was found but couldn't be executed | Check the configured command and the file's permissions and format inside the image (commands below). A script on a bind mount keeps the host file's permissions, so a missing execute bit on the host shows up here. | That the file is missing. 126 is "can't execute", 127 is "can't find". |
| 127 | The command couldn't be found | Same command check. Confirm the binary exists in this image and on its PATH; a slimmer base image or a typo in Compose command: is a common cause. A shell inside the container also exits 127 when a command in a script isn't found. | A crash after startup. The program you meant to run never started. |
| 137 | Killed with SIGKILL (128 + 9) | Look at OOMKilled from step 1. true: memory is the lead hypothesis; check the memory limit and kernel log (commands below). false: look for who sent the kill with docker events: docker kill, or a docker stop that waited out its grace period (default 10 s) because the app ignored SIGTERM. | Out of memory. 137 alone is not OOM. Even OOMKilled=true should be matched against timestamps in host memory and kernel logs. More in the exit code 137 guide. |
| 139 | Killed with SIGSEGV (128 + 11), a segmentation fault | Logs around the exit, then the kernel log for a segfault line naming the binary or library. Note any native dependency, base image or runtime upgrade in this deploy. | Where the bug is. It tells you the process touched invalid memory, not which component or why. |
| 143 | Stopped with SIGTERM (128 + 15) | Find out who asked it to stop: a redeploy, docker stop, docker compose down/up recreating it, a host shutdown, or an orchestrator. docker events and your deploy timeline usually answer this. | A crash. 143 is a normal stop request; the real question is who sent it and whether the replacement container came up. |
125, 126 and 127 can come from two places. When Docker couldn't start the process, they are the exit status of the docker run command itself: you see them in the CI or deploy log, the container usually stays Created, and State.Error holds the message (after a 125, the container's own ExitCode in docker inspect may show a different value, such as 128). When a shell or entrypoint script inside a running container hits the same problem, the container itself exits with 126 or 127 and the message is in docker logs.
Codes above 128 generally mean "killed by signal (code − 128)". kill -l 137 in bash prints KILL; try it for any code you don't recognize. Any other number is whatever the application chose to return, so its logs or documentation decide what it means.
Commands referenced in the table
Check what the container runs (codes 0, 126, 127):
docker inspect --format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' YOUR_CONTAINER
docker image inspect --format '{{.Os}}/{{.Architecture}}' YOUR_IMAGE
uname -m
The second and third lines compare the image's platform with the host's (for example linux/arm64 against x86_64). A mismatch usually shows up in the logs as exec format error.
Memory and signals (codes 137, 139, 143). docker events only returns recent events the daemon still holds (the last 256), so run it soon after the incident:
docker inspect --format 'oom={{.State.OOMKilled}} memory_limit_bytes={{.HostConfig.Memory}}' YOUR_CONTAINER
docker events --since 1h --until "$(date +%s)" --filter container=YOUR_CONTAINER
free -h
sudo dmesg -T | grep -i -E 'out of memory|killed process|segfault' | tail -n 20
memory_limit_bytes=0means no container memory limit was set; the host's memory is the limit.In
docker events, akillwithsignal=15followed about 10 seconds later bysignal=9anddiewithexitCode=137is the "ignored SIGTERM, then force-killed on stop" pattern, not an OOM.Kernel lines only count as evidence when their time matches
finishedfrom step 1.dmesg -Tprints host local time, and its conversion can drift on machines that have been suspended.
What these do not tell you: free and docker stats show the present, not the peak at the moment of the kill, and a stopped container usually has no stats at all. Depending on the setup, an OOM kill decided at host level may not set Docker's OOMKilled flag.
3. Read a bounded log window and find the first error
docker logs --since 30m --timestamps YOUR_CONTAINER 2>&1 | head -n 80
docker logs --since 30m --timestamps YOUR_CONTAINER 2>&1 | grep -n -i -E 'error|fatal|panic|exception|denied|not found|refused' | head -n 20
--sincealso accepts a timestamp such as2026-09-29T14:05:00(read as local time unless it ends inZ), so you can start just before the deploy. With Compose,docker compose logs --since 30m --timestamps SERVICEdoes the same per service.2>&1matters:docker logspasses the container's stderr through as stderr, and most errors live there, so without itheadandgrepmiss them.Read the first error after the deploy, not only the last line. In a restart loop the same failure repeats, and the lines just before the first one usually carry the real clue.
Empty output doesn't clear the app. It may log to a file inside the container, or the logging driver may not make logs available to
docker logs.
First errors that come up often (each is still a hypothesis until you check it):
connection refused/could not connect: a dependency isn't running or the address is wrong. Inside a container,localhostis the container itself, not another service.permission denied: file ownership on mounts, or the image's runtime user.no such file or directoryfor a script that clearly exists: often Windows (CRLF) line endings on the script's first line, or an interpreter the image doesn't have.exec format error: the image was built for a different CPU architecture (see the platform check above).A missing environment variable or config key: compare key names with the last working deploy. Don't print secret values.
Before you paste logs into a chat, a ticket or an AI prompt, redact passwords, tokens, connection strings, customer emails and internal addresses.
Doing this in OpsMate
OpsMate puts an SSH terminal and an AI assistant on the same server page. You can type the docker ps, docker inspect and docker logs commands above yourself, which is usually fastest for the exit code. You can also describe the problem in plain language, for example "find out why YOUR_CONTAINER exits after the deploy and summarize the first errors in its logs". The AI proposes troubleshooting commands, and beyond logs it can use docker, journalctl, ps, df and ss for checks. After a command has run, click Analyze to get a conclusion; the commands and output stay in the terminal so you can check it against the raw lines. When you click Analyze, the command output is sent to cloud AI for analysis; what the desktop app keeps on your own machine by default is your SSH credentials. If the logs contain customer data or secrets, run the commands yourself and share only a redacted excerpt.
Boundaries
An exit code is a clue, not a diagnosis. The same number has several possible causes, especially 1 and 137. Base a conclusion on the code, the log lines and matching timestamps together.
The commands in this guide don't change your containers. The AI is mainly for troubleshooting: dangerous commands are blocked. Low-risk fixes such as restarting a service or rotating logs run automatically by default. Rolling back the image, editing Compose files, changing restart policies or raising memory limits are changes you decide on, with a way to roll back. For what OpsMate does and doesn't do on its own, see the FAQ.
An AI summary is a starting point. Check it against the inspect output and the log timestamps.
If you can't SSH into the host, OpsMate can't reach it either. Start with your cloud provider's console.
Illustrative example
Illustrative example (not a real customer case): after switching the image to a slimmer base, docker ps -a shows Restarting (127) 4 seconds ago. docker inspect prints 127 false with an empty error, so Docker started the process and it's not a memory kill. started and finished are about a second apart. The first line of docker logs --since 15m --timestamps reads /app/start.sh: 5: exec: gunicorn: not found: the entrypoint script runs, but the new base image doesn't include the app server. Changing the restart policy or adding memory would not help. The next step is a decision for a person: roll back to the previous image tag, or rebuild with the dependency installed, and then watch whether the container stays up.
AI diagnostic prompt
A container on this server exits a few seconds after deploy. Without changing anything, get its State.ExitCode, OOMKilled, Error, StartedAt, FinishedAt and restart count, summarize the first errors in its logs since the deploy with timestamps, and tell me what that exit code usually means and what it does not prove. Separate confirmed facts from hypotheses and list what is missing. Do not restart or remove containers, change restart policies, edit Compose files or change memory limits. If a change is needed, propose the smallest step with its risks, how to verify it and how to roll it back, and wait for my approval.
Next checks
Container stuck in
Restarting? Docker container keeps restarting covers restart policies, log windows and memory versus other kills.Exit code 137 or
OOMKilled=true? Container exited with code 137: out of memory or something else?Logs mention a full disk? Docker logs filling your disk.
Want the terminal and AI side by side? AI and SSH commands in one workspace.
Try OpsMate
500 free AI calls per month and unlimited servers. The desktop app keeps your SSH credentials on your own machine by default.
Need help interpreting the evidence?
OpsMate helps developers and operators investigate with AI. Review the evidence. After you click Analyze, the command output is sent to cloud AI for analysis; redact sensitive information first.