Django_本番環境構築①_nginxとgunicornでDjangoアプリケーションを動かす #363
1週間ほど色々と沼にハマっており、更新が遅くなりました。
noteを始めてから最も長期間空けてしまいましたが、ちゃんとした本番環境の構築は初めての挑戦なので、やむなしと思うことにしています。
今回は、本番環境でアプリケーションを運用するためのシステム設計を行い、ローカル環境で動作させてみるところまでです。
本番環境はAWSを想定しており、最終的にはAWSやHTTPS、DNS周りの設定まで完成させていきたいと思っていますが、そのあたりは追々で。
本番環境はWebサーバーやWSGIサーバーを用いて運用することが一般的とされており、当然ながらDjangoの開発用サーバーを使った`python manage.py runserver`の利用は推奨されません。
そのため、ローカル環境でDjangoだけで開発してきた時と比べ、システム設計自体を見直す必要があります。
システム設計
インフラ: docker
まず大前提として、アプリケーションはDockerコンテナを用いていくこととします。必要な設定やライブラリ管理なども楽ですし、後述するWebサーバーもDockerの公式イメージを使えば楽に使用できるからです。
Webサーバ: nginx
Webサーバーは静的ファイル(HTML, CSS, JS)の配信やリバースプロシキとしての役割を果たします。リバースプロシキとは、クライアントからのリクエストをバックエンドのアプリケーションサーバーに転送し、アプリケーションサーバーからのレスポンスをクライアントに返す役割です。ロードバランシング(負荷分散)、SSL終端、キャッシュ、HTTP/2対応などの機能を実現します。
今回は高パフォーマンスなWebサーバーであるnginxを使用します。
WSGIサーバ(アプリケーションサーバ):gunicorn
WSGIとはWeb Server Gateway Interfaceの略で、Pythonアプリケーションを実行してWebサーバとやり取りするためのインターフェースです。アプリケーションからのレスポンスをWebサーバーに返し、動的なコンテンツを生成・配信する役割を担います。
今回は代表的なPythonのWSGIサーバーであるgunicornを使用します。
gunicornはプリフォークモデルを採用しており、マスタープロセスが複数のワーカープロセスを生成・管理します。これによって同時接続に対応できます。
Webアプリフレームワーク:Django
フレームワークはDjangoです。ここでは説明を割愛します。
以上がシステム設計の全体感です。上記のようにNginxをリバースプロキシとして使用し、Gunicornをアプリケーションサーバーとして使用することは一般的な構成と言えます。これにより、Nginxが静的ファイルの配信やリクエストのロードバランシングを担当し、GunicornがPythonアプリケーションの処理を行います。
ディレクトリ構造
上記のシステム設計に基づいたディレクトリ構造は以下になります。「my_application」配下がDjangoのルートディクレクトリで、その親ディレクトリにDockerやNginx周りの設定ファイルが並びます。
解説に使用しないファイルは割愛していますが、以下のようになります。
my_application -------- my_application
|- config -------- settings.py
| ` wsgi.py
|- media
|- staticfiles
` manage.py
.env
.gitignore
docker-compose.yml
Dockerfile
Dockerfile.nginx
nginx.conf
Dockerの設定
Dockerコンテナは、WebアプリケーションとNginxで2つ立ち上げます。
まずは本体であるWebアプリケーション用にDockerfileを用意します。
[Dockerfile]
FROM python:3
USER root
RUN apt-get update
RUN apt-get -y install locales && \
localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm
RUN apt-get update
# libcap2-binはgunicornに特定の権限(ここではポート番号1024未満のポートにもバインド可能にする)
RUN apt-get install -y vim less libcap2-bin
RUN apt-get -y install ffmpeg # アプリケーションで使用
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
RUN python -m pip install django
RUN python -m pip install django-environ # 今後設定するのAWS環境で使用
RUN python -m pip install django-storages # 今後設定するのAWS環境で使用
RUN python -m pip install boto3 # 今後設定するのAWS環境で使用
RUN python -m pip install yt-dlp # アプリケーションで使用
RUN python -m pip install gunicorn
# gunicornのプロセスにcap_net_bind_serviceというケイパビリティを設定し、ポート番号1024未満のポートにもバインド可能にする
RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/gunicorn
RUN mkdir -p /var/log/gunicorn/
次にWebサーバーであるNginx用にDockerfile.nginxを用意します。
[Dockerfile.nginx]
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
ポートや依存関係の整理のため、docker-compose.ymlを以下のように記述します。
[docker-compose.yml]
version: '3.10'
services:
my_application:
container_name: my_application
build:
context: .
dockerfile: Dockerfile
working_dir: '/root/my_application'
tty: true
volumes:
- ./my_application:/root/my_application
- /var/www/my_application/staticfiles:/var/www/my_application/staticfiles
image: my_application
env_file:
- .env
nginx:
container_name: nginx
build:
context: .
dockerfile: Dockerfile.nginx
ports:
- "80:80"
image: my_application-nginx
depends_on:
- my_application
volumes:
- /var/www/my_application/staticfiles:/var/www/my_application/staticfiles
このdocker-compose.ymlでいくつかポイントがあります。
まず、「staticfiles」を「/var/www/my_application」にボリュームしている点です。ホストマシン上にある「my_application/staticfiles」をそのまま使ってしまうと、Webサーバーであるnginx経由でアクセスしようとした時にパーミッションエラーが起きてしまいます。
そのため、静的ファイル配信用のフォルダである「/var/www/」配下にstaticfilesの中身を一式でコピーしておき、そこにnginxがアクセスするようにしています。
次に、nginxコンテナでport設定している点もポイントです。ここでは"80:80"としていて、一旦HTTP通信だけを受け付けています。本番環境では443もルーティングして、HTTPS通信も受け付けるようにします。
Djangoの開発用サーバーだけを使っていた時は本体のコンテナでルーティングしていましたが、ここではNginxがクライアントのリクエストの窓口になるため、Nginxコンテナで設定しています。
Nginxの設定
Dockerで大部分は設定できますが、Nginxはより詳細の設定が必要です。「nginx.conf」に定義していきます。
[nginx.conf]
user nginx;
events {}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream django_app {
server yt-converter:8000;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://yt-converter:8000;
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;
}
location /static/ {
alias /var/www/yt-converter/staticfiles/;
}
}
}
ここで極めて重要な設定が最初の2行です。
include /etc/nginx/mime.types;
default_type application/octet-stream;
MIME(マイム)タイプとは、インターネット上でファイルやデータの形式を識別するために使用される文字列です。MIMEタイプは、「タイプ/サブタイプ」の形式で表され、例えば、HTMLのMIMEタイプは text/html、CSSのMIMEタイプは text/css となります。
Webサーバーは、ファイルをクライアント(ブラウザ)に送信するときに、HTTPヘッダーの Content-Type フィールドにMIMEタイプを含めることで、クライアントが受信したファイルの形式を正しく解釈できるようにします。これにより、ブラウザは適切な方法でファイルを処理し、表示できます。
「include /etc/nginx/mime.types;」を nginx.conf に追加することで、Nginxは /etc/nginx/mime.types ファイルに定義されたMIMEタイプを使用して、正しいコンテンツタイプをクライアントに送信できるようになります。
「default_type application/octet-stream;」はMIMEタイプが上記ファイルに定義されていない場合に、Nginxが安全なデフォルト動作を行うようになる設定です。
これらの設定がされていない場合、CSSやJSのファイル自体は読み込めているのに、CSSもJSも効いていない、という事象が起こりえます。私自身は数日間ここでハマっていました。。
また、最後のlocation /static/のところも要注意です。ここが違っているとNot found errorになってしまいます。
Djangoの設定
Django側はsettings.pyだけ触れておきます。
ここではstatic_rootはいつも通り設定しています。「/var/www/」を使うのは、ホストマシンから静的ファイルを配信しないといけないというローカル環境特有の制約でもあるので、後ほどの手順で「staticfiles」から手動コピーします。
[settings.py]
DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [BASE_DIR / 'static']
コンテナ立ち上げ後の手順
準備ができたらDockerを立ち上げます。
docker compose up -d --build
コンテナを立ち上げた後は以下の手順を踏みます。まずはDjangoアプリケーションのコンテナに入ります。
docker exec -it my_application bash
collectstaticで静的ファイルを集めます。私はSCSSを使っていますが、SCSSが混じっているとnginxがうまく読み込めないらしいので、コンパイルしておいてSCSS自体はスキップします。
python manage.py collectstatic --ignore "*.scss"
次に、集めてきた静的ファイルを配信用ディレクトリにコピーします。本当は直接collectstaticしたいところですが、ホストコンピュータ上のパーミッションの問題で出来なかったやつです。
一度コンテナから出ます。
exit
ホストマシン上で以下のコマンドでコピーします。パスに注意してください。
# 保存先ディレクトリを作る
sudo mkdir -p /var/www/yt-converter/staticfiles
# コピーする
sudo cp -R /path/to/your/application/my_application/my_application/staticfiles/* /var/www/my_application/staticfiles/
これで配信用フォルダに静的ファイルを設置できました。
ホストマシンとDockerコンテナはボリュームしているので、コンテナの中から読み取り可能です。
もう一度アプリケーションのコンテナに入り、gunicornでサーバーを起動します。
docker exec -it my_application bash
# コンテナ内で以下のコマンド
gunicorn -w 4 config.wsgi:application --bind 0.0.0.0:8000
Djangoではconfigディレクトリ配下の「wsgi.py」でアプリケーションサーバーを受け付けています。上記コマンドにより、gunicornがアクセスします。
これで、本番環境さながらにアプリケーションを動かすことができるはずです。
ちなみにNginxはコンテナを起動するだけで動作するので、特に他の操作は必要ありません。これはNginxの公式Dockerイメージには、事前に設定されたデフォルト設定が含まれているからです。
ここまでお読みいただきありがとうございました!!