Django＋MySQL＋nginx の開発環境を Docker Compose で構築する

# 動作環境とこれまでの経緯

- ホスト環境
    - macOS Catalina 10.15.6
    - VirtualBox 6.1.14
- ゲスト環境
    - Ubuntu Server 20.04.1 LTS
    - Docker 19.03.13
    - Docker Compose 1.27.3
- これまでの経緯
    - [ローカルに Ubuntu Server を用意した](https://qiita.com/hajime-f/items/5d3373e98985776b4240)
    - [そこに Docker Compose をインストールした](https://qiita.com/hajime-f/items/2bd1cad93d3ae4a62be8)

というわけで、Web アプリケーションを開発するために、**Django＋MySQL＋nginx というマッスルドッキングな開発環境**を Docker で構築していきたいと思います。

# ディレクトリ構成
当初のディレクトリ構成は次のとおり。

```bash
app
├── docker-compose.yml
├── mysql
│   ├── Dockerfile
│   └── init.d
│       └── init.sql
├── nginx
│   ├── conf
│   │   └── app_nginx.conf
│   └── uwsgi_params
└── python
    ├── Dockerfile
    └── requirements.txt
```

この状態から
- docker-compose.yml
- Dockerfile（python）
- requirements.txt
- Dockerfile（mysql）
- init.sql
- app_nginx.conf
- uwsgi_params
の7つのファイルを編集することになります。

ちなみに、以下では Django で「app」というアプリケーションを作る例を説明しています。各自で作りたいアプリケーションの名前に置き換えてね。

# docker-compose.yml は最終的にこうなった

最初に結果を書きましょう。
ググったり本を読んだりしながら試行錯誤し、無慈悲なエラーメッセージと戦いながら、最終的にこんな docker-compose.yml にたどり着きました。長い道中を振り返れば、思わず目頭が熱くなる。

```yml:docker-compose.yml
version: '3.7'

services:
  python:
    build:
      context: ./python
      dockerfile: Dockerfile
    command: uwsgi --socket :8001 --module app.wsgi --py-autoreload 1 --logto /tmp/uwsgi.log
    restart: unless-stopped
    container_name: Django
    networks:
      - django_net
    volumes:
      - ./src:/code
      - ./static:/static
    expose:
      - "8001"
    depends_on:
      - db

  db:
    build:
      context: ./mysql
      dockerfile: Dockerfile
    restart: unless-stopped
    container_name: MySQL
    networks:
      - django_net
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "*******"
      TZ: "Asia/Tokyo"
    volumes:
      - app.db.volume:/var/lib/mysql
      - ./mysql/init.d:/docker-entrypont-initdb.d

  nginx:
    image: nginx:1.17
    restart: unless-stopped
    container_name: nginx
    networks:
      - django_net
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
      - ./static:/static
    depends_on:
      - python

networks:
  django_net:
    driver: bridge

volumes:
  app.db.volume:
    name: app.db.volume
```

構成を適当に略記すると、こんなイメージかしら。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667942/1eec7e3b-f929-586e-d561-d982f8f09df6.png)

# Django の設定

なにはともあれ、Django の設定から。
「python」ディレクトリ直下の ```Dockerfile``` を、次のように編集します。

```:./python/Dockerfile
FROM python:3.8.5
WORKDIR /code
ADD requirements.txt /code/
RUN pip install --upgrade pip && pip install -r requirements.txt
ADD . /code/
```

まず、```code``` という名前の作業ディレクトリを作成します。そのディレクトリに ```requirements.txt``` を追加した後、```pip``` で必要なパッケージをインストールして、最後に ```code``` をイメージに追加します。

で、```pip``` でインストールするパッケージは、次のように ```requirements.txt``` に列挙しておきます。今回必要になるのは、Django 本体と、nginx とソケット通信するための uWSGI と、MySQL に接続するための mysqlclient の３つ。

```:./python/requirements.txt
Django==2.2.12
uwsgi==2.0.18
mysqlclient==1.4.6
```

ここまでの設定を踏まえて ```docker-compose.yml``` で Django を設定すると、次のとおりになります。

```yml:docker-compose.yml（抜粋）
python:
  build:
    context: ./python        # docker-compose.yml からの Dockerfile の相対パス
    dockerfile: Dockerfile   # 「Dockerfile」 に設定を書いてますよ、という明示的な指定
  command: uwsgi --socket :8001 --module app.wsgi --py-autoreload 1 --logto /tmp/uwsgi.log
  restart: unless-stopped    # コンテナが異常停止した場合は再起動する
  container_name: Django     # コンテナ名を定義
  networks:
    - django_net             # ネットワーク名「django_net」を指定
  volumes:
    - ./src:/code            # /code に ./src をマウントする
    - ./static:/static       # /static に ./static をマウントする
  expose:
    - "8001"     # uwsgi が nginx とソケット通信するために、8001 番のポートを開けておく
  depends_on:
    - db
```

uWSGI の設定がややこしいですが、```app.wsgi``` の部分が ```[Django のプロジェクト名].wsgi``` であることを守れば、あとはおまじないです。

# MySQL の設定

MySQL の設定は、正直よく分かりません。だいたい、私はクエリが満足に書けないレベルで RDB は嫌いなのです。

マニュアルなどをいろいろと調べた結果、どうやらこれがベストプラクティスらしい。しらんけど。

```:./mysql/Dockerfile
FROM mysql:5.7
COPY init.d/* /docker-entrypoint-initdb.d/
```

```sql:./mysql/init.d/init.sql
CREATE DATABASE IF NOT EXISTS app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'app_user'@'%' IDENTIFIED BY '[パスワード]';
GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'%';
FLUSH PRIVILEGES;
```

ちなみに、上記の ```init.sql``` で作成した ```app.db```（データベースの名前）は、後述の ```settings.py``` の設定に必要になるので、忘れないように。

この設定に基づいて、```docker-compose.yml``` で MySQL を設定すると、次のとおりになります。

```yml:docker-compose.yml（抜粋）
db:
  build:
    context: ./mysql         # docker-compose.yml からの Dockerfile の相対パス
    dockerfile: Dockerfile   # 「Dockerfile」 に設定を書いてますよ、という明示的な指定
  restart: unless-stopped    # コンテナが異常停止した場合は再起動する
  container_name: MySQL      # コンテナ名を定義
  networks:
    - django_net             # ネットワーク名「django_net」を指定
  ports:
    - "3306:3306"            # 通信するポート番号を指定
  environment:
    MYSQL_ROOT_PASSWORD: "*******"  # ルートパスワードを環境変数で設定
    TZ: "Asia/Tokyo"                # タイムゾーンを環境変数で設定
  volumes:
    - app.db.volume:/var/lib/mysql   # データベースをボリューム「app.db.volume」に保存
    - ./mysql/init.d:/docker-entrypont-initdb.d
```

「app.db.volume」という名前のボリュームにデータベースを保存することにしたので、「volumes」を設定します。

```yml:docker-compose.yml（抜粋）
volumes:
  app.db.volume:
    name: app.db.volume
```

# nginx の設定

uWSGI の設定もよく分かりません。正直そこまで uWSGI のことをガッツリ調べる気にならんかった。すまん。

次のとおり、適当にパラメータを設定しておけばいいらしい。しらんけど。

```:./nginx/uwsgi_params
uwsgi_param  QUERY_STRING       $query_string;
uwsgi_param  REQUEST_METHOD     $request_method;
uwsgi_param  CONTENT_TYPE       $content_type;
uwsgi_param  CONTENT_LENGTH     $content_length;

uwsgi_param  REQUEST_URI        $request_uri;
uwsgi_param  PATH_INFO          $document_uri;
uwsgi_param  DOCUMENT_ROOT      $document_root;
uwsgi_param  SERVER_PROTOCOL    $server_protocol;
uwsgi_param  REQUEST_SCHEME     $scheme;
uwsgi_param  HTTPS              $https if_not_empty;

uwsgi_param  REMOTE_ADDR        $remote_addr;
uwsgi_param  REMOTE_PORT        $remote_port;
uwsgi_param  SERVER_PORT        $server_port;
uwsgi_param  SERVER_NAME        $server_name;
```

で、nginx の設定なのですが、まず uWSGI でソケット通信するためのポート 8001 番を開けてやる必要があります。

また、nginx の待ち受けポートを設定します。私の設定は 80 番になっているのですが、これはコンテナを動かす仮想マシンの設定で、[ゲスト（仮想マシン）の 80 番を流れるデータを、ホストの 8081 番に流すように設定している](https://qiita.com/hajime-f/items/5d3373e98985776b4240)からです。

これで、ホストのブラウザで「127.0.0.1:8081」にアクセスすれば、ゲストで動く Web アプリが見られるというわけ。

```conf:./nginx/conf/app_nginx.conf
upstream django {
  ip_hash;
  server python:8001;  # uWSGI で Django と nginx とが通信するためのポート
}

server {
  listen      80;      # 待ち受けポート
  server_name 127.0.0.1;
  charset     utf-8;

  location /static {
    alias /static;
  }
  
  client_max_body_size 75M;
  
  location / {
    uwsgi_pass  django;
    include     /etc/nginx/uwsgi_params;
  }
}

server_tokens off;
```

この設定に基づいて、```docker-compose.yml``` で nginx を設定すると、次のとおりになります。Django の設定と同様に、```/static``` に ```./static``` をマウントしているので、Django で処理しない静的コンテンツは nginx でレスポンスを返せます。

```yml:docker-compose.yml（抜粋）
nginx:
  image: nginx:1.17         # nginx のイメージを適当に取ってくる
  restart: unless-stopped   # コンテナが異常停止した場合は再起動する
  container_name: nginx     # コンテナ名を定義
  networks:
    - django_net            # ネットワーク名「django_net」を指定
  ports:
    - "80:80"               # 通信するポート番号を指定
  volumes:
    - ./nginx/conf:/etc/nginx/conf.d
    - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
    - ./static:/static      # /static に ./static をマウントする
  depends_on:
    - python
```

あと、ここまで「django_net」を使うと言っておきながら全然設定していなかったので、忘れずに「networks」の項目も設定します。

```yml:docker-compose.yml（抜粋）
networks:
  django_net:
    driver: bridge
```

# Django プロジェクトの設定

さて、ここまでで Docker の設定は全部終わりました。あとは Django 本体の設定を残すだけです。

まずはプロジェクトを立ち上げましょう。

```bash
$ docker-compose run python django-admin.py startproject app .
```

このコマンドを叩いた後で ```src``` と ```static``` の中を覗くと、いろいろと Django 用のファイルができているはず。

次に、```./src/app/settings.py``` を編集します。

```python:./src/app/settings.py
DATABASES = {
    'default': {
        # 'ENGINE': 'django.db.backends.sqlite3',    # コメントアウト
        'ENGINE': 'django.db.backends.mysql',        # 追加
        # 'NAME': os.path.join(BASE_DIR, 'app_db'),  # コメントアウト
        'NAME': 'app_db',    # init.sql で CREATE したデータベース名と同じことを確認
        'USER': 'app_user',  # 追加
        'PASSWORD': '*****', # 追加（環境変数で指定したパスワードと同じもの）
        'PORT': '3306',      # 追加
        'HOST': 'db',        # 追加
    }
}

STATIC_URL = '/static/'   # 追加
STATIC_ROOT = '/static/'  # 追加

...（中略）

LANGUAGE_CODE = 'ja'      # 編集
TIME_ZONE = 'Asia/Tokyo'  # 編集
```

あとは Django でおなじみのコマンドを docker-compose で叩くだけです。

```bash
$ docker-compose run python ./manage.py migrate          # DBマイグレーション
$ docker-compose run python ./manage.py createsuperuser  # 管理者の設定
$ docker-compose run python ./manage.py collectstatic    # 静的ファイルをコピー
```

# コンテナの起動と停止

さて、いよいよコンテナを動かします。
出でよ Django！

```bash
$ docker-compose up -d
```

そして、ホストのブラウザから 127.0.0.1:8081 にアクセスすると…
![スクリーンショット 2020-10-27 21.26.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667942/4135f80f-37d3-e738-174f-057fbf613e57.png)
やったぜ！！
**ハロー！Docker！
ハロー！Django！**

コンテナを停止する時は、```stop``` します。

```bash
$ docker-compose stop
```

# 無慈悲なエラーメッセージたち

さて、こんな感じで記事にまとめると、まるで私がサクサクと設定したように誤解されそうですので、どれほど七転八倒しながら紆余曲折を経たかを書き残しておこうと思います。

まずこのエラーメッセージ。

```
ERROR: The Compose file './docker-compose.yml' is invalid because:
services.pithon.volumes contains an invalid type, it should be an array
```

なんやねんこれ、何が悪いねん、と憤りながらググりまくったのですが、直接的な解決法は見つかりません。

ふと「it should be array」（そこは配列のはずなのに、そうなってないでしょ）がポイントであることに気づき、YAML の構文を調べたところ……[「YAML では、行頭に『-』をつけることで配列を表現します。『-』のあとには半角スペースを入れてください」](https://magazine.rubyist.net/articles/0009/0009-YAML.html#%E9%85%8D%E5%88%97)という記載が。

よく見ると、ここ、

```yml
volumes:
  - ./src:/code
```

が、

```yml
volumes:
  -./src:/code  # 「-」 の後に半角スペースがない
```

こうなってた。**これに気づくのに2時間かかった**。

**このクソ計算機が！！お前は親戚のめんどくさいオッサンか！！**
たった半角スペースが１個なかっただけでこの仕打ち。もうやってらんない。そもそも「ヤムル」とかいう名前が気に食わない。韓国料理かよ（それはナムル）。

次に、このエラーメッセージ。

```
Unsupported config option for services.nginx: 'deponds_on'
```

よく見たら分かるのですが、```deponds_on``` ではなく、```depends_on``` が正しい。アホみたいなスペルミスだが、**これに気づくのに2時間かかった**。

**このクソ計算機が！！私の貴重な人生の時間を返せ！！**
[ディープラーニングで顔認識](https://qiita.com/hajime-f/items/f2cdae75bcb033322ec8)とか正直どうでもよくて、この手のヒューマンエラーをそっと修正してくれる気の利いた計算機が欲しい。

はい、さらにこのエラーメッセージ。

```
ERROR: Service 'nginx' depends on service 'python' which is undefined.
```

なんでこんなエラーが出ていたかというと、```python``` を ```pithon``` とミスタイプしていたから（最初のエラーメッセージ参照）。**これに気づくのに1時間かかった**。

**このクソ計算機が！！てめえの頭はハッピーセットかよ！！**
2コマ漫画みたいな展開に我ながら愕然としますが、[Dvorak 配列では、y と i が上下で隣り合わせである](https://qiita.com/hajime-f/items/c74122cded27cd7447fd)ことが原因と思われます。

# 次回をお楽しみに

さて、私がどれほど計算機を憎んでいるか、なぜ大学院で機械学習を専攻しようと考えたか、そしてなぜ研究者もエンジニアも辞めたか、お分かりいただけたでしょうか（本稿の趣旨とは全然違う話）。

**オモチャとして遊ぶにはちょうどいいが、これを仕事にする気にはなれない**。
要するに、どう見ても無才です。本当にありがとうございました。

最後は大変ハートウォーミングなオチになってしまいましたが、次回は AWS に本番環境を作っていきたいと思います。乞うご期待。