Troubleshooting
Docker & Compose

Fix a Django Docker Container That Exits Immediately

Fix Django containers that exit or restart-loop on start: exit codes and logs, CMD and entrypoint mistakes, missing env vars, and database readiness.

Problem statement

docker compose up -d reports the Django container as started, but docker compose ps shows it as Exited (1) or Restarting, and the site is unreachable. Or the container runs for a few seconds and stops with Exited (0), which looks successful but leaves nothing serving requests.

A container runs as long as its main process runs. When the main process (PID 1 in the container) exits, the container stops. For a Django web container, that main process should be Gunicorn (or another app server) running in the foreground. Containers exit immediately when:

  • the process crashes on startup (import error, missing environment variable, database unreachable)
  • the command finishes normally (an entrypoint script that runs migrations and exits without starting the server)
  • the process daemonizes into the background, leaving PID 1 to exit
  • the command does not exist or is not executable
Container lifetimedocker runstarts PID 1Entrypointsetup, exec "$@"GunicornforegroundExit codetells you whyThe container lives exactly as long as its main process; exit 0 means it finished.

Quick answer

  1. Read the exit code: docker compose ps -a or docker inspect --format '{{.State.ExitCode}}' <container>.
  2. Read the logs from the failed run: docker compose logs --tail=100 web.
  3. Exit code 0: the command completed; make sure the final command runs the server in the foreground and the entrypoint ends with exec "$@".
  4. Exit code 1 with a traceback: fix the Python error (settings, imports, environment variables).
  5. Exit code 126 or 127: the entrypoint or command is not executable or not found (permissions, Windows line endings, wrong path).
  6. Exit code 137: killed, usually for memory.
  7. Reproduce interactively with docker compose run --rm --entrypoint sh web and run the command by hand.

Step-by-step solution

1. Read the exit code

docker compose ps -a
docker inspect --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' myapp-web-1
the exit code narrows it downMeaningLook at0command finished normallyforeground process1✕Python raised at start-updocker logs traceback126entrypoint not executablechmod, shebang127command not foundPATH, CRLF endings137!killed: often out of memoryOOMKilled flagdocker inspect shows ExitCode and OOMKilled togetherdjango-deployment.com
A web server should never exit on its own, so even 0 is a bug here.
Exit codeMeaning
0process finished without error
1general error, typically an unhandled Python exception
2often misuse of a shell command or invalid arguments
126command found but not executable
127command not found
137killed with SIGKILL (128 + 9), often the OOM killer or docker kill
143terminated with SIGTERM (128 + 15), normal on docker stop

2. Read the logs

docker compose logs --tail=100 web
docker logs --tail=100 myapp-web-1

Logs from previous attempts of a restarting container are included. If the output is empty, the process may have exited before writing anything, or output is buffered. Set PYTHONUNBUFFERED=1 in the image so Python writes logs immediately.

3. Exit code 0: the server is not running in the foreground

A typical broken entrypoint:

#!/bin/sh
python manage.py migrate --noinput
python manage.py collectstatic --noinput
the main process must keep runningExits with code 0✕gunicorn --daemon✕script ends after migrate✕CMD runs a setup command onlyStays up✓gunicorn in the foreground✓entrypoint ends with exec "$@"✓logs to stdoutWhen the main process ends, the container endsdjango-deployment.com
Docker is the supervisor; nothing inside should daemonise.

It runs two commands and exits, so the container stops. Fix the pattern so the script hands over to the container command:

#!/bin/sh
set -e

python manage.py migrate --noinput

exec "$@"
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
  • set -e stops the script if migrations fail, instead of starting a broken app.
  • exec "$@" replaces the shell with the CMD process, so Gunicorn becomes PID 1 and receives stop signals directly.

Other exit-0 causes:

  • gunicorn --daemon (never daemonize in containers)
  • a command: override in Compose that runs a one-off task such as python manage.py check
  • a process manager configured to run in the background

Running migrations in the web container's entrypoint is simple for single-container deployments. With multiple replicas, run migrations as a separate one-off step instead.

4. Exit code 1: Python errors at startup

Read the traceback's last lines. Frequent causes in containers:

Missing environment variables:

KeyError: 'DJANGO_SECRET_KEY'

Check what the container receives:

docker compose config | sed -n '/web:/,/^  [a-z]/p' | grep -A30 environment
docker compose run --rm web env | cut -d= -f1 | sort

The second command lists variable names without values. Remember that Compose's .env file in the project directory is for variable substitution in compose.yaml, not automatically passed into containers; use env_file: or environment: for that.

Module import errors:

ModuleNotFoundError: No module named 'config'

WORKDIR is not the directory containing manage.py, or the code was not copied into the image. Check with docker compose run --rm web sh -c 'pwd; ls'.

Database not reachable at startup:

django.db.utils.OperationalError: connection to server at "db" (172.18.0.2), port 5432 failed: Connection refused

The database container is starting but not ready yet. depends_on alone only orders startup. Use a health check:

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 3s
      retries: 10
  web:
    depends_on:
      db:
        condition: service_healthy

