Belajar Django - Docker, ASGI & Deployment
Episode 23 of 27

Belajar Django - Docker, ASGI & Deployment

Mendeploy aplikasi devblog ke produksi: server ASGI dengan Uvicorn dan Gunicorn, reverse proxy Nginx, containerisasi dengan Docker Compose, pipeline CI/CD, serta praktik deploy ke VPS/cloud dengan migrasi dan collectstatic.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Sampai episode 22, semuanya berjalan di development server runserver. Di episode ini kita menutup siklus: deploy aplikasi devblog ke produksi dengan arsitektur yang benar — server ASGI, reverse proxy, containerization, dan CI/CD. Ini episode tempat semua keputusan dari 22 episode sebelumnya diuji.

Mengapa topik ini penting? Karena produksi adalah lingkungan yang berbeda total dari development: tanpa runserver, tanpa DEBUG, dengan traffic nyata. Arsitektur deploy yang salah berarti downtime, config drift, dan proses rilis yang menyakitkan. Deployment yang terotomatisasi membuat rilis fitur jadi aktivitas rutin, bukan acara menegangkan.

Pilih Server ASGI

runserver dilarang di produksi (episode 3). Karena aplikasi memakai Channels/WebSocket (episode 21), kita butuh server ASGI, bukan WSGI:

  • Uvicorn — server ASGI modern (mendukung async dan HTTP/1.1 + WebSocket).
  • Gunicorn — manajer worker WSGI klasik; versi 23+ punya gunicorn.workers.gunicorn_worker.AsyncGunicornWorker untuk memakai Uvicorn di dalamnya.

Kombinasi paling umum untuk Django + Channels: Gunicorn sebagai process manager + Uvicorn worker:

Install server produksi
pip install gunicorn uvicorn
gunicorn.conf.py
import multiprocessing
 
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 120
graceful_timeout = 60
max_requests = 1000
max_requests_jitter = 100

workers mengikuti formula CPU (4 core → 9 worker). max_requests me-restart worker setelah 1000+ request (mencegah kebocoran memori). Uvicorn worker membuat setiap worker menangani HTTP dan WebSocket.

Tip

Karena aplikasi memakai WebSocket, server ASGI wajib mendukung upgrade protocol HTTP→WebSocket. UvicornWorker di Gunicorn melakukan itu. Jika kalian tidak butuh WebSocket, arsitektur bisa disederhanakan ke WSGI murni — tetapi untuk devblog yang sudah punya realtime, ASGI adalah pilihan yang benar.

Nginx Reverse Proxy

Nginx berdiri di depan Gunicorn: menangani TLS (HTTPS), melayani static/media, dan meneruskan request ke aplikasi.

nginx.conf
server {
    listen 80;
    server_name devblog.example.com;
 
    location /static/ {
        alias /var/www/devblog/staticfiles/;
        expires 30d;
    }
 
    location /media/ {
        alias /var/www/devblog/media/;
        expires 7d;
    }
 
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
 
    location /ws/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600;
    }
}

Header yang dikirim Nginx penting:

  • X-Forwarded-Proto $scheme — memberitahu Django bahwa koneksi asli adalah HTTPS (untuk SECURE_SSL_REDIRECT episode 18 dan SECURE_COOKIE_*).
  • X-Forwarded-For — untuk throttling berbasis IP asli (episode 19).
  • Blok /ws/ memakai Upgrade/Connection untuk WebSocket — tanpanya handshake WebSocket gagal.

Django perlu tahu ia di belakang proxy. Tambahkan:

Pythonsettings.py - trusted proxy
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True

Warning

Hanya aktifkan SECURE_PROXY_SSL_HEADER jika Nginx adalah satu-satunya jalur masuk ke Django. Jika aplikasi bisa diakses langsung (port 8000 terbuka), attacker bisa memalsukan header X-Forwarded-Proto: https dan menipu Django. Aturan firewall: hanya Nginx yang boleh reach aplikasi.

