Containerizing A Simple Django Application With Docker And Docker Compose

Modern DevOps practices for deploying Django applications with containers - automation, security, and observability built-in.

Docker has transformed how we deploy web applications, and Django is no exception. Containerizing your Django application eliminates the dreaded "works on my machine" problem while providing consistent environments from development through production. This guide covers the essential practices for building secure, maintainable containerized Django deployments that form the foundation of modern DevOps workflows.

Modern DevOps isn't just about packaging code--it's about automation, security, and observability. Containers serve as the atomic unit of deployment, enabling automated pipelines, security isolation, and integrated monitoring. By containerizing Django properly, you gain the ability to deploy confidently, scale predictably, and operate reliably. For a comprehensive approach to automated deployments, see our guide on creating CI/CD pipelines with GitHub Actions.

Container Deployment Benefits

100%

Environment Consistency

10x

Faster Deployments

60%

Reduced Security Incidents

3x

Faster Incident Recovery

Why Containerize Django Applications

The shift from traditional server deployment to containerized deployment represents more than just packaging--it enables a fundamentally different approach to operations.

Consistency Across Environments

Docker eliminates configuration drift by packaging not just your Django application but its entire runtime environment. Every dependency, every library version, every configuration detail is captured in the container image. This means your development environment matches staging, which matches production. No more bugs that only appear in production because of a missing system package or different library version.

Foundation for Automation

Containers are designed for automation. When your Django application runs in containers, you can trigger deployments automatically, roll back instantly with image references, and scale horizontally with container orchestration. Each deployment is reproducible because it's defined by an immutable image tag rather than a series of shell commands.

Security Isolation

Containers provide process isolation at the operating system level. Your Django application runs in its own namespace with controlled access to resources. Combined with proper security configuration, this isolation contains potential vulnerabilities and limits blast radius if something goes wrong.

Preparing Your Django Project for Docker

Before writing Dockerfiles, ensure your Django project follows practices that work well in containerized environments.

Dependency Management

Your requirements file should be comprehensive and deterministic. Pin all dependency versions to ensure the same packages install across every environment. Consider separating development dependencies (testing frameworks, debugging tools) from production requirements.

# requirements.txt - production dependencies
Django>=4.2,<5.0
gunicorn>=21.0,<22.0
psycopg[binary]>=3.1.0
whitenoise>=6.0.0
dj-database-url>=2.0.0
python-dotenv>=1.0.0

# requirements-dev.txt - development only
pytest>=7.0.0
pytest-django>=4.5.0
factory-boy>=3.3.0

Environment Configuration

Follow the twelve-factor app methodology: store configuration in environment variables. This keeps secrets out of your codebase and allows the same image to be configured differently across environments.

# core/settings.py - using python-dotenv
import os
from dotenv import load_dotenv

load_dotenv()

DATABASES = {
 'default': {
 'ENGINE': 'django.db.backends.postgresql',
 'HOST': os.environ.get('DB_HOST', 'localhost'),
 'PORT': os.environ.get('DB_PORT', '5432'),
 'NAME': os.environ.get('DB_NAME', 'django_app'),
 'USER': os.environ.get('DB_USER', 'postgres'),
 'PASSWORD': os.environ.get('DB_PASSWORD', ''),
 }
}

SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')

Building the Dockerfile

The Dockerfile defines your container's contents. Following security best practices from the OWASP Docker Security Cheat Sheet, we'll build an image that's both functional and secure. For professional web development services that implement these patterns, our team ensures containers are production-ready from day one.

Base Image Selection

Choose an appropriate base image and pin its version. The official Python images are well-maintained and audited. For production, slim variants reduce the attack surface significantly.

# Dockerfile
FROM python:3.12-slim-bookworm AS python-base

