Dev — docker-compose.dev.yml

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      target: development              # build only up to the dev stage
    volumes:
      - .:/app                         # mount source code — changes reflect instantly
    environment:
      DJANGO_SETTINGS_MODULE: myproject.settings.development
      DEBUG: "true"
      DB_HOST: db
      CACHE_URL: redis://cache:6379
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"
    depends_on:
      db:
        condition: service_healthy
 
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myproject_dev
      POSTGRES_USER: postgres
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      retries: 5
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"                    # expose for TablePlus, pgAdmin, etc.
 
  cache:
    image: redis:7-alpine
 
volumes:
  db-data:

Production — docker-compose.prod.yml

services:
  web:
    image: mydjango:${IMAGE_TAG}       # pinned tag set by CI/CD pipeline — no build: key
    restart: unless-stopped
    command: >
      gunicorn myproject.wsgi:application
        --bind 0.0.0.0:8000
        --workers 4
        --timeout 120
    environment:
      DJANGO_SETTINGS_MODULE: myproject.settings.production
      DB_HOST: db
      CACHE_URL: redis://cache:6379
      SECRET_KEY: ${SECRET_KEY}
      DATABASE_URL: ${DATABASE_URL}
      ALLOWED_HOSTS: ${ALLOWED_HOSTS}
    volumes:
      - static-files:/app/staticfiles  # shared with nginx
    ports:
      - "127.0.0.1:8000:8000"          # loopback only — nginx proxies to this
    depends_on:
      db:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: "0.5"
 
  db:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      retries: 5
    volumes:
      - db-data:/var/lib/postgresql/data
    # No ports: — db not exposed to the host in production
    deploy:
      resources:
        limits:
          memory: 1g
 
  cache:
    image: redis:7-alpine
    restart: unless-stopped
 
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - static-files:/app/staticfiles:ro
      - ./certbot/conf:/etc/letsencrypt:ro
    depends_on:
      - web
 
volumes:
  db-data:
  static-files:

Dev vs Prod Differences at a Glance

ConcernDevelopmentProduction
Image sourcebuild: from local DockerfilePre-built image with pinned tag
Source codeBind-mounted (- .:/app)Baked into the image
Servermanage.py runserver (auto-reload)gunicorn (multi-worker)
App port binding0.0.0.0:8000 (all interfaces)127.0.0.1:8000 (loopback only)
Database portExposed on 5432 for GUI toolsNot exposed
SecretsHardcoded dev valuesInjected by CI/CD via ${VAR}
Restart policyNoneunless-stopped
Resource limitsNonecgroup limits via deploy.resources
Static filesServed by Django dev serverCollected to volume, served by nginx
nginxNot presentFronts the app, serves static files

The Dockerfile Supports Both

The dev file uses build: { target: development } — it builds the image locally from the dev stage. The prod file uses a pre-built image: tag with no build: key — CI/CD built and pushed it; the server only pulls.

The same Dockerfile handles both via multi-stage builds. See dockerfile.


See Also