Dockerize a Django Application

Khimananda Oli 8 min read Programming and Languages
Dockerize a Django Application

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.

Nginx ContainerPort 80/443Static FilesTLS TerminationDjango + GunicornWSGI ServerApp LogicWorkers: 2-4PostgreSQLPort 5432Persistent VolumeData Layerproxy_passTCP/IP
Production architecture when you Dockerize a Django application: Nginx handles static files and TLS, Gunicorn runs the WSGI app, PostgreSQL persists data on a volume.

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.

Container StartENTRYPOINT triggeredWait for DBcurl poll loopRun Migrationsmanage.py migrateExec GunicornWhy This Order Matters• DB must be reachable before migrations run• Migrations must complete before workers serve traffic• exec replaces shell → signals reach Gunicorn directly• Graceful shutdown prevents dropped requests• Health checks pass only after Gunicorn binds port• Failed migration = container exits (fail-fast)• Logs stream to stdout for container orchestrators
Startup sequence when you Dockerize a Django application: the entrypoint script enforces correct ordering so migrations run before Gunicorn accepts traffic.

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.

StrategyBest ForComplexityPerformance
Nginx sidecar containerSelf-hosted VPS, on-premMediumHigh
S3/GCS + CloudFront/CDNCloud-native, global audienceLow (with django-storages)Highest
WhiteNoise middlewareSmall apps, simple deploysLowestModerate

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-compile or poetry export to generate a fully pinned requirements.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:

  1. Use python:3.12-slim instead of python:3.12 (saves ~700MB).
  2. Combine RUN commands to reduce layers (each layer adds metadata overhead).
  3. Remove apt caches in the same RUN statement that installs packages.
  4. Use --no-cache-dir on every pip install command.
  5. Audit installed packages with pip list in the final image; remove anything unnecessary.

A well-optimized Django production image should be 180–280MB. If yours exceeds 400MB, something is wrong.

Naive Single-Stage Buildpython:3.12 (full) — 980 MB basegcc + libpq-dev + pip cache — 320 MBApp code + .pyc + .git — 85 MBDev/test dependencies included — 140 MBTotal: ~1.5 GBOptimized Multi-Stage Buildpython:3.12-slim runtime — 150 MBPinned deps only (no cache) — 65 MBApp code (no .git, no .pyc) — 12 MBlibpq5 runtime only — 8 MBTotal: ~235 MB84% smaller
Image size comparison: optimizing your build when you Dockerize a Django application reduces images from 1.5GB to under 250MB.

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.

Frequently Asked Questions

Use python:3.13-slim-bookworm for production. It includes essential Debian libraries while keeping the image size under 200MB, reducing attack surface and pull times compared to full or alpine variants that often require compiling C extensions.

Never bake secrets into images. Pass them at runtime using docker compose env_file or Kubernetes secrets. Configure django-environ in settings.py to read these variables, ensuring configuration remains externalized across development, staging, and production environments.

Containers start simultaneously without dependency guarantees. Add a healthcheck to your database service and use wait-for-it.sh or dockerize in your Django entrypoint script. This ensures the database accepts TCP connections before Gunicorn attempts initialization.

Yes. Never use manage.py runserver in production containers. Gunicorn with uvicorn workers handles async ASGI traffic efficiently, providing process management and signal handling required for graceful shutdowns during deployments and scaling events.

Run collectstatic during the build phase, not at runtime. Mount a shared volume or push assets to S3/CloudFront. Nginx should serve these files directly via a reverse proxy sidecar, bypassing Python entirely for better performance.

Create a non-root user named django with UID 1000. Set ownership of /app and media directories to this user. Running as root violates security best practices and causes permission errors when mounting host volumes on Linux systems.

Copy requirements.txt and install dependencies before copying application code. This separates infrequent dependency installs from frequent code changes, allowing Docker to reuse cached layers and reducing rebuild times from minutes to seconds during development.

Yes. Execute docker exec -it bash to access the shell. For pdb debugging, attach to the specific Gunicorn worker process or run management commands directly inside the container using docker compose run instead of exec.

Run migrations as a separate one-off task using docker compose run web python manage.py migrate. Do not place migrations in the entrypoint script, as concurrent container starts can cause race conditions and failed schema updates.

Expose internal port 8000. Map it to host port 80 or 443 only in development. Production setups should terminate TLS at a reverse proxy like Traefik or Nginx, forwarding plain HTTP traffic to the container backend.

Under 300MB.

No. Build the same image for web and worker services. Override the command in docker-compose.yml to run celery -A project worker instead of Gunicorn, ensuring identical dependencies and eliminating version drift between components.

Create a comprehensive .dockerignore file excluding .git, venv, pycache, and local settings. Without this, COPY instructions include unnecessary files, increasing image size and potentially leaking credentials or proprietary source code into distributed artifacts.

Highly recommended. Compile system dependencies and wheels in a builder stage, then copy only installed packages to the final slim runtime image. This removes compilers and headers, significantly reducing CVE exposure and final artifact size.

Run hadolint for static analysis and trivy for vulnerability scanning. Validate functionality by executing the test suite inside the built image using docker compose run web pytest before pushing to any container registry.