← Back to all posts

Docker Container Exits Immediately: Read the Process Before Restarting

Compare a completed job, a deliberate error, and a backgrounded HTTP server to diagnose a stopped Docker container before changing its restart policy.

A blue Docker whale carrying rounded shipping containers

A container that exits immediately has not necessarily crashed. It may have completed a one-shot command, rejected its configuration, or lost the foreground process you expected to keep it alive. Read the command and its final state before adding a restart policy.

The experiment below makes those cases distinguishable. It was tested with Docker Engine 29.5.2 in a local Linux VM, using ubuntu:22.04 and python:3.14-alpine. These image tags can change; record the image identity if you need to reproduce an exact result.

Inspect the stopped instance you actually ran

For your existing container, substitute its name for your-container:

docker ps -a --filter name=your-container
docker logs --tail 50 your-container
docker inspect your-container --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}}'
docker inspect your-container --format \
  'path={{.Path}} args={{json .Args}}'

docker ps without -a omits stopped containers. The logs command reads available process output; it does not execute the application again. An empty log is therefore an observation, not proof that startup succeeded.

Inspection connects the exit status to the actual process and arguments. Avoid diagnosing a different instance created by a later docker run. Docker’s run reference, inspection command, and logs command define these interfaces.

Do not use --rm for the failing examples below: it removes the stopped instance you need to inspect. The names are for this disposable exercise; choose unused names if they already exist on your machine.

Compare normal completion with an explicit error

A finite command can succeed and still leave a stopped container:

docker run --name blog-exit-ok ubuntu:22.04 \
  sh -c 'printf "job complete\n"'
docker inspect blog-exit-ok --format \
  'status={{.State.Status}} exit={{.State.ExitCode}}'

The output includes job complete, followed by status=exited exit=0. There is no missing server to restart: the configured job finished.

Now make the process reject a deliberately simulated configuration condition:

docker run --name blog-exit-error ubuntu:22.04 \
  sh -c 'printf "missing configuration\n" >&2; exit 7'
docker logs blog-exit-error
docker inspect blog-exit-error --format \
  'status={{.State.Status}} exit={{.State.ExitCode}}'

The run command returns status 7. The stored log explains the deliberately chosen error, and inspection shows exit=7. In this fixture, 7 means what our script defined; it is not a universal Docker code for missing configuration.

A real application needs its own error interpretation. Check the failing application’s documentation and the surrounding log, not a list that assigns one cause to every nonzero number. If your docker run fails before the process executes, distinguish the Docker invocation error from an application’s recorded exit status.

Backgrounding the service can end the container

This starts a server as a shell background job and then explicitly exits the shell:

docker run --name blog-background-server python:3.14-alpine \
  sh -c 'python -m http.server 8000 --bind 0.0.0.0 & exit 0'
docker inspect blog-background-server --format \
  'status={{.State.Status}} exit={{.State.ExitCode}}'

The container exits with status 0 even though the command text mentions a server. The main shell has ended. A background child is not a replacement for the container’s main process.

Run the actual server as the foreground command instead:

docker run -d --name blog-foreground-server \
  -p 127.0.0.1:18080:8000 \
  python:3.14-alpine \
  python -m http.server 8000 --bind 0.0.0.0
curl --retry 5 --retry-all-errors --retry-delay 1 -fsS \
  http://127.0.0.1:18080/
docker inspect blog-foreground-server --format \
  'status={{.State.Status}} path={{.Path}} args={{json .Args}}'

The response is a directory listing, and inspection identifies python as the running command. Docker’s -d detaches the client; it does not tell the application to daemonize itself. For an entrypoint shell script that completes setup before launching a service, exec can replace the shell with that service instead of leaving an unnecessary wrapper.

The host mapping is restricted to loopback. The server listens on the container’s interfaces so the published connection can reach it. If the container runs but the response fails, use the EXPOSE versus publish experiment; that is a different diagnosis from a process that already exited.

Python’s http.server is a small demonstration server, not a production deployment recommendation. Startup can race the first request, which is why this local check uses bounded retries.

Change the cause before the restart behavior

Evidence Next investigation
Exit 0 and the finite task’s expected result exists Confirm that completion is intended
Nonzero exit plus an application error Correct the named configuration, input, or dependency
Exit 0 with a shell that backgrounds a service Run the intended service in the foreground
OOMKilled=true Inspect memory use, configured limits, and host evidence
Empty logs plus a state error Inspect the launch command and Docker’s error
Running container with no useful response Check application binding, ports, readiness, and request path

An exit code alone does not establish an out-of-memory kill, and a running process alone does not establish a healthy service. Keep those judgments separate.

Restart policies define what Docker does after a stop or failure. They do not supply a missing variable or turn a completed batch job into a server. Likewise, adding tail -f /dev/null can keep a process alive without proving that the intended application is functioning.

Remove only the named exercise resources when finished:

docker rm blog-exit-ok blog-exit-error blog-background-server
docker stop blog-foreground-server
docker rm blog-foreground-server

Keep the images if you will repeat the experiment. Before restarting a real stopped instance, state which command should remain alive, which output proves its task, and what evidence would distinguish normal completion from failure.

References