Django中級 #6 Static/Media の運用と storage backends

読了 7分

基礎 #5 で静的ファイルを初めて扱いました。開発サーバ (python manage.py runserver) が勝手に配信してくれるモードでした。運用に移ると、その自動が消えます — Django は運用で静的ファイルを配信しないのが慣例 です。

今回はそのギャップを埋めます。2 種類のファイルと、それを扱う運用パターンまで。

  • Static — 開発者が作ったファイル (CSS/JS/画像/フォント)
  • Media — ユーザーがアップロードしたファイル (プロフィール写真、添付など)

Static vs Media — 2 つを分離する理由 #

StaticMedia
出処開発者 (リポジトリにコミット)ユーザーアップロード
変更頻度デプロイ単位リアルタイム
バックアップの必要性低い (リポジトリにある)高い (失うと終わり)
CDNほぼ常に時々
キャッシング強いキャッシュ (ハッシュファイル名)普通/短いキャッシュ

この違いから Django は 2 つのシステムを分離してあります。設定も別、ハンドリングも別。

Static — 開発者が作ったファイル #

3 つの設定 — STATIC_URLSTATICFILES_DIRSSTATIC_ROOT #

名前が似ていてよく混乱する 3 つです。

settings.py
STATIC_URL = "/static/"

STATICFILES_DIRS = [
    BASE_DIR / "static",            # プロジェクト全体の静的ファイル
]

STATIC_ROOT = BASE_DIR / "staticfiles"   # collectstatic の出力先
設定意味用途
STATIC_URLブラウザが見る URL prefix/static/css/style.css
STATICFILES_DIRS開発時に追加で探すディレクトリプロジェクト全体の静的ファイル
STATIC_ROOTcollectstatic が集めた結果運用時に nginx が配信

加えて、アプリ別の static/ ディレクトリAppDirectoriesFinder が自動で見つけてくれます。blog/static/blog/style.css のような形。

テンプレートで #

テンプレート
{% load static %}

<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="logo">
<script src="{% static 'js/app.js' %}"></script>

{% static %} タグが STATIC_URL + ファイルパスを組み合わせてくれます。

collectstatic — 運用デプロイの要 #

開発時は 複数の箇所に散らばった 静的ファイルを:

  • アプリ別の static/
  • STATICFILES_DIRS のディレクトリ群
  • 外部ライブラリ (django.contrib.admin など) の静的ファイル

collectstatic1 か所 (STATIC_ROOT) に集めてくれます。

デプロイ時に 1 度
python manage.py collectstatic --noinput

--noinput は「既存のファイルを上書きしますか?」のようなプロンプトを自動で yes 処理。CI で必須。

デプロイフロー:

デプロイ段階
1. コード pull / イメージビルド
2. python manage.py migrate           ← DB スキーマ
3. python manage.py collectstatic --noinput  ← 静的ファイルを集める
4. 新プロセス起動 (gunicorn 再起動など)

STATIC_ROOT は git ignore #

STATIC_ROOTcollectstatic がデプロイごとに新しく作るディレクトリです。git に入れないでください。

.gitignore
staticfiles/
media/

Media — ユーザーアップロードファイル #

モデル — FileFieldImageField #

blog/models.py
from django.db import models

class Profile(models.Model):
    user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to="avatars/%Y/%m/", blank=True)

class Post(models.Model):
    title = models.CharField(max_length=200)
    cover = models.ImageField(upload_to="covers/", blank=True)
    attachment = models.FileField(upload_to="attachments/%Y/%m/", blank=True)

upload_to の動作:

  • "avatars/"MEDIA_ROOT/avatars/ 以下に保存
  • "covers/%Y/%m/" — 年/月別フォルダ自動生成 (covers/2026/05/)
  • callable も可能 — 関数で動的パス

ImageFieldPillow が必要です。

インストール
pip install Pillow

設定 — MEDIA_URLMEDIA_ROOT #

settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

開発時の配信 — 一行 #

