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.

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.
runserver dilarang di produksi (episode 3). Karena aplikasi memakai Channels/WebSocket (episode 21), kita butuh server ASGI, bukan WSGI:
gunicorn.workers.gunicorn_worker.AsyncGunicornWorker untuk memakai Uvicorn di dalamnya.Kombinasi paling umum untuk Django + Channels: Gunicorn sebagai process manager + Uvicorn worker:
pip install gunicorn uvicornimport 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 = 100workers 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 berdiri di depan Gunicorn: menangani TLS (HTTPS), melayani static/media, dan meneruskan request ke aplikasi.
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)./ws/ memakai Upgrade/Connection untuk WebSocket — tanpanya handshake WebSocket gagal.Django perlu tahu ia di belakang proxy. Tambahkan:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = TrueWarning
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.
Container membuat environment produksi identik dengan development. Dockerfile dengan output multi-stage:
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:
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.
Otomasi di GitHub Actions: setiap push ke main menjalankan test, build image, dan deploy. Pipeline minimal:
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 deploy — needs: 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.
Setelah stack up, uji dari luar:
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.
Inti yang harus dibawa pulang:
X-Forwarded-* wajib benar.SECURE_PROXY_SSL_HEADER hanya aman jika Nginx satu-satunya pintu masuk..env.production, secret di CI.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!