Django Dockerfile for Production with Gunicorn (Multi-Stage)

This Django Dockerfile builds dependencies in a separate stage, collects static files at build time, and serves the app with Gunicorn on python:3.13-slim as a non-root user. It listens on port 8000 and expects a reverse proxy or WhiteNoise to serve static assets. It is a solid default for most Django projects.

CI status
Build passing
Image size (uncompressed)
155 MB
Build time
11 s
Runtime image
python:3.13-slim
Port
8000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=config.settings
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN SECRET_KEY=build-only python manage.py collectstatic --noinput \
 && useradd --create-home --uid 10001 app
USER app
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
.dockerignore
.git
__pycache__/
*.pyc
.venv/
.env
media/
staticfiles/
db.sqlite3
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM python:3.13-slim AS builder

    A disposable stage for installing packages. Anything pip leaves behind here never reaches the image you deploy.

  2. RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

    Installs dependencies into an isolated prefix without keeping pip's cache, so the runtime stage can copy one clean directory.

  3. ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=config.settings

    Turns off .pyc files, makes logs stream immediately, and tells Django and Gunicorn which settings module to load without extra flags.

  4. COPY --from=builder /install /usr/local

    Places the installed packages where the system Python already looks, so imports work exactly as they did in the builder stage.

  5. RUN SECRET_KEY=build-only python manage.py collectstatic --noinput \

    Settings usually refuse to load without SECRET_KEY, so a throwaway value is passed only for this command. Static files are collected once at build time instead of on every container start.

  6. && useradd --create-home --uid 10001 app USER app

    Creates an unprivileged user in the same layer and switches to it, so Gunicorn and your code never run as root.

  7. CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

    Starts Gunicorn with three worker processes on all interfaces. Tune the worker count to the CPU you give each container.

Build and run

terminal
docker build -t django-app .
docker run --rm -p 8000:8000 -e SECRET_KEY=change-me django-app

The real SECRET_KEY is supplied at runtime, never baked into the image. Add your database URL and ALLOWED_HOSTS the same way with -e flags or an env file, then open http://localhost:8000.

Other Django Dockerfile variants

Gunicorn with synchronous workers is the most widely used way to serve Django and fits typical request/response apps. Projects built around async views or channels would use an ASGI server instead. Those variants appear in the table below once they pass CI.

VariantRuntime imageImage sizeCI
Python slim + Gunicorn (multi-stage)python:3.13-slim155 MBPassing
Python slim + Uvicorn (ASGI)python:3.13-slim155 MBPassing

Troubleshooting

ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting
collectstatic needs a destination. Set STATIC_ROOT in settings, for example to a staticfiles directory inside the project, and rebuild the image.
The build fails during collectstatic because a setting such as DATABASE_URL is missing
collectstatic loads all settings. Give every required environment variable a harmless default during the build, or pass it inline the same way the SECRET_KEY placeholder is.
DisallowedHost error when opening the site
Add the host name you browse to, such as localhost or your domain, to ALLOWED_HOSTS. Read it from an environment variable so each deployment sets its own value.

FAQ

Why run collectstatic at build time?
Static files only change when code changes, which already means a new image. Collecting them during the build keeps startup fast and lets every replica serve identical assets.
How many Gunicorn workers should I use?
The file starts with three. A common rule of thumb is two to four per CPU core, but measure under real load, because memory per worker often becomes the limit first.
Where should migrations run?
Run them as a separate one-off step in your deploy pipeline, using the same image, rather than on container start. That avoids several replicas racing to migrate at once.