【Docker】NGINX +Gunicorn+Djangoをローカルでシンプルに動かした!

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

この記事は約18分で読めます。
広告

シンプル構成でDocker+Nginx+Gunicorn+Djangoを学習するのが目的です。
データベースとstatic(静的ファイルの配信)は複雑になるので入れません。
環境変数も分けてません。
あくまでローカル環境で動かすことしか考えておりません。
ネットでこの辺のことを調べるとSoketファイルやserviceファイルの話がでてきますが
今回、Dockerでは関係なさそうです。
私も初心者でよく分かりませんが、切り分けてみないとごちゃごちゃになって分からなくなります。
まずは全体をシンプルにとらえて、
Soketファイルやserviceファイルが何なのか調べていこうと思います。
(知っていた方がいいと思いますが、Docker使っていると知らなくても動かせます。)
記事中の細かい解説はChatGptやBingに聞いたものを修正しているので、
断定解説したあとに疑問が書かれていて読みにくいかもしれません。
また私の誤解や間違いもあると思いますのでご了承ください。
手順として
Pythonが動くDockerコンテナを用意しDjangoを動かす。
Gunicorn+Djangoで動かす。
Nginx+Gunicorn+Djangoで動かす。
環境:Windows11、DockerDesktop
こちらを参考にさせていただきました。
【Docker(Docker-Compose)】Python,Djangoの開発・本番環境構築【Postgres,Gunicorn,Nginx利用】
最終的には、こっちにしようと思います。

ローカルで、ある程度わかったらVPSで試すのもおすすです。

広告

Docker

独自解釈で解説します。
Dockerとは、カスタマイズしたコンピューターを作ることで、本体はコンテナという部分です。
イメージ・・コンテナの設計図。
コンテナ・・イメージから作成される本体(実行環境)です。
Dockerfile・・イメージを作成するための手順を記述したファイル。設計図を作る仕様書みたいなもの。
Dockerコマンド・・Dockerfileに基づいてイメージを作成(ビルド)したり、イメージを元にコンテナを作って起動したりできる。

コンテナが複数の場合
Docker Composeというツールが使われ、Docker Composeコマンドで指示する。その際、読まれるのがdocker-compose.yml。

Dockerの使い方は以下も参考にしてください。

Python環境コンテナを作る

クィックスタート: Compose と Djangoを元にデータベースを使わない最小限のものでDjangoを動かします。

ディレクトリ校正
NINGUN1(任意の文字)
 ├ Dockerfile
 ├ requirements.txt
 └ docker-compose.yml

Dockerfile

Pythonが実行できる環境をつくり、Djangoとgunicorn(後で使う)をpipした状態のコンテナを作ります。

# syntax=docker/dockerfile:1
FROM python:3
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/

コードを解説します。1行目は、ないサンプルもよく見るので、なくてもいいかも。3、4行目はPythonの場合、定番なので意味は気にしなくてもいいかも。5-8行目でポイントは、今、自分が見ているVSCodeのコードとコンテナの中との関係をおさえることが大事です。また、DockerfileとymlファイルはDockerを制御するためのファイルなので、特別扱いして存在しないものと考えたほうが考えやすいと思います。

  1. # syntax=docker/dockerfile:1:Dockerfile の構文の最新版を指す 。ここの#はコメントアウトではない。
  2. FROM python:3: この行は、Dockerイメージのベースイメージとして公式のPython 3イメージを使用することを指定しています。
  3. ENV PYTHONDONTWRITEBYTECODE=1: この行は、Pythonがバイトコードファイルを生成しないように設定しています。これにより、Dockerコンテナ内でPythonコードの実行が効率的になります。
  4. ENV PYTHONUNBUFFERED=1: この行は、Pythonの出力をバッファリングせずに即座に表示するように設定しています。これにより、ログやエラーメッセージがリアルタイムで表示されます。
  5. WORKDIR /code: この行は、コンテナ内での作業ディレクトリを/codeに設定しています。これは、後続のコマンドが/codeディレクトリ内で実行されることを意味します。
  6. COPY requirements.txt /code/: この行は、パソコンのVSCodeで今、書いているカレントディレクトリにあるrequirements.txtファイルをコンテナ内の/code/ディレクトリにコピーします。
  7. RUN pip install -r requirements.txt: この行は、requirements.txtファイルに記載されたライブラリをpipを使用して、コンテナ内にインストールされます。
  8. COPY . /code/: この行は、パソコンのVSCodeで今、書いているカレントディレクトリ内のすべてのファイルとディレクトリをコンテナ内の/code/ディレクトリにコピーします。これにより、Pythonアプリケーションのソースコードやその他のファイルがコンテナ内に配置されます。