Django は運用でメディアを配信しませんが、開発時は便宜上 一行で有効化できます。

config/urls.py — 開発時
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

if settings.DEBUG ガードで囲んで 運用では動作しない ように防ぎます。

テンプレートで #

テンプレート
{% if user.profile.avatar %}
  <img src="{{ user.profile.avatar.url }}" alt="avatar">
{% endif %}

avatar.urlMEDIA_URL + 保存パスを自動で組み合わせます。

運用 — Django は静的ファイルを配信しない #

運用環境での静的/メディア配信の標準パターン 3 つ:

パターン 1 — nginx が直接 #

最も伝統的で最も速い方法。

nginx.conf 一部
server {
    listen 80;
    server_name myblog.com;

    location /static/ {
        alias /srv/myblog/staticfiles/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /srv/myblog/media/;
        expires 30d;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;   # gunicorn
    }
}

/static//media/ は nginx が直接ディスクから配信。それ以外のリクエストだけ gunicorn (Django) へ。

パターン 2 — クラウドストレージ (S3 など) #

スケールが大きくなったり多重サーバになるとディスク共有が難しくなります。S3、GCS、Azure Blob のようなオブジェクトストレージに移します。

パターン 3 — WhiteNoise (小さなアプリ用) #

シンプルなアプリでは nginx なしで gunicorn 前段で静的ファイルを配信 できます。

django-storages + S3 — 本格パターン #

インストール
pip install django-storages[s3] boto3
settings.py — メディアを S3 に
INSTALLED_APPS = [
    ...
    "storages",
]

# AWS 資格情報 (環境変数から読むことを推奨)
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]
AWS_STORAGE_BUCKET_NAME = "my-blog-media"
AWS_S3_REGION_NAME = "ap-northeast-2"
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com"
AWS_DEFAULT_ACL = None
AWS_QUERYSTRING_AUTH = False    # 公開バケットなら

# Django 4.2+ の STORAGES 設定
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

こうしておけば Post.cover.save(...) のような呼び出しが 自動で S3 にアップロード されます。モデルコードは 1 行も変わりません。cover.url も自動で S3 URL。

Django 4.2+ の STORAGES 設定 #

Django 4.2 から DEFAULT_FILE_STORAGE / STATICFILES_STORAGE が deprecated。統合された STORAGES dict を使います。

STORAGES — 静的はローカル、メディアは S3
STORAGES = {
    "default": {                                   # メディア (モデルの FileField/ImageField)
        "BACKEND": "storages.backends.s3.S3Storage",
    },
    "staticfiles": {                                # 静的ファイル
        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
    },
}

静的ファイルも S3 に — CDN 効果 #

静的も S3 + CloudFront
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {"location": "media"},
    },
    "staticfiles": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {"location": "static"},
    },
}

collectstatic が自動で S3 にアップロードします。CloudFront のような CDN を前段に置くとグローバルに速い静的配信。

セキュリティ一行 — 資格情報 #

AWS キーをコード/リポジトリに入れないでください。環境変数、シークレットマネージャ、IAM Role どこに置くにせよコード外。

✅ 環境変数から
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]

EC2/ECS/EKS なら IAM Role を付けてキー自体をコードで扱わないのが最も安全な答えです。

WhiteNoise — 小さなアプリの正解 #

別の nginx も S3 も負担な小さなアプリでは WhiteNoise が答えです。gunicorn 前段で静的ファイルを直接配信します。

インストール
pip install whitenoise
settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",   # SecurityMiddleware の直後
    ...
]

STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

CompressedManifestStaticFilesStorage とは:

  • manifest — ファイル内容のハッシュをファイル名に含む (style.abc123.css)。変更時に新しい URL になりキャッシュ無効化が自動
  • compressed — gzip / brotli 圧縮版を事前に作っておく
デプロイ時
python manage.py collectstatic --noinput

