Djangoの静的ファイルをNginxから配信する-お名前ドットコムVPSへ移行への道14

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約22分で読めます。

Djangoで静的ファイルを WhiteNoise による直接配信から、Nginx による配信へ移行しました。 この記事では、以下の点を丁寧に解説します。

  • BASE_DIR が実際にどこを指しているのか
  • 開発時の static と staticfiles の関係
  • STATIC_URL / STATIC_ROOT / STATICFILES_STORAGE の役割
  • STATIC_URL と実際の HTTPS パスの関係
  • Docker + Nginx で静的ファイルを共有する方法
  • 複数 Django プロジェクトで 1 つの volume を共有してよいか
  • WhiteNoise と Nginx の違いとパフォーマンス比較

こちらが上記方法で配信してるアプリです。

  1. settings.pyの各変数は実際にどこを指してるのか確認
    1. ディレクトリ構成(抜粋)Dckerfile、docker-compose.yml、settings.py
    2. BASE_DIRとSTATIC_ROOTの実際の値を確認
    3. BASE_DIRの定義を解説
    4. BASE_DIRを読み込んでいるところを詳しく解説
      1. file__ には何が入っているのか?
      2. パターンA(昔ながらの書き方:os.path 版)
      3. パターンB(モダンな書き方:pathlib 版)
      4. なぜこんな面倒なことをするのか?
  2. 静的ファイルを配信に必要な知識(static、statifiles他)
  3. Nginxから配信するように変更する
    1. Django側の設定 (settings.py)
    2. Docker Composeの設定 (docker-compose.yml)
    3. Nginxの設定 (default.conf)
    4. 複数のDjangoコンテナで共通にvolumeは使えるか?
      1. なぜ同じボリュームを使い回しても大丈夫なのか?
    5. 【超重要】ファイルが上書き合体(衝突)しないための注意点
      1. 対策:各アプリのstaticの中に「固有のフォルダ」を挟む
    6. 他のサービスへ追加する場合の記述例
      1. コンテナの中で collectstatic コマンドを実行する
      2. ボリュームにファイルを反映させる手順
  4. 以前(Django+WhiteNoise)と比べて、Nginx配信は速くなるのか?
    1. Python の処理を通らない(最大の理由)
    2. Djangoの同時接続数(ワーカースレッド)を消費しない
    3. カーネルレベルの高速化(sendfile)
  5. Nginxでさらに高速設定
    1. Nginxの設定変更
    2. WhiteNoiseとNginxの挙動の違い
    3. Nginx運用時のベストプラクティス
  6. まとめ
広告
MINISFORUM日本公式ストア

settings.pyの各変数は実際にどこを指してるのか確認

まず、私の環境を例に、settings.py の各変数が実際のコンテナ内でどのパスに対応しているのか整理します。

ディレクトリ構成(抜粋)Dckerfile、docker-compose.yml、settings.py

ホストのディレクトリ構成

django2/
├── docker-compose.yml
├── Dckerfile
├── ac_web2ren/
│       ├── access_counter/
│       │        └── settings.py
│       ├── hajimete_app
│       │        └── static/
│       └── staticfiles/
│                └── hajime/
wafvps/
├── docker-compose.yml
├── modesecurity/
│        └── jikken1.conf

各ファイルの関係ある部分だけを抜粋してます。

Dckerfile
WORKDIR /code
COPY . /code/

django2/docker-compose.yml
services:
  # --- 各アプリケーションコンテナ ---
  ac_web:
    build: .
    volumes:
      - .:/code
    container_name: django_ac_web
    working_dir: /code/ac_web2ren

この設定により、ホスト側の django2/ がコンテナ内の /code と完全に共有されます。

django2/ac_web2ren/access_counter/settings.py

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

DEBUG =False


MIDDLEWARE = [

    'whitenoise.middleware.WhiteNoiseMiddleware',
]

STATIC_URL = "acweb2ren/static/"
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

print("static_rootは以下です。")
print(STATIC_ROOT)

BASE_DIRとSTATIC_ROOTの実際の値を確認

