Write a Docker Entrypoint Script for Django

Write a robust docker-entrypoint.sh for Django: wait for dependencies, optionally run migrations and collectstatic behind flags, support web, worker, and one-off commands, exec the final process for signals, and keep secrets out of logs.

Problem statement

A Django image usually runs several roles: the web server, Celery workers, Beat, and one-off management commands. An entrypoint script is where container startup logic lives: waiting for the database, deciding whether to run migrations, and launching the right process. Done badly, it runs migrations in every replica at once, swallows signals so containers take ten seconds to stop, prints secrets into logs, or makes docker run image python manage.py shell impossible. A small, disciplined script avoids all of that.

A good Django entrypointContainer startsENTRYPOINT scriptPreparewait for DB, flagsOptional stepsmigrate if askedexec "$@"web, worker, or cmdThe script prepares the environment, then replaces itself with whatever command was given.
  1. Use #!/bin/sh and set -eu; keep the script short and POSIX.
  2. Optionally wait for the database with a bounded retry loop.
  3. Run migrations only when an environment flag asks for it (RUN_MIGRATIONS=1), ideally in a single dedicated container or Job.
  4. Do not run collectstatic at startup; do it at image build time.
  5. End with exec "$@" so the command (gunicorn, celery, python manage.py ...) becomes PID 1 and receives signals.
  6. Set ENTRYPOINT ["/app/docker-entrypoint.sh"] and the default CMD to the web server.

Step-by-step solution

1. The script

#!/bin/sh
# /app/docker-entrypoint.sh
set -eu

wait_for_db() {
  python - <<'PY'
import os, sys, time
import psycopg
url = os.environ.get("DATABASE_URL")
if not url:
    sys.exit(0)
for i in range(30):
    try:
        psycopg.connect(url, connect_timeout=3).close()
        sys.exit(0)
    except psycopg.OperationalError:
        print(f"waiting for database ({i + 1}/30)", flush=True)
        time.sleep(2)
sys.exit("database not reachable")
PY
}

if [ "${WAIT_FOR_DB:-1}" = "1" ]; then
  wait_for_db
fi

if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then
  python manage.py migrate --noinput
fi

exec "$@"

The database wait prints attempt numbers, never the URL, so credentials do not land in logs.

2. Wire it into the Dockerfile

COPY --chmod=755 docker-entrypoint.sh /app/docker-entrypoint.sh
RUN SECRET_KEY=build-only python manage.py collectstatic --noinput
USER app
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["gunicorn", "-c", "gunicorn.conf.py", "config.wsgi:application"]

With ENTRYPOINT plus CMD, the container runs docker-entrypoint.sh gunicorn .... Any command given at runtime replaces CMD:

docker run myapp celery -A config worker -l info
docker run -it myapp python manage.py shell
same image, different commandsCommand (CMD)Flagswebgunicorn -c gunicorn.conf.py ...RUN_MIGRATIONS=0workercelery -A config workerRUN_MIGRATIONS=0beatcelery -A config beatone replica onlymigrate✓python manage.py migrateone-off, once per releaseshellpython manage.py shellinteractive, -itThe entrypoint prepares; the command decides the roledjango-deployment.com
Building one image for every role guarantees they all run the same code.

3. Migrations: once, not per replica

Running migrate in every web container start means three replicas race to apply the same migrations. Django takes no global lock, so concurrent runs can fail or deadlock. Prefer a separate one-off step per release (a Compose migrate service, a Kubernetes Job, a release phase). The RUN_MIGRATIONS flag lets single-container setups, such as a small VPS with one web container, opt in.

4. Why exec matters

Without exec, the shell stays as PID 1 and Gunicorn is its child. docker stop sends SIGTERM to PID 1; sh does not forward it, so Docker waits ten seconds and then kills everything with SIGKILL, cutting off in-flight requests. With exec, Gunicorn is PID 1, receives SIGTERM, and shuts down gracefully.

5. What not to put in the entrypoint

  • collectstatic at every start: slow, and wrong for read-only filesystems. Do it in the image build.
  • createsuperuser with default passwords.
  • pip install or anything that downloads at runtime.
  • Printing the environment (env, set -x with secrets present).
entrypoint responsibilitiesBelongs here✓bounded wait for dependencies✓opt-in migrate behind a flag✓exec "$@" as the last lineDoes not✕collectstatic on every start✕migrate in every replica by default✕set -x or env dumps with secretsKeep startup fast, predictable, and quiet about secretsdjango-deployment.com
Anything slow or one-off belongs in the build or a separate release step.

Verification check: docker run --rm myapp python -c "print('ok')" prints ok (commands pass through), docker stop on a web container returns in about a second (signals reach Gunicorn), and the logs show no connection strings.

Explanation

Docker concatenates ENTRYPOINT and CMD into the process it starts. An entrypoint script that ends with exec "$@" acts as a wrapper: it performs setup in a shell, then replaces itself with the requested command, keeping process ID 1 and its signal handling. This lets one image serve every role with predictable startup, while release-level tasks such as migrations stay explicit and run once.

Edge cases / notes

  • Line endings and permissions cause "no such file or directory" and "permission denied"; see the entrypoint fix guide.
  • tini or --init add an init process that reaps zombies; useful if your app spawns subprocesses.
  • Kubernetes can use command/args overrides; the entrypoint still runs unless command replaces it.
  • Distroless images have no shell; implement the wait and flags in a small Python entrypoint instead.

Next steps

FAQ

Should my Django entrypoint run migrations?

Only behind a flag for single-container setups. With several replicas, run migrations once as a separate step.

Why end the entrypoint with exec "$@"?

So the final command becomes PID 1 and receives stop signals, allowing graceful shutdown.

Where should collectstatic run?

At image build time, so containers start fast and can use read-only filesystems.

How do I run a shell with an entrypoint image?

docker run -it myapp python manage.py shell; the entrypoint prepares and then executes your command.

Learning path · step 2 of 12

Docker, from image to production

Keep going