Dockerfile dan Docker Compose

Container membuat environment produksi identik dengan development. Dockerfile dengan output multi-stage:

Dockerfile
FROM python:3.13-slim AS base
 
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1
 
WORKDIR /app
 
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq-dev gcc \
    && rm -rf /var/lib/apt/lists/*
 
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
RUN python manage.py collectstatic --noinput
 
EXPOSE 8000
CMD ["gunicorn", "-c", "gunicorn.conf.py", "devblog.asgi:application"]

collectstatic di build-time menghasilkan staticfiles/ di dalam image (episode 17). PYTHONUNBUFFERED=1 memastikan log langsung sampai ke stdout.

Docker Compose menyatukan seluruh stack:

docker-compose.yml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: devblog
      POSTGRES_USER: devblog
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
 
  web:
    build: .
    command: sh -c "python manage.py migrate && gunicorn -c gunicorn.conf.py devblog.asgi:application"
    environment:
      DJANGO_ENVIRONMENT: production
      DB_HOST: db
      REDIS_URL: redis://redis:6379/0
    env_file:
      - .env.production
    depends_on:
      - db
      - redis
 
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - web
 
volumes:
  pgdata:

migrate dijalankan sebelum gunicorn start — migrasi idempotent sehingga aman dijalankan setiap container start. Secret (DB_PASSWORD) di-load dari .env.production yang tidak pernah di-commit.

CI/CD Pipeline

Otomasi di GitHub Actions: setiap push ke main menjalankan test, build image, dan deploy. Pipeline minimal:

.github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - run: pip install -r requirements.txt
      - run: python manage.py check
      - run: pytest --cov=blog
 
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: registry.example.com
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_TOKEN }}
      - run: docker build -t registry.example.com/devblog:${{ github.sha }} .
      - run: docker push registry.example.com/devblog:${{ github.sha }}
      - run: ssh "${{ secrets.DEPLOY_HOST }}" "cd /srv/devblog && docker compose pull web && docker compose up -d --no-deps web"

Test harus lulus sebelum deployneeds: test memastikan image yang di-build berasal dari kode yang sudah diuji. Deploy memakai docker compose up -d dengan image ber-SHA: rollback semudah menunjuk ke SHA sebelumnya.

Note

Secret di GitHub Actions disimpan di repository settings (bukan di kode). DEPLOY_HOST, REGISTRY_TOKEN, dan DB_PASSWORD adalah contoh secret yang wajib disembunyikan — persis prinsip env management dari episode 16, diterapkan di level CI/CD.

Verifikasi Deployment

Setelah stack up, uji dari luar:

Verifikasi deployment
curl -I https://devblog.example.com/healthz/
curl -s http://127.0.0.1:8000/admin/login/ | grep -o "<title>[^<]*"
docker compose ps
docker compose logs web --tail=50

/healthz/ dari episode 4 harus 200. Pastikan juga manage.py check --deploy (episode 18) bersih di environment produksi.

Penutup

Inti yang harus dibawa pulang:

  • Gunicorn + Uvicorn worker = process manager + server ASGI untuk HTTP dan WebSocket.
  • Nginx menangani TLS, static/media, dan proxy; header X-Forwarded-* wajib benar.
  • SECURE_PROXY_SSL_HEADER hanya aman jika Nginx satu-satunya pintu masuk.
  • Docker multi-stage + Compose: migrate di start, env dari .env.production, secret di CI.
  • CI/CD: test → build → push → deploy; image ber-SHA memudahkan rollback.
  • Verifikasi dengan healthz, docker compose ps, dan check --deploy.

Di episode 24 selanjutnya kita menyiapkan pertumbuhan: Scaling, Caching & High Availability — desain stateless, Redis untuk cache & session, read replica database, scale horizontal, serta load testing dan tuning. Sampai jumpa di episode 24!

Belajar Django - Docker, ASGI & Deployment | Belajar Django