目次へ

requirements.txt

requirements.txtの中身はDjangoとgunicornだけ。

Django
gunicorn

docker-compose.yml

Dockerfileとrequirements.txtがあればdocker-compose.ymlなくてもいいか確認しました。
Dockerコマンドでビルド(イメージ、コンテナを作成)して、
そのあとDjangoのプロジェクトを手動で作ろうと思ったができなかった。

Python(Django)を動かすだけならDockerコマンドで代替できる。
ただ-itを追加しないとコンテナがすることがなくてすぐ停止してしまう。-itを付けるとターミナルでPythonのコマンド入力待ちになる。
docker build -t my_app .
my_appというイメージが出来上がります。
docker run -it -v $(pwd):/code -p 8000:8000 my_app
これはホストマシンの現在のディレクトリ ($(pwd)) を /code にマウントし、
ホストのポート8000をコンテナのポート8000にマッピングしmy_app イメージを実行します。
ただこのpythonターミナルの状態では、Pythonのコードしか受け付けません。(printとか)
なのでdjango-admin startproject を実行してもエラーになる。
pythonターミナルから抜けるとコンテナが停止するので、コンテナの中に入ってdjango-admin startprojectもできません。
なのでDjangoを使うなら、下のdocker-composeの方法になります。
exit()でターミナルから抜けるとDockerも停止する。

docker-compose.ymlを追加する。
本来、複数のコンテナを使うときに使われるが単独でも使えた。

version: "3.9"
services:
  web:
    build: .
    volumes:
      - .:/code
    ports:
      - "8000:8000"

これは、Docker Composeを使用して複数のコンテナを管理するための設定ファイルの一部です。以下はこのYAMLファイルの各部分の説明です。

  1. version: "3.9": この行は、Docker Composeのバージョンを指定しています。このファイルはDocker Composeのバージョン3.9に対応しています。
  2. services: このセクションは、Docker Composeが管理するコンテナの定義を開始します。ここで、webという名前のサービスを定義しています。
  3. build: .: この行は、webサービスのコンテナイメージをビルドする方法を指定しています。
    .は、docker-compose.ymlが存在するディレクトリを指します。つまり、現在のディレクトリでDockerイメージをビルドすることを意味します。通常、Dockerfileが同じディレクトリに存在することが期待されます。
  4. volumes: - .:/code: この行は、ホストマシン(今回パソコン)のカレントディレクトリとコンテナ内の/codeディレクトリとの間でボリュームを設定します。これにより、ホストマシン上のファイルがコンテナ内にマウントされ、ファイルの変更がリアルタイムでコンテナ内に反映されます。これは、開発中にコードの変更を即座に反映するのに役立ちます。(DockerfileのCopyは1回だけコピー、マウントは常にコピーされると思われる。)
  5. ports: - "8000:8000": この行は、ホストマシン(今回はパソコン)のポートとコンテナのポートをマッピング(くっつける)します。具体的には、ホストマシンのポート8000をコンテナのポート8000にマッピングしています。これにより、ホストマシンからコンテナ内のアプリケーションにアクセスできるようになります。たとえば、ホストマシンのブラウザでhttp://localhost:8000にアクセスすると、コンテナ内のアプリケーションにアクセスできます。
ちなみに参考にしたクィックスタート: Compose と Djangoの
データベースを削除する前のdocker-compose.ymlは
dbとwebで2つコンテナが作られてました。
version: "3.9"
services:
  db:
    image: postgres←Dockerファイルを使わなくても、これでイメージを読み込んでいると思われます。
    略
  web:
    略
    depends_on:
      - db

イメージとコンテナを以下で作ります。

ターミナルで
docker-compose run -p 8000:8000 web bash
注意:-p 8000:8000はymlのports:で書いたことと同じことなので本来必要ありませんが、
改めてコマンドのオプションに書いておかないと有効になりませんでした。
この件の対策として他に、こちらが参考になります。docker-compose run は portをmappingしない

