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
Quick answer
- Read the exit code:
docker compose ps -aordocker inspect --format '{{.State.ExitCode}}' <container>. - Read the logs from the failed run:
docker compose logs --tail=100 web. - Exit code
0: the command completed; make sure the final command runs the server in the foreground and the entrypoint ends withexec "$@". - Exit code
1with a traceback: fix the Python error (settings, imports, environment variables). - Exit code
126or127: the entrypoint or command is not executable or not found (permissions, Windows line endings, wrong path). - Exit code
137: killed, usually for memory. - Reproduce interactively with
docker compose run --rm --entrypoint sh weband 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
| Exit code | Meaning |
|---|---|
0 | process finished without error |
1 | general error, typically an unhandled Python exception |
2 | often misuse of a shell command or invalid arguments |
126 | command found but not executable |
127 | command not found |
137 | killed with SIGKILL (128 + 9), often the OOM killer or docker kill |
143 | terminated 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
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 -estops the script if migrations fail, instead of starting a broken app.exec "$@"replaces the shell with theCMDprocess, 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 aspython 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
Causes:
- Not executable: add
RUN chmod +x /app/docker-entrypoint.sh, or set the mode withCOPY --chmod=755. - Windows line endings: a script edited on Windows has
\r\nline 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 withsed -i 's/\r$//' docker-entrypoint.shand add a.gitattributesrule such as*.sh text eol=lf. - Missing interpreter: a script starting with
#!/bin/bashin an image without bash (for example Alpine). Use#!/bin/shor 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
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(andhealthyif 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_onwithout health checks for database readiness. - Committing shell scripts with Windows line endings.
- Using the Compose
.envfile and expecting its values inside containers.
Next steps
- Build a correct image with How to Dockerize a Django App for Production.
- Fix database connectivity with Fix Django Cannot Connect to Postgres in Docker Compose.
- Once running, fix assets with Fix Django Static Files 404 in Docker.
- Review the whole setup with the Django Docker Production Checklist.
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.