まず、実行してBASE_DIRとSTATIC_ROOTが実際に何が設定されているかをprintして調べてみます。
print(STATIC_ROOT)の結果です。
static_rootは以下です。
/code/ac_web2ren/staticfiles

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')によって、BASE_DIRとstatifilesをつなげたものなのでBASE_DIRは/code/ac_web2renということになります。

/codeは
Dckerfileで定義されてる
WORKDIR /code
さらにdocker-compose.ymlで以下のようにホストとバインドマウント(共有)されてる。
  volumes:
    - .:/code
ホストでの「.」の表す位置は、このvolumes:の記述があるdocker-compose.ymlの場所です。
また、これはバインドマウント(.から始まる場合はバインドマウント)で、ホストの.とコンテナのcodeが共通のディレクトリ、ファイルになり、どちらかにあるディレクトリ、ファイルを変更するともう片方に反映されます。
具体的には、上記のホストのディレクトリ構成のdjango2が、コンテナの/codeと共通になります。

コンテナの中に入って確認してみると
docker exec -it コンテナ名 bash  
root@0046ef6baff3:/code# ls
Dockerfile     docker-compose.yml       requirements.txt     ac_web2ren   他
これはホストでdjango2でlsを実行したときと同じ結果です。

BASE_DIRの定義を解説

今度は、BASE_DIRを定義面から解説します。
BASE_DIRを定義してるsettings.pyがコンテナのどこにあるか確認すると/code/ac_web2ren/access_counter/settings.pyにある。
root@0046ef6baff3:/code/ac_web2ren/access_counter# ls
__init__.py  asgi.py  settings.py  urls.py  views.py  wsgi.py

settings.pyでBASE_DIRは旧タイプの書き方が2つある。最近のDjangoは新タイプの書き方。
(旧)BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
(新)BASE_DIR = Path(__file__).resolve().parent.parent
両方とも「このコード(BASE_DIR定義)が記述してあるsettings.py から見て、2つ上の階層の絶対パスを取得する」という処理をしています。

つまり、settings.pyがある場所が
/code/ac_web2ren/access_counter/settings.pyの場合
1個上
/code/ac_web2ren/access_counter
2個上
/code/ac_web2ren ← これがBASE_DIR 

まとめると
・BASE_DIRはコンテナの中のsettings.pyから2つ上の階層
・そこはホストのdocker-compose.ymlがある階層にマウントされている(共通になってる)。

BASE_DIRを読み込んでいるところを詳しく解説

__file__ の正体と、BASE_DIRを読み込んでいるそれぞれのコードがどう動いているのかを分かりやすく解説します。


file__ には何が入っているのか?

__file__ は、Pythonが自動的に用意してくれる特別な変数(マジック変数)です。

ここには、「今このコードが実行されているファイル自身のパス」が格納されています。

今回のケース(settings.py の中)であれば、__file__ の中身は具体的に以下のようになります。

__file__ の中身: /code/ac_web2ren/access_counter/settings.py

※環境によっては相対パス(ac_web2ren/access_counter/settings.py)になることもありますが、意味としてはこのファイルを指します。


パターンA(昔ながらの書き方:os.path 版)

Python

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Django 3.0より前のバージョンで標準だった書き方です。呪文のようですが、内側から順番に解き明かすと簡単です。

  1. os.path.abspath(__file__)
    • __file__ を「省略なしの正確な絶対パス」に変換します。
    • 結果:/code/ac_web2ren/access_counter/settings.py
  2. os.path.dirname(...)(1回目)
    • 指定されたファイルの「一つ上のフォルダ(ディレクトリ名)」を取り出します。
    • 結果:/code/ac_web2ren/access_countersettings.py が入っているフォルダ)
  3. os.path.dirname(...)(2回目)
    • さらに「もう一つ上のフォルダ」を取り出します。
    • 結果:/code/ac_web2ren (これが BASE_DIR になる)

パターンB(モダンな書き方:pathlib 版)

Python

BASE_DIR = Path(__file__).resolve().parent.parent