docker-compose.ymlを使わずにdocker runで実行したときと同じように何も処理はしていないのですが、
コンテナは停止せず動き続けます。
ターミナルの表示がroot@10ce04718d61:/code#に変わりました。
これはコンテナの中にいることを表しています。

Djangoプロジェクトを作ります。そのまま以下入力してEnter
django-admin startproject composeexample .

パソコンのVSCodeで今、書いているカレントディレクトリとコンテナのcodeディレクトリがマウントされているので、コンテナ内のcodeディレクトリに対して行ったことはVSCodeにも反映されます。
なのでVSCodeにもしばらくしてDjangoのプロジェクトファイルができます。

python manage.py runserver 0.0.0.0:8000
を実行しhttp://localhost:8000/またはhttp://127.0.0.1:8000/にアクセスするとDjangoの画面が表示します。
ターミナルにStarting development server at http://0.0.0.0:8000/と出ますが

http://0.0.0.0:8000/
ではエラーになるので注意してください。

python manage.py runserverは開発用のウェブサーバーが起動されDjangoが実行されています。
今度は、それを使わずgunicornをアプリケーションサーバーとして使ってDjangoを動かしてみます。
目次へ

Gunicorn+Django

コンテナの中に入ったroot@10ce04718d61:/code#の状態で
再開やDockerの中に入るなどDockerの操作方法は、こちらを参考にしてください。

gunicornはrequirments.txtに書いて、すでにpipインストールされてます。
実行。
gunicorn composeexample.wsgi:application --bind 0.0.0.0:8000
http://localhost:8000/またはhttp://127.0.0.1:8000/にアクセスするとDjangoの画面が表示します
末尾に以下のオプションを付けるとアクセスログも表示します
--access-logfile -

下のオプションを付けるとworkerの数を増やせます。workerとは、
それぞれが同じDjangoのアプリを動かすことができ、複数のリクエストを同時に処理できます。
ローカルでやる分には1個でいいので必要ないです。
--workers 2

動くが気になる動作がある。

root@dcc6cba1ce60:/code# gunicorn composeexample.wsgi:application --bind 0.0.0.0:8000
[2023-11-03 05:41:23 +0000] [14] [INFO] Starting gunicorn 21.2.0
[2023-11-03 05:41:23 +0000] [14] [INFO] Listening at: http://0.0.0.0:8000 (14)
[2023-11-03 05:41:23 +0000] [14] [INFO] Using worker: sync
[2023-11-03 05:41:23 +0000] [15] [INFO] Booting worker with pid: 15
ここでhttp://localhost:8000/にアクセスしてDjangoの画面が出たが、
30秒後にWORKER TIMEOUTになってしまう。
新しいworkerが立ち上がるので動作は続ける。
[2023-11-03 05:41:57 +0000] [14] [CRITICAL] WORKER TIMEOUT (pid:15)
[2023-11-03 05:41:57 +0000] [15] [INFO] Worker exiting (pid: 15)
[2023-11-03 05:41:57 +0000] [14] [ERROR] Worker (pid:15) exited with code 1
[2023-11-03 05:41:57 +0000] [14] [ERROR] Worker (pid:15) exited with code 1.
[2023-11-03 05:41:57 +0000] [16] [INFO] Booting worker with pid: 16

原因はhttps://devcenter.heroku.com/ja/articles/python-gunicornのワーカーのタイムアウトに書いてある。
ワーカーが過去 30 秒以内に何の処理も完了していない場合、
Gunicorn によって暗黙的にワーカーが再起動されます。
何の処理も完了していない」が何を意味するのか分からない。

オプションの--timeout 0を末尾に付けて実行すれば
停止、再起動はしなくなるが、ちゃんと動作しているのでこのままにする。
逆に0にして無効にしてしまうと、何かあった時に動かなくなるかも。
目次へ

Nginx+Gunicorn+Django

docker-compose.ymlを修正します。gunicornの起動も入れます。
portsはホスト側のポートとコンテナ側のポートをマッピング(つなげる)します。
今まではGunicorn+Djangoが入っているwebコンテナのポートがホストとつながっていました。
これをportsからexposeに変えます。
exposeはコンテナ側のポートを指定します。指定されたコンテナ側のポートはホストマシンには公開されず、仮想ブリッジにのみ晒されます。
晒されたコンテナ側のポートへは、リンクされたサービス(今回Nginx)からのみアクセスが可能です。
ホストとマッピングするのはNginxのコンテナのポートなので、nginxコンテナのところにportsの設定を書きます。
depends_on:でnginxコンテナがwebコンテナに依存(接続)することを示します。
ディレクトリ構成