The doubled $$ escapes the dollar sign so Compose passes $POSTGRES_USER to the container's shell instead of substituting it from your host environment.

5. Exit codes 126 and 127: entrypoint problems

exec /app/docker-entrypoint.sh: permission denied
exec /app/docker-entrypoint.sh: no such file or directory
reading entrypoint errorsexec /app/entrypoint.sh:permission deniedexec /app/entrypoint.sh:no such file or directory/bin/sh^M: bad interpreter1231126: chmod +x in the Dockerfile.2127: the file exists, but its shebang shell does not.3Windows line endings: convert to LF.A .gitattributes rule stops CRLF endings coming backdjango-deployment.com
Alpine images have /bin/sh but no /bin/bash, a common 127.

Causes:

  • Not executable: add RUN chmod +x /app/docker-entrypoint.sh, or set the mode with COPY --chmod=755.
  • Windows line endings: a script edited on Windows has \r\n line endings, so the kernel looks for an interpreter named /bin/sh\r, which reports "no such file or directory" even though the file exists. Convert with sed -i 's/\r$//' docker-entrypoint.sh and add a .gitattributes rule such as *.sh text eol=lf.
  • Missing interpreter: a script starting with #!/bin/bash in an image without bash (for example Alpine). Use #!/bin/sh or install bash.
  • Wrong path: the script was copied to a different location or overwritten by a bind mount in development Compose files.

6. runserver in containers

If you use runserver for development containers, bind to all interfaces:

python manage.py runserver 0.0.0.0:8000

runserver bound to 127.0.0.1 keeps the container running but unreachable from the host, which looks like a failure even though the container does not exit. For production images, use Gunicorn.

7. Exit code 137: memory

docker inspect --format '{{.State.OOMKilled}}' myapp-web-1
docker stats --no-stream

If OOMKilled is true, the container exceeded its memory limit. Reduce Gunicorn workers or raise the limit.

8. Debug interactively

Override the entrypoint to get a shell in the same image, environment, and network:

docker compose run --rm --entrypoint sh web
get inside before it exits1docker run --rm -it --entrypoint shsame image2Load the same env--env-file .env3python manage.py checkimports and settings4Run the CMD by handsee the real error5Fix, rebuild, rerunrestart policy offA restart loop hides the first error; turn it off while debuggingdjango-deployment.com
docker logs --tail shows the last lines before each crash.

Then run the startup steps one at a time:

python -c "import django; print(django.get_version())"
python manage.py check --deploy
python manage.py migrate --plan
gunicorn config.wsgi:application --bind 0.0.0.0:8000

The first command that fails is your problem, with full output.

9. Restart policies hide the problem

restart: unless-stopped restarts a crashing container forever, filling logs and consuming resources, while the site stays down. Keep the restart policy (it helps with transient failures), but add monitoring for restart counts:

docker inspect --format '{{.RestartCount}}' myapp-web-1

10. Verify

docker compose up -d
sleep 10
docker compose ps
docker compose logs --tail=20 web
curl -sI http://127.0.0.1:8000/ -H "Host: www.example.com" | head -1

Verification check:

  • the web container shows Up (and healthy if a health check is defined)
  • logs show Gunicorn listening and workers booting
  • the restart count does not increase
  • a request returns an HTTP response

Explanation

Containers are process wrappers: their lifetime is their main process's lifetime. Django containers exit immediately because that process either crashed during startup, finished its work, or handed off to something in the background. The exit code tells you which category you are in, the logs tell you why, and an interactive shell in the same image lets you reproduce each startup step without guessing.

Edge cases / notes

Container healthy locally, exits in production

Production often has a read-only filesystem, a non-root user, or missing secrets that local .env files provided. Compare docker compose config output between environments.

Exits only after deploys

If new images exit but old ones did not, diff the Dockerfile and dependencies. A new required environment variable without a matching deploy configuration change is common.

Signals and graceful shutdown

Using exec in the entrypoint lets Gunicorn receive SIGTERM on docker stop. Without it, the shell receives the signal and Docker kills the process group after a timeout.

Common mistakes

  • Entrypoint scripts that do not end with exec "$@".
  • Relying on depends_on without health checks for database readiness.
  • Committing shell scripts with Windows line endings.
  • Using the Compose .env file and expecting its values inside containers.

Next steps

FAQ

Why does my Django Docker container exit with code 0?

The main command finished. Usually the entrypoint runs setup commands and exits without starting Gunicorn in the foreground. End the entrypoint with exec "$@" and set Gunicorn as the CMD.

How do I see why a Docker container exited?

Run docker compose logs web for its output and docker inspect for the exit code and whether it was OOM killed. Then start a shell in the image to reproduce the startup commands.

Why does my entrypoint script say no such file or directory when it exists?

The script likely has Windows line endings or references an interpreter such as bash that the image does not contain. Convert to LF line endings or change the shebang.

How do I make Django wait for Postgres in Docker Compose?

Add a pg_isready health check to the database service and use depends_on with condition: service_healthy on the web service.

Keep going