
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Dockerizing a Django application eliminates environment drift and ensures your code runs identically from local development to cloud production. Many teams struggle when they first containerize web frameworks, often creating bloated images or misconfiguring static files. To properly Dockerize a Django application, you need a multi-stage build, a production-grade WSGI server like Gunicorn, and a strategy for serving static assets separately from your Python process.
How do you write a production Dockerfile to Dockerize a Django application?
The most common mistake when teams Dockerize a Django application is using a single-stage build that includes compilers, headers, and pip caches in the final image. This produces images over 1GB and expands your attack surface. A multi-stage Dockerfile solves both problems by separating build-time dependencies from runtime artifacts.
Multi-stage Dockerfile for Django
This Dockerfile targets Python 3.12 slim, uses a builder stage for dependency installation, and produces a final image under 250MB. It assumes your project has a requirements.txt at the root and a standard Django project structure.
# Stage 1: Builder
FROM python:3.12-slim AS builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
DJANGO_SETTINGS_MODULE=myproject.settings.production
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 curl && \
rm -rf /var/lib/apt/lists/* && \
addgroup --system django && \
adduser --system --ingroup django django
COPY --from=builder /install /usr/local
WORKDIR /app
COPY --chown=django:django . .
RUN python manage.py collectstatic --noinput || true
USER django
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"] Key decisions in this Dockerfile deserve explanation. The --prefix=/install flag in the builder stage isolates installed packages so we can copy them cleanly without dragging in pip itself. Setting PYTHONDONTWRITEBYTECODE=1 prevents .pyc files from being written inside the container, which avoids permission issues and keeps the filesystem clean. The collectstatic step runs at build time so static files are baked into the image and ready for Nginx to serve immediately on startup.
Why non-root users matter
Running containers as root is a security risk I flag in every audit. If an attacker exploits your Django app, root access inside the container can escalate to host-level compromise depending on your runtime configuration. Creating a dedicated django user and switching to it before the CMD instruction limits blast radius. This is non-negotiable for SOC 2 compliance and aligns with CIS Docker Benchmark recommendations.
How do you configure Gunicorn and entrypoint scripts for Django containers?
Gunicorn is the standard WSGI server when you Dockerize a Django application because Django's built-in development server is single-threaded and insecure. However, bare Gunicorn commands in your Dockerfile make it hard to run migrations, wait for databases, or execute management commands before starting workers.
Entrypoint script pattern
Create an entrypoint.sh script that handles pre-start tasks deterministically:
#!/bin/bash
set -e
echo "Waiting for PostgreSQL..."
while ! curl -s "postgresql:5432" > /dev/null; do
sleep 1
done
echo "PostgreSQL is ready."
echo "Running database migrations..."
python manage.py migrate --noinput
echo "Starting Gunicorn..."
exec gunicorn myproject.wsgi:application \
--bind 0.0.0.0:8000 \
--workers "${GUNICORN_WORKERS:-3}" \
--threads "${GUNICORN_THREADS:-2}" \
--timeout 120 \
--access-logfile - \
--error-logfile - Update your Dockerfile's final lines to use this script:
COPY --chmod=755 entrypoint.sh /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"] The exec command replaces the shell process with Gunicorn, ensuring signals like SIGTERM reach Gunicorn directly for graceful shutdowns. Without exec, Gunicorn runs as a child process and won't respond to Docker stop commands properly — a frequent cause of hanging deploys and failed health checks.
Tuning Gunicorn workers
A common formula is (2 × CPU cores) + 1 workers. In containers, CPU limits are set via cgroups, so check your actual allocation rather than host core count. For a 2-core container, start with 3–5 workers. Use threads (--threads 2) if your workload is I/O-bound (API calls, database queries) to improve throughput without multiplying memory usage. Monitor with Prometheus metrics to validate worker saturation before adjusting.
How do you handle static files and media when you Dockerize a Django application?
Django was never designed to serve static files efficiently in production. When you Dockerize a Django application, you have three viable strategies for static and media assets, each with distinct trade-offs.
| Strategy | Best For | Complexity | Performance |
|---|---|---|---|
| Nginx sidecar container | Self-hosted VPS, on-prem | Medium | High |
| S3/GCS + CloudFront/CDN | Cloud-native, global audience | Low (with django-storages) | Highest |
| WhiteNoise middleware | Small apps, simple deploys | Lowest | Moderate |
Nginx sidecar with shared volume
For teams hosting on a VPS or Nepal-based infrastructure where CDN costs are prohibitive, mount a named volume between your Django and Nginx containers:
# docker-compose.yml snippet
services:
django:
build: .
volumes:
- static_volume:/app/staticfiles
# ...
nginx:
image: nginx:1.27-alpine
volumes:
- static_volume:/usr/share/nginx/html/static:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "80:80"
depends_on:
- django
volumes:
static_volume: Your Nginx config should serve /static/ directly and proxy everything else to Gunicorn. Set STATIC_ROOT = "/app/staticfiles" in Django settings and ensure collectstatic runs during the image build. Media files (user uploads) require a separate writable volume or object storage — never store them in the container filesystem.
Object storage for cloud deployments
If you're deploying to AWS, Azure, or GCP, use django-storages with S3-compatible backends. This removes static file concerns from your Docker image entirely. Configure DEFAULT_FILE_STORAGE and STATICFILES_STORAGE in your production settings, and your container becomes stateless — critical for horizontal scaling and zero-downtime deploys.
What are the security and optimization best practices when you Dockerize a Django application?
Security isn't optional when you Dockerize a Django application for production. These practices come from real audit preparation and incident response experience.
- Pin all dependency versions. Use
pip-compileorpoetry exportto generate a fully pinnedrequirements.txt. Unpinned dependencies are a supply chain risk and cause irreproducible builds. - Scan images before deployment. Integrate Trivy or Grype into your CI pipeline. Fail builds on critical CVEs. See container image scanning with Trivy for setup details.
- Never embed secrets in images. Use Docker secrets, Kubernetes Secrets, or external vaults. Environment variables injected at runtime are acceptable; hardcoded values in Dockerfiles are not.
- Use .dockerignore aggressively. Exclude
.git,__pycache__,.env,node_modules, test directories, and documentation. Every excluded file reduces build context size and prevents accidental secret leakage. - Set resource limits. Always define CPU and memory limits in Compose or Kubernetes. Unbounded containers cause noisy-neighbor issues and OOM kills. Reference Kubernetes resource limits for right-sizing guidance.
- Enable structured logging. Configure Django's LOGGING dict to output JSON to stdout. Containers should never log to files. Pair with structured logging best practices for parseable, searchable output.
Optimizing image size further
Beyond multi-stage builds, consider these reductions:
- Use
python:3.12-sliminstead ofpython:3.12(saves ~700MB). - Combine RUN commands to reduce layers (each layer adds metadata overhead).
- Remove
aptcaches in the same RUN statement that installs packages. - Use
--no-cache-diron every pip install command. - Audit installed packages with
pip listin the final image; remove anything unnecessary.
A well-optimized Django production image should be 180–280MB. If yours exceeds 400MB, something is wrong.
Deploy Your Dockerized Django Application With Confidence
When you Dockerize a Django application correctly, you gain reproducible deployments, faster CI pipelines, and infrastructure that passes security audits without last-minute scrambles. Start with the multi-stage Dockerfile above, add the entrypoint script, choose your static file strategy based on your hosting environment, and integrate image scanning into your pipeline before your first production deploy. If you need help designing a containerized Django architecture that meets compliance requirements or scales reliably, reach out to discuss your project.