docker-compose.yml

version: "3.9"
services:
  web:
    build: .
    command: gunicorn composeexample.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - .:/code
    expose:
      - 8000
  nginx:
    build: ./nginx
    ports:
        - 1317:80
    depends_on:
        - web  

外部とつながるNginxを作ります。上のNginxとつなげるもので別物です。
ルートにnginxフォルダを追加し、そこにDockerfileとnginx.confファイルを追加します。
Nginxコンテナはこれを元に作られます。
Dockerfileの中身

FROM nginx:1.19.0-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

FROM nginx:1.19.0-alpineはNginxのベースイメージからイメージ作成
2行目はdefault.confファイルを削除
3行目でVSCodeにあるconf.dをコンテナにコピー
つまり2、3行目はdefault.conf→nginx.confに入れ替えています。
(ネットで見る他のサンプルはdefault.confを消していないものも多い。要確認)

nginx.confの中身

upstream project {
    server web:8000;
}
 
server {
 
    listen 80;
 
    location / {
        proxy_pass http://project;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
 
}

個人的メモ:
upstream project {server web:8000;}
をなくして、以下にしたらダメだった。
location / {
proxy_pass http://web:8000;

http://www2.matsue-ct.ac.jp/home/kanayama/text/nginx/node79.html
を見ると「upstreamはサーバグループを定義します。」となっているので
なくしても問題ないはずです。

赤色の部分が重要と思います。
この設定ファイルは、リバースプロキシを設定しています。
リバースプロキシは、クライアントからのリクエストを受け取り、
バックエンドサーバーに転送するために使用されます。
今回、バックエンドサーバーは、http://projectで指定されたwebサーバー(webコンテナ)です。
listen 80;はNginxのポート80が受け口であることを指定しています。
ymlの設定でホストの1317ポートとくっついています。
proxy_passディレクティブは、リバースプロキシが受信したリクエストを転送するバックエンドサーバーを指定します。http://project;のprojectは上のupstream project {のprojectのこと。
proxy_set_headerディレクティブは、リクエストヘッダーに追加するヘッダーを指定してます。
Nginxで止めていた部分(Nginxが書き換えた部分)を止めないようにする設定だと思います。
$proxy_add_x_forwarded_forは、既に存在するX-Forwarded-ForヘッダーにクライアントのIPアドレスを追加するために使用されます。これにより、バックエンドサーバーは、リクエストを送信したクライアントのIPアドレスを識別できます。
proxy_set_header Host $host;は、リバースプロキシが受信したリクエストに、オリジナルのホスト名を追加するためのヘッダーを設定します。
proxy_redirectディレクティブは、リダイレクトを無効にします。(リダイレクトが、どこでリダイレクトすることを指しているのか要確認)

Dockerデスクトップで今回作ったコンテナとイメージをすべて削除します。
新しくイメージを作ります。
docker-compose up -d

http://localhost:1317/にDjangoの画面が表示されます。
目次へ

所感

これを本番運用するためにはDockerが動いて独自ドメインとコンテナが紐づく仕組みをもったものが必要です。私が無料でDjangoをデプロイしているRenderで、できるか考えます。まずrenderでnew→web serviceでgitレポジトリをコネクトすると入力画面が出て、その中のruntimeにDockerがあります。これ選んで、デプロイできるかというと多分できないと思います。少なくとも無料プランではメモリが500Mしかないのに今回のDockerのイメージが1Gもあるので無理だと思います。(イメージが全部メモリを使っているわけではないと思いますが)また、もともとNginx+gunicornの組み合わせは用意されていて、普段はDjangoのプロジェクトだけデプロイして使っています。Getting Started with Django on RenderにDjangoのデプロイの仕方が書いてあり。その中でrender.ymlを使ったdocker-compose.ymlに似たフォーマットを書いてデプロイする方法があります。こちらが本筋だと思います。なのでたとえDockerで万が一デプロイできたとしても、Pythonの環境など、かなり無駄な処理が入って動く可能性があります。この辺がVPSとかとPAAS(Render.com)とかの分かれ道なんだと思います。

この記事を書いたイチゲを応援する(質問でもokです)
Vプリカでのお支払いがおすすめです。

MENTAやってます(ichige)

コメント

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