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
# 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"].git
__pycache__/
*.pyc
.venv/
.env
media/
staticfiles/
db.sqlite3
Dockerfile
.dockerignoreBefore you build
- Name your project module config so config.wsgi and config.settings resolve, or adjust both references.
- Set STATIC_ROOT in settings so collectstatic knows where to write files.
- Read SECRET_KEY from the environment instead of hard-coding it.
- Add gunicorn to requirements.txt.
- Serve the collected static files with your reverse proxy or with WhiteNoise.
How this Dockerfile works
FROM python:3.13-slim AS builderA disposable stage for installing packages. Anything pip leaves behind here never reaches the image you deploy.
RUN pip install --no-cache-dir --prefix=/install -r requirements.txtInstalls dependencies into an isolated prefix without keeping pip's cache, so the runtime stage can copy one clean directory.
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=config.settingsTurns off .pyc files, makes logs stream immediately, and tells Django and Gunicorn which settings module to load without extra flags.
COPY --from=builder /install /usr/localPlaces the installed packages where the system Python already looks, so imports work exactly as they did in the builder stage.
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.
&& useradd --create-home --uid 10001 app USER appCreates an unprivileged user in the same layer and switches to it, so Gunicorn and your code never run as root.
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
docker build -t django-app .
docker run --rm -p 8000:8000 -e SECRET_KEY=change-me django-appThe 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Python slim + Gunicorn (multi-stage) | python:3.13-slim | 155 MB | Passing |
| Python slim + Uvicorn (ASGI) | python:3.13-slim | 155 MB | Passing |
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.