Django 3.1以降で標準になった、直感的で読みやすい書き方です。オブジェクト指向的にパスを扱います。これも左から順番に見ていきます。

  1. Path(__file__)
    • 文字列だったパスを、便利な Path オブジェクトに変換します。
  2. .resolve()
    • os.path.abspath と同じで、シンボリックリンクなどを解決した「正確な絶対パス」を確定させます。
    • 結果:/code/ac_web2ren/access_counter/settings.py
  3. .parent(1回目)
    • 「親フォルダ」を取得します。
    • 結果:/code/ac_web2ren/access_counter
  4. .parent(2回目)
    • 「さらにその親フォルダ」を取得します。
    • 結果:/code/ac_web2ren(これが BASE_DIR になる)

なぜこんな面倒なことをするのか?

もし BASE_DIR = "/code/ac_web2ren" と直接書いてしまうと、PCを変えたり、本番サーバー(AWSなど)にデプロイしてプロジェクトの配置場所が変わったときに、コードが動かなくなってしまいます。

このように settings.py 自身の場所から逆算して、自動的にプロジェクトのルート(/code/ac_web2ren)を探し出す」 仕組みにしておくことで、どこに持って行っても設定ファイルを書き換えずにそのまま動くようになる(ポータビリティが上がる)のです。

静的ファイルを配信に必要な知識(static、statifiles他)

BASE_DIRに関しては上述しましたが、その他、以下の知識も必須です。

  • static(アプリ内の元ファイル)
  • staticfiles(collectstatic 後の集約先)
  • STATIC_URL(URL 上のパス)
  • STATIC_ROOT(集約先のディレクトリ)
  • STATICFILES_STORAGE(圧縮やハッシュ化の設定)
  • collectstatic(static → staticfiles へコピー)
  • WhiteNoise(Django 側で静的ファイルを返す仕組み)

これらの関係を下の記事でご確認ください。

【Django入門】DjangoアプリをWeb開発環境で開発、公開するまでの手順-ステップ1(PaizaCloud)
無料のWeb開発環境PaizaCloudでDjangoアプリを開発する簡単な手順になります。この記事ではDjangoのプログラムの内容の解説はありません。Djangoアプリをサーバー(Heroku)でデプロイ(公開)するために必要な知識手順…

Nginxから配信するように変更する

今まではDjangoの前にNginxがあるにもかかわらず、Djangoから静的ファイルを配信していました。
NginxとDjango(Gunicorn等)を組み合わせて静的ファイルをNginxから直接配信する方法に変更します、方法は、「共有ボリューム」を使って、Djangoが生成したファイルをNginxが覗き見できるように設定するのが一般的です。

以下の3ステップで設定します。


Django側の設定 (settings.py)

まず、Djangoに「静的ファイルをどこに集めるか」を指示します。

Python