WhiteNoise が STATIC_ROOT から直接配信します。nginx なしで Heroku / Railway / Fly のような PaaS に上げるとき最もよくある答え。

Static のみ、Media は不可 #

WhiteNoise は ユーザーアップロード (media) は扱いません。Media は依然 S3 のような外部ストレージが必要です。ユーザーアップロードがないアプリなら WhiteNoise だけで十分。

Storage バックエンド抽象化 #

Django はすべてのファイル保存を Storage 抽象クラスの後ろに置きます。FileFieldstorage= 引数でモデル別に異なるストレージも可能。

モデル別に異なるストレージ
from storages.backends.s3 import S3Storage

private_storage = S3Storage(bucket_name="my-private-bucket", default_acl="private")

class Document(models.Model):
    file = models.FileField(upload_to="docs/", storage=private_storage)

非公開文書は別の非公開バケット、公開画像はデフォルトバケット、のような分離が可能です。

Pre-signed URL — 非公開ファイルの共有 #

S3 バケットが非公開でも 時間制限 URL を発行して一時的にアクセスさせられます。

signed URL
url = private_storage.url(document.file.name)
# https://...amazonaws.com/...?X-Amz-Algorithm=...&Expires=...

AWS_QUERYSTRING_AUTH = TrueAWS_QUERYSTRING_EXPIRE = 3600 (1 時間) のような設定で期限を制御します。

画像処理 — 一行案内 #

アップロードされた画像を サムネイル/リサイズ する必要があるなら、よくあるライブラリ 2 つ:

ライブラリ特徴
django-imagekitモデルフィールドに specs を定義、リクエスト時または事前に生成
sorl-thumbnailテンプレートタグで即時生成、キャッシュ
imagekit の例
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill

class Profile(models.Model):
    avatar = models.ImageField(upload_to="avatars/")
    avatar_thumb = ImageSpecField(
        source="avatar",
        processors=[ResizeToFill(150, 150)],
        format="JPEG",
        options={"quality": 85},
    )

サイズ別の変換をモデルに宣言し、ライブラリが勝手に生成。画像が重ければバックグラウンドジョブ (DRF #4 Celery) に流すパターンもよくあります。

実戦チェックリスト — 運用デプロイ前 #

  • DEBUG = False
  • ALLOWED_HOSTS 明示
  • STATIC_ROOT 設定 + .gitignore 登録
  • python manage.py collectstatic --noinput がデプロイスクリプトに含まれる
  • nginx / WhiteNoise / S3 のうちどの方法で静的ファイルを配信するか決定
  • メディアファイルのバックアップポリシー (S3 ならバージョン管理 / ライフサイクル)
  • AWS 資格情報は環境変数 / IAM Role
  • セキュリティヘッダ (#5SECURE_SSL_REDIRECTHSTS など)

まとめ #

今回押さえたもの:

  • Static vs Media — 開発者/ユーザー、変更頻度、バックアップ必要性が異なる 2 種類
  • STATIC_URL (ブラウザ prefix)、STATICFILES_DIRS (開発時の検索パス)、STATIC_ROOT (collectstatic の出力)
  • collectstatic — デプロイ時に散らばった静的ファイルを 1 か所に集める
  • MEDIA_URLMEDIA_ROOT、モデルの FileField / ImageFieldupload_to
  • 開発時のみ static(settings.MEDIA_URL, ...) で配信
  • 運用では Django が静的ファイルを配信しない — nginx / S3 / WhiteNoise
  • django-storages[s3] + STORAGES (4.2+) で S3 への切り替え
  • WhiteNoise — 小さなアプリの答え、ミドルウェア一行 + CompressedManifestStaticFilesStorage
  • Storage 抽象化でモデル別に異なるストレージ
  • Pre-signed URL で非公開ファイルの時間制限共有
  • 画像処理: django-imagekitsorl-thumbnail

次回 (#7 テスト) では、中級の最後 — テスト です。django.test.TestCase、fixtures、factory_boy、pytest-django まで一カ所で。

X