# Set Python defaults
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Create non-root user (OWASP Rule #2)
RUN groupadd --system --gid 1001 djangoapp && \
 useradd --system --uid 1001 --gid djangoapp djangoapp

# Install dependencies as root, then switch user
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
 pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY --chown=djangoapp:djangoapp . .

# Switch to non-root user
USER djangoapp

# Expose port
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
 CMD python manage.py health_check || exit 1

# Run with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--access-logfile", "-", "--error-logfile", "-", "core.wsgi:application"]

Multi-Stage Builds

For even smaller production images, use multi-stage builds to separate build-time dependencies from runtime:

# Multi-stage build example
FROM python:3.12-slim-bookworm AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheel -r requirements.txt

FROM python:3.12-slim-bookworm AS runtime
WORKDIR /app
RUN apt-get update && apt-get install --no-install-recommends -y \
 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /wheel /wheel
RUN pip install --no-cache-dir --find-links /wheel -r requirements.txt
COPY . .

Configuring Docker Compose

Docker Compose orchestrates multi-container deployments, defining how services communicate and persist data. Our DevOps consulting services help teams implement these patterns at scale, including Kubernetes migration strategies for growing applications.

# docker-compose.yml
version: '3.8'

services:
 web:
 build: .
 ports:
 - "8000:8000"
 environment:
 - DEBUG=1
 - DB_HOST=db
 - DB_NAME=django_app
 - DB_USER=postgres
 - DB_PASSWORD=postgres
 depends_on:
 db:
 condition: service_healthy
 volumes:
 - static_volume:/app/static
 networks:
 - django_network
 security_opt:
 - no-new-privileges:true
 read_only: true
 tmpfs:
 - /tmp

 db:
 image: postgres:16-bookworm
 volumes:
 - postgres_data:/var/lib/postgresql/data
 environment:
 - POSTGRES_DB=django_app
 - POSTGRES_USER=postgres
 - POSTGRES_PASSWORD=postgres
 healthcheck:
 test: ["CMD-SHELL", "pg_isready -U postgres"]
 interval: 10s
 timeout: 5s
 retries: 5
 networks:
 - django_network

volumes:
 postgres_data:
 static_volume:

networks:
 django_network:
 driver: bridge

Security Configuration in Compose

The Docker Compose configuration above applies multiple security layers:

  • no-new-privileges: Prevents privilege escalation through setuid/setgid
  • read_only root filesystem: Containers cannot write to their root filesystem
  • tmpfs mount: Provides writeable space only for temporary files
  • Custom network: Database is isolated on an internal network
  • Health checks: Ensures services are healthy before dependent services start
Security Hardening Checklist

Apply these OWASP-recommended security measures to every Django container

Non-Root User

Always run containers as non-root users. Root in container = root on host if escaped.

Drop All Capabilities

Remove Linux capabilities with --cap-drop all, then add only what's needed.

Resource Limits

Set memory and CPU limits to prevent resource exhaustion attacks.

Read-Only Filesystem

Run containers with --read-only unless they absolutely need to write.

No New Privileges

Prevent processes from gaining additional privileges via setuid/setgid.

Secrets Management

Never bake secrets into images. Use environment variables or secrets management.

Health Checks and Monitoring

Health checks are essential for production deployments. They enable load balancers to detect unhealthy instances and orchestrators to restart failed containers. Implementing comprehensive monitoring is a core part of our DevOps automation services, ensuring production systems remain observable and recoverable.

Django Health Check Endpoint

Create a simple health check endpoint that verifies database connectivity and critical services:

# core/management/commands/health_check.py
from django.core.management.base import BaseCommand
from django.db import connection
import sys

class Command(BaseCommand):
 help = 'Check if the application is healthy'

 def handle(self, *args, **options):
 checks = {'database': False, 'cache': False}
 
 # Check database
 try:
 with connection.cursor() as cursor:
 cursor.execute('SELECT 1')
 checks['database'] = True
 except Exception as e:
 self.stderr.write(f'Database check failed: {e}')
 
 if all(checks.values()):
 self.stdout.write('OK')
 sys.exit(0)
 else:
 self.stderr.write(f'Failed checks: {checks}')
 sys.exit(1)

Health Check Configuration

The HEALTHCHECK directive in your Dockerfile or the healthcheck section in Docker Compose tells Docker how to verify your application is healthy. Configure appropriate intervals and timeouts based on your startup time:

# Health check timing configuration
healthcheck:
 test: ["CMD", "python", "manage.py", "health_check"]
 interval: 30s # How often to check
 timeout: 10s # How long to wait for response
 start_period: 10s # Grace period for startup
 retries: 3 # Consecutive failures before restarting

Logging for Containers

Containerized applications should log to stdout and stderr. Docker captures these streams and makes them available via docker logs. Structure your logs for easy parsing:

# logging configuration
LOGGING = {
 'version': 1,
 'disable_existing_loggers': False,
 'formatters': {
 'json': {
 '()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
 },
 },
 'handlers': {
 'console': {
 'class': 'logging.StreamHandler',
 'formatter': 'json',
 'stream': 'ext://sys.stdout',
 },
 },
 'root': {
 'handlers': ['console'],
 'level': 'INFO',
 },
}

CI/CD Integration

Containerization enables powerful automation pipelines. Every change can be built, tested, and deployed automatically. For Laravel applications, our guide on CI/CD for Laravel with GitHub Actions covers similar patterns that apply to Django.

Container Scanning in CI/CD

Integrate security scanning into your deployment pipeline to catch vulnerabilities before they reach production:

# GitHub Actions example with Trivy scanning
name: Docker CI/CD
on:
 push:
 branches: [main]
 pull_request:
 branches: [main]

jobs:
 build-and-scan:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 
 - name: Build Docker image
 run: docker build -t myapp:${{ github.sha }} .
 
 - name: Run Trivy vulnerability scanner
 uses: aquasecurity/trivy-action@master
 with:
 image-ref: 'myapp:${{ github.sha }}'
 format: 'table'
 exit-code: '1'
 severity: 'CRITICAL,HIGH'
 
 - name: Push to registry
 if: github.ref == 'refs/heads/main'
 run: |
 docker tag myapp:${{ github.sha }} registry.io/myapp:latest
 docker push registry.io/myapp:latest

Recommended Scanning Tools

  • Trivy: Open-source vulnerability scanner for containers
  • Snyk: Comprehensive security scanning with fix recommendations
  • Docker Scout: Docker's built-in security analysis
  • Hadolint: Dockerfile linting to catch common mistakes

Automated Testing

Run your full test suite in the container before deploying:

# Add to Dockerfile for testing
FROM python:3.12-slim AS tester
COPY --from=runtime /app /app
WORKDIR /app
RUN pip install pytest pytest-django
CMD ["pytest", "--tb=short", "-v"]

Frequently Asked Questions

Ready to Containerize Your Django Application?

Our DevOps team specializes in building secure, scalable container infrastructure for Django applications. From initial containerization to production Kubernetes deployments, we handle the complexity so you can focus on your application.

Sources

  1. Better Stack: Containerizing Django Applications with Docker - Complete tutorial covering Django project setup, Docker configuration, Gunicorn, and production deployment with Docker Compose.

  2. OWASP Docker Security Cheat Sheet - Authoritative security guidelines for container deployments including non-root users, capability limiting, resource constraints, and read-only filesystems.

  3. TestDriven.io: Dockerizing Celery and Django - Production-focused Django Dockerization with Celery workers, Redis for task queues, and proper multi-container architecture patterns.