はじめに
Djangoの開発環境はpython manage.py runserverで動くが、本番環境ではこれを使ってはいけない。
公式ドキュメントにも「runserverは本番用ではない」と明記されている。本番ではgunicornでDjangoアプリを動かして、その前段にNginxを置く構成が定番。FastAPIのデプロイ記事でDockerの基本は書いたので、今回はDjango固有の部分を中心に整理した。
FastAPIデプロイとの構成の違い
まず全体像を整理する。
# FastAPI
Internet → uvicorn(HTTPサーバー + アプリ)→ DB
# Django
Internet → Nginx → gunicorn(WSGIサーバー)→ Django → DB
↓
staticfiles(CSS/JS/画像)
FastAPIはuvicornがHTTPサーバーとアプリを兼ねるが、DjangoはNginxが必要。理由は2つ。
staticファイルの配信 — Djangoは開発環境では静的ファイルを自分で配信できるが、本番ではDEBUG=Falseにすると配信しない。Nginxが代わりに配信する。
WSGIとASGIの違い — DjangoはデフォルトでWSGI(同期)。gunicornがWSGIサーバーとして動く。FastAPIはASGI(非同期)なのでuvicornを使う。
プロジェクト構成
myproject/
├── Dockerfile
├── compose.yaml
├── nginx/
│ ├── Dockerfile
│ └── default.conf
├── myproject/
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # 共通設定
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ └── users/
├── static/ # 開発時の静的ファイル
├── staticfiles/ # collectstaticの出力先
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
└── manage.py
設定ファイルを環境ごとに分割するのがDjangoのベストプラクティス。
settings.pyの分割
# myproject/settings/base.py(共通設定)
from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"apps.users",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "myproject.urls"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "mydb"),
"USER": os.environ.get("DB_USER", "user"),
"PASSWORD": os.environ.get("DB_PASSWORD", "password"),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
LANGUAGE_CODE = "ja"
TIME_ZONE = "Asia/Tokyo"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles" # collectstaticの出力先
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
# myproject/settings/development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# 開発用にデバッグツールバーを追加
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
INTERNAL_IPS = ["127.0.0.1"]
# myproject/settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
# セキュリティ設定
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# キャッシュ(本番はRedisを使う)
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ.get("REDIS_URL", "redis://localhost:6379"),
}
}
# ログ設定
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
# 環境変数で使う設定を切り替える
export DJANGO_SETTINGS_MODULE=myproject.settings.production
requirements.txtの分割
# requirements/base.txt
Django>=5.0
djangorestframework>=3.15
psycopg2-binary>=2.9
gunicorn>=21.0
# requirements/development.txt
-r base.txt
django-debug-toolbar>=4.0
pytest-django>=4.8
# requirements/production.txt
-r base.txt
whitenoise>=6.6 # staticファイルの配信(オプション)
sentry-sdk>=1.40 # エラー監視
Dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
DJANGO_SETTINGS_MODULE=myproject.settings.production
WORKDIR /app
# 依存関係をインストール
COPY requirements/production.txt requirements/production.txt
RUN pip install --no-cache-dir -r requirements/production.txt
# アプリのコードをコピー
COPY . .
# staticファイルを収集
RUN python manage.py collectstatic --noinput
# 非rootユーザーで実行
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application",
"--bind", "0.0.0.0:8000",
"--workers", "4",
"--timeout", "120",
"--access-logfile", "-",
"--error-logfile", "-"]
PYTHONDONTWRITEBYTECODE=1で.pycファイルを生成しない、PYTHONUNBUFFERED=1でログをバッファリングしないように設定。Dockerコンテナでは定番の設定。
collectstaticをビルド時に実行してNginxが配信できるようにする。
gunicornのワーカー数
# 推奨: (CPUコア数 × 2) + 1
--workers 4 # 2コアなら (2×2)+1 = 5、余裕を見て4
FastAPIのuvicornと同じ発想。
Nginxの設定
# nginx/Dockerfile
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
# nginx/default.conf
upstream django {
server web:8000; # compose.yamlのサービス名
}
server {
listen 80;
server_name localhost;
# staticファイルの配信
location /static/ {
alias /app/staticfiles/;
expires 30d;
add_header Cache-Control "public, no-transform";
}
# mediaファイルの配信
location /media/ {
alias /app/mediafiles/;
expires 7d;
}
# Djangoアプリへのプロキシ
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90;
client_max_body_size 10M;
}
}
/static/へのリクエストはNginxが直接ファイルを返す。それ以外はgunicornにプロキシする。FastAPIではuvicornが全部処理するのでNginxは不要だったが、DjangoはstaticファイルをNginxで配信するのがほぼ必須。
compose.yaml
services:
nginx:
build:
context: ./nginx
ports:
- "80:80"
volumes:
- static_volume:/app/staticfiles # staticファイルを共有
- media_volume:/app/mediafiles
depends_on:
web:
condition: service_healthy
web:
build: .
env_file:
- .env
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/mediafiles
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s # マイグレーションに時間がかかるので長めに
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
static_volume: # NginxとDjangoコンテナ間でstaticファイルを共有
media_volume:
ポイントはstatic_volumeとmedia_volumeをNginxとwebコンテナで共有していること。Djangoがcollectstaticでファイルをボリュームに書き込み、Nginxがそのボリュームから直接配信する。
マイグレーションの実行タイミング
本番でのマイグレーションをどのタイミングで実行するかは悩む部分。
パターン① entrypointスクリプト
# entrypoint.sh
#!/bin/bash
set -e
echo "マイグレーションを実行中..."
python manage.py migrate --noinput
echo "アプリを起動..."
exec "$@"
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
# compose.yaml
web:
build: .
entrypoint: ["/entrypoint.sh"]
command: ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
パターン② 初期化コンテナ(推奨)
services:
migrate:
build: .
command: python manage.py migrate --noinput
env_file: .env
depends_on:
db:
condition: service_healthy
restart: "no" # 一回実行したら終了
web:
build: .
depends_on:
migrate:
condition: service_completed_successfully
マイグレーションと本番サーバーを分けることで、複数のwebコンテナが同時にmigrateを実行してしまう問題を避けられる。
開発環境のcompose.override.yaml
# compose.override.yaml(Gitignore)
services:
web:
build:
context: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/app
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.development
- DEBUG=true
ports:
- "8000:8000"
nginx:
profiles:
- production # 開発環境ではNginxを起動しない
開発環境ではrunserverでホットリロードを使い、Nginxは使わない。profilesでサービスの起動条件を制御できる。
.env
# .env(Gitignore)
SECRET_KEY=your-production-secret-key-minimum-50-chars
DEBUG=false
ALLOWED_HOSTS=example.com,www.example.com
DB_NAME=mydb
DB_USER=user
DB_PASSWORD=secure-password
DB_HOST=db
DB_PORT=5432
DJANGO_SETTINGS_MODULE=myproject.settings.production
REDIS_URL=redis://redis:6379
ヘルスチェックエンドポイント
# apps/health/views.py
from django.http import JsonResponse
from django.db import connection
from django.db.utils import OperationalError
def health_check(request):
try:
connection.ensure_connection()
db_status = "ok"
except OperationalError:
db_status = "error"
status_code = 200 if db_status == "ok" else 503
return JsonResponse(
{"status": "ok" if db_status == "ok" else "error", "db": db_status},
status=status_code,
)
# urls.py
from django.urls import path
from apps.health.views import health_check
urlpatterns = [
path("health/", health_check),
...
]
デプロイ手順まとめ
# 1. イメージをビルド
docker compose build
# 2. DBを起動してマイグレーション
docker compose up -d db
docker compose run --rm migrate
# 3. 全サービスを起動
docker compose up -d
# ログを確認
docker compose logs -f web
docker compose logs -f nginx
# コンテナの状態を確認
docker compose ps
FastAPIデプロイとの比較
| 項目 | Django | FastAPI |
|---|---|---|
| HTTPサーバー | gunicorn(WSGI) | uvicorn(ASGI) |
| Nginxが必要か | 必須(staticファイル) | 任意 |
| staticファイル | collectstatic必要 | 基本なし |
| ワーカー管理 | gunicorn | uvicorn / gunicorn |
| マイグレーション | manage.py migrate | alembic upgrade head |
| コンテナ数 | 3(Nginx + web + DB) | 2(web + DB) |
| 設定の分割 | settings/本番・開発 | pydantic-settings |
まとめ
- 本番では
runserverの代わりにgunicornを使う - Nginxはstaticファイルの配信とリバースプロキシが役割
- NginxとDjangoコンテナでstaticファイルをVolumeで共有する
- マイグレーションは初期化コンテナで実行するのが安全
- settings.pyは環境ごとに分割して
DJANGO_SETTINGS_MODULEで切り替える
FastAPIはNginxなしで動くシンプルな構成だったが、DjangoはNginxが必要な分コンテナが一つ増える。staticファイルの扱いを最初に設計しておかないと後で詰まるポイントだった。