# 1. 配信URLの指定
STATIC_URL = '/static/'(実際のurlではドメイン直後の文字列になります)
私の場合、同一ドメインで複数のコンテナ(プロジェクト)を運用しているのでドメインとstaticの間に文字列(acweb2ren)を入れてます。
STATIC_URL = "acweb2ren/static/"
具体的な静的ファイルへのURL例としては
https://django6.kikuichige1.com/acweb2ren/static/hajime/test.webp
(本番環境ではkikuichige1をとったkikuichigeをドメインで使ってます。
なのでhttps://django6.kikuichige.com/acweb2ren/static/hajime/test.webpでアクセスできます。)
ドメイン(サブドメイン):https://django6.kikuichige1.com
STATIC_URL:acweb2ren(コンテナ振り分けのための文字列)/static
静的ファイルの場所:hajime/test.webp

# 2. collectstatic実行時の集積先(コンテナ内のパス)
import os
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
具体的には、BASE_DIRは/code/ac_web2renなのでSTATIC_ROOTは/code/ac_web2ren/staticfilesです。

Docker Composeの設定 (docker-compose.yml)

ここが重要です。「名前付きボリューム」を作成し、DjangoコンテナとNginxコンテナの両方にマウントします。私の場合、DjangoとNginxで別のymlで書いているためvolumeの書き方で、ちょっと工夫が必要です。

volumeには2種類あります。
・「バインドマウント」パス(./ など)から始まる書き方のものです。ホスト側の特定のディレクトリやファイルを、そのままコンテナに「直結」
・「名前付きボリューム」ボリュームに名前をつけるやり方。中身を確認しにくいが、コンテナ間のデータ共有に便利。

以下、赤字が実行コマンドと修正箇所になります。

# ターミナルで共通のボリュームを1つ作成
sudo docker volume create shared_static_volume

ボリュームの本名をshared_static_volumeと上のコマンドで定義していますが、servicesのvolumeはstatic_volumeという「あだ名」(エイリアス)をつかっています。
ボリュームの本名が production_static_volume に変わったとしても、修正するのは最下部の name: の1箇所だけで済みます。上のサービス欄(- static_volume:...)をわざわざすべて書き換える必要がなくなります。

YAML

django2/docker-compose.yml
services:
  # --- 各アプリケーションコンテナ ---
  ac_web:
    build: .
    volumes:
      - .:/code
   - static_volume:/code/ac_web2ren/staticfiles  # Djangoがファイルを入れる場所。名前付きボリューム
    container_name: django_ac_web
    working_dir: /code/ac_web2ren
volumes: #すでに作成済みであることを宣言しないと新たに作られてしまう。
  static_volume: # ① このymlファイル内で使う「あだ名」(エイリアス)
    external: true # ② 新しく作らずに、外部にある既存のものを探してね
    name: shared_static_volume # ③ Dockerシステムに登録されている「本名」
wafvps/docker-compose.yml
services:

  waf:
    image: owasp/modsecurity-crs:nginx-alpine
    ports:
      - "80:80"
      - "443:443"
    restart: always
    volumes:
      - ./usr/share/nginx/html:/usr/share/nginx/html #すでに定義していたバインドマウント
      # - static_volume:/usr/share/nginx/html/static  # これだと、上で定義してバインドされているディレクトリの中にあるディレクトリを名前付きボリュームにバインドしようとしてるので、かぶってしまいエラーになる。
      - static_volume:/usr/share/nginx/static_files # バインドマウントされているところより上の階層を名前付きボリュームにバインドしてるので問題ない。

volumes: #すでに作成済みであることを宣言しないと新たに作られてしまう。
  static_volume:
    external: true
    name: shared_static_volume

Nginxの設定 (default.conf)

Nginxに「/static/ へのリクエストが来たら、Djangoに聞かずに自分でファイルを出すように」と設定します。

Nginx

    # 静的ファイルの配信設定
    location /acweb2ren/static/ {
        alias /usr/share/nginx/static_files/;  # Nginxで配信する。
    }
    location /acweb2ren {
        proxy_pass   http://django_ac_web:8006;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme; 
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-CSRFToken $cookie_csrftoken;
    }
それぞれymlのあるところに移動して
docker compose up -d --build

複数のDjangoコンテナで共通にvolumeは使えるか?

結論から申し上げますと、他のすべてのサービス(d_rakutenvue_nocliren など)でも、まったく同じ static_volume をそのまま使い回して大丈夫です!

むしろ、1つのNginxからすべてのアプリの静的ファイルを配信する予定であれば、別々のボリュームを作るのではなく、1つの static_volume に全員分を集約するのが正解です。

なぜ大丈夫なのか、そして使う際のデザイン上の注意点を解説します。


なぜ同じボリュームを使い回しても大丈夫なのか?

Dockerの名前付きボリュームは、複数のコンテナが同時に接続して、同時に書き込みや読み込みを行っても壊れない仕組み(マルチアタッチ)になっています。

各サービスで collectstatic を実行すると、共通の shared_static_volume の中にそれぞれの静的ファイルが次々と放り込まれていきます。Nginx側からは、その1つのボリュームを覗くだけで、全アプリのファイルが一度に手に入るため、管理が非常にシンプルになります。


【超重要】ファイルが上書き合体(衝突)しないための注意点

同じボリュームを使い回す場合、1つだけ絶対に気をつけなければいけないルールがあります。それは「アプリ間でフォルダ名やファイル名が被らないようにする」ということです。

例えば、ac_webd_rakuten の両方に、まったく同じパスで static/css/style.css というファイルがあったとします。

これをそのまま共通ボリュームに集約すると、後から実行された方の style.css中身が上書きされて消えてしまいます。

対策:各アプリのstaticの中に「固有のフォルダ」を挟む

Djangoのベストプラクティスに従い、それぞれのプロジェクト内で以下のように静的ファイルを配置してください。

  • ac_web プロジェクト内:static/ac_web/css/style.css
  • d_rakuten プロジェクト内:static/rakuten/css/style.css

このように、static/ の直下にプロジェクト名やアプリ名のフォルダを1つ挟んでおけば、collectstatic で1つのボリュームに集約されたときも、以下のように綺麗に住み分けができます。

Plaintext

shared_static_volume (共通ボリュームの直下)
├── admin/         <-- Django共通の管理画面用(これは共通なので上書きされても問題なし)
├── ac_web/        <-- ac_webのファイルが隔離される
└── rakuten/       <-- d_rakutenのファイルが隔離される

他のサービスへ追加する場合の記述例

docker-compose.yml に反映する例、以下のようにそれぞれのサービスの下に追記すればOKです。コンテナ側のパス(コロン : の右側)は、それぞれのサービスの working_dir に合わせて指定してください。
Nginxのconfファイルも上記の変更が必要です。

YAML

services:
  ac_web:
    <<: *django-common
    working_dir: /code/ac_web2ren
    volumes:
      - static_volume:/code/ac_web2ren/staticfiles  # OK

  d_rakuten:
    <<: *django-common
    working_dir: /code/d_rakuten
    # 追記する
    volumes:
      - static_volume:/code/d_rakuten/staticfiles    # settings.pyのSTATIC_ROOTと合わせる

  vue_nocliren:
    <<: *django-common
    working_dir: /code/vue-nocliren
    # 追記する
    volumes:
      - static_volume:/code/vue-nocliren/staticfiles # settings.pyのSTATIC_ROOTと合わせる

コンテナの中で collectstatic コマンドを実行する

docker-compose.yml にボリュームの設定を追記して up -d --build しただけでは、まだボリュームの中にファイルは追加されません。

原因は、「docker-compose.yml はファイルを共有する『器(フォルダ)』を用意しただけで、Django側がまだその中にファイルを書き込んでいないから」です。

新しく追加した各サービス(d_rakutenvue_nocliren など)の静的ファイルをボリュームに反映させるには、コンテナの中で collectstatic コマンドを実行する必要があります。

以下の手順でコマンドを実行してみてください。


ボリュームにファイルを反映させる手順

コンテナを起動した状態で、新しくボリュームを追加したサービスに対して、それぞれ以下のコマンドを実行してください。

Bash

# d_rakuten の静的ファイルを集約
sudo docker compose exec d_rakuten python manage.py collectstatic --noinput

# vue_nocliren の静的ファイルを集約
sudo docker compose exec vue_nocliren python manage.py collectstatic --noinput

これで各コンテナ内のDjangoが動き出し、それぞれの settings.pySTATIC_ROOT(つまり、いま紐付けた static_volume)に向けてファイルを一斉にコピーします。

以前(Django+WhiteNoise)と比べて、Nginx配信は速くなるのか?

結論から言うと、確実に速くなります。

以前の構成(Nginxがリクエストを受け取り、後ろの Django/WhiteNoise コンテナへ横流しして配信していた構成)と比べると、以下のような理由からパフォーマンスが大幅に向上します。

Python の処理を通らない(最大の理由)

  • 以前の構成:
    Nginx ➔ Gunicorn(Python) ➔ Django ➔ WhiteNoise(Pythonコードでファイルを読み込み、ヘッダーを判定して応答)
  • 新しい構成(Nginx配信):
    Nginx(C言語で書かれた超高速なWebサーバーが直接ディスクからファイルを返す) ➔ 完了(Djangoには一切負荷がかからない)

Pythonのプログラムを動かすには、メモリの確保やCPUの計算など多くのオーバーヘッドがかかります。C言語で最適化されたNginxが直接ファイルを返す方が、圧倒的にレスポンスが早くなります。

Djangoの同時接続数(ワーカースレッド)を消費しない

Django(Gunicornなど)が一度に処理できるリクエスト数には上限があります。静的ファイルの配信をDjangoに任せていると、画像やCSSのダウンロードだけでDjangoの処理枠が埋まってしまい、肝心のHTML生成やAPIのレスポンスが遅くなる原因になります。静的ファイルをNginxに完全移譲することで、Djangoは本来の動的処理に集中できるようになります。

カーネルレベルの高速化(sendfile)

Nginxは、Linuxカーネルの sendfile という仕組みを使って、ディスク上の静的データをメモリ(アプリケーション層)にコピーすることなく、直接ネットワークソケットに流し込むことができます。WhiteNoise(Python)では真似できない、ハードウェアの性能を限界まで引き出す配信が可能です。

Nginxでさらに高速設定

DjangoのWhiteNoiseが提供する「リクエストのAccept-Encodingに応じて、事前に生成された圧縮ファイル(.gz)を自動選択して配信する機能」は、Nginxでは静的圧縮ファイルの配信機能(*_staticディレクティブ)として提供されています。

Nginxでこれを実現するための仕組みと設定方法は以下の通りです。

Nginxでは、クライアントが対応している圧縮形式(Accept-Encoding: gzipなど)を自動で判別し、ディスク上に用意された最適なファイルを選んで返します。

  • Gzip(.gz)への対応: gzip_static モジュール(標準搭載)

Nginxの設定変更

NginxでWhiteNoiseと同等の配信を行う場合の基本的な設定例です。

    # 静的ファイルの配信設定
    location /acweb2ren/static/ {
        alias /usr/share/nginx/static_files/;  # Nginxで配信する。
        # 1. 永続的なブラウザキャッシュの設定 (WhiteNoiseの独自ヘッダーと同等)
        expires max;
        add_header Cache-Control "public, immutable";

        # 2. Accept-Encoding に応じた事前圧縮ファイルの配信設定
        gzip_static on;    # .gz があれば自動選択

        # 3. プロキシ環境用の設定(CDNやプロキシに圧縮を正しくキャッシュさせる)
        gzip_vary on;
    }

WhiteNoiseとNginxの挙動の違い

Nginxに配信を任せる場合、ファイルの事前生成(圧縮)手順に1点だけ違いがあります。

項目WhiteNoiseNginx
ファイルの生成collectstatic コマンド実行時に、元のファイルと一緒に .gz を自動生成する。Nginx自身はファイルを自動生成しない。元のファイルと同じ場所に .gz ファイルが配置されている必要がある
配信の仕組みPython(WSGI/ASGI)プロセス内で判定して配信する。C言語で書かれたWebサーバー層で直接配信するため、非常に高速。

Nginx運用時のベストプラクティス

DjangoアプリをNginxの裏で動かす場合、settings.pyMIDDLEWARE から whitenoise を削除して完全にNginxへ移行するパターンと、両方有効にしておくパターンがあります。

両方有効(Nginxが手前で配信し、設定漏れがあればWhiteNoiseが処理する)にしておけば、ビルド時にWhiteNoiseが生成した .gz ファイルをそのままNginxが効率よく再利用して配信できるためおすすめです。

まとめ

この記事は、単なる設定例ではなく:

  • Docker の仕組み
  • Django の静的ファイルの本質
  • Nginx の高速配信の仕組み
  • 複数プロジェクト運用時の設計思想
  • WhiteNoise との比較
  • 実運用でハマるポイント(volume 衝突など)

これらを 実例ベースで深く理解できる構成になっています。

イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。
MENTAやってます(ichige)

タイトルとURLをコピーしました