動作環境とこれまでの経緯
- ホスト環境
- macOS Catalina 10.15.6
- VirtualBox 6.1.14
- ゲスト環境
- Ubuntu Server 20.04.1 LTS
- Docker 19.03.13
- Docker Compose 1.27.3
- これまでの経緯
というわけで、Web アプリケーションを開発するために、Django+MySQL+nginx というマッスルドッキングな開発環境を Docker で構築していきたいと思います。
ディレクトリ構成
当初のディレクトリ構成は次のとおり。
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 にたどり着きました。長い道中を振り返れば、思わず目頭が熱くなる。
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
Django の設定
なにはともあれ、Django の設定から。
「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 の3つ。
Django==2.2.12
uwsgi==2.0.18
mysqlclient==1.4.6
ここまでの設定を踏まえて docker-compose.yml
で Django を設定すると、次のとおりになります。
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 は嫌いなのです。
マニュアルなどをいろいろと調べた結果、どうやらこれがベストプラクティスらしい。しらんけど。
FROM mysql:5.7
COPY init.d/* /docker-entrypoint-initdb.d/
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 を設定すると、次のとおりになります。
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」を設定します。
volumes:
app.db.volume:
name: app.db.volume
nginx の設定
uWSGI の設定もよく分かりません。正直そこまで uWSGI のことをガッツリ調べる気にならんかった。すまん。
次のとおり、適当にパラメータを設定しておけばいいらしい。しらんけど。
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 番に流すように設定しているからです。
これで、ホストのブラウザで「127.0.0.1:8081」にアクセスすれば、ゲストで動く Web アプリが見られるというわけ。
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 でレスポンスを返せます。
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」の項目も設定します。
networks:
django_net:
driver: bridge
Django プロジェクトの設定
さて、ここまでで Docker の設定は全部終わりました。あとは Django 本体の設定を残すだけです。
まずはプロジェクトを立ち上げましょう。
$ docker-compose run python django-admin.py startproject app .
このコマンドを叩いた後で src
と static
の中を覗くと、いろいろと Django 用のファイルができているはず。
次に、./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 で叩くだけです。
$ docker-compose run python ./manage.py migrate # DBマイグレーション
$ docker-compose run python ./manage.py createsuperuser # 管理者の設定
$ docker-compose run python ./manage.py collectstatic # 静的ファイルをコピー
コンテナの起動と停止
さて、いよいよコンテナを動かします。
出でよ Django!
$ docker-compose up -d
そして、ホストのブラウザから 127.0.0.1:8081 にアクセスすると…
やったぜ!!
ハロー!Docker!
ハロー!Django!
コンテナを停止する時は、stop
します。
$ 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 では、行頭に『-』をつけることで配列を表現します。『-』のあとには半角スペースを入れてください」という記載が。
よく見ると、ここ、
volumes:
- ./src:/code
が、
volumes:
-./src:/code # 「-」 の後に半角スペースがない
こうなってた。これに気づくのに2時間かかった。
このクソ計算機が!!お前は親戚のめんどくさいオッサンか!!
たった半角スペースが1個なかっただけでこの仕打ち。もうやってらんない。そもそも「ヤムル」とかいう名前が気に食わない。韓国料理かよ(それはナムル)。
次に、このエラーメッセージ。
Unsupported config option for services.nginx: 'deponds_on'
よく見たら分かるのですが、deponds_on
ではなく、depends_on
が正しい。アホみたいなスペルミスだが、これに気づくのに2時間かかった。
このクソ計算機が!!私の貴重な人生の時間を返せ!!
ディープラーニングで顔認識とか正直どうでもよくて、この手のヒューマンエラーをそっと修正してくれる気の利いた計算機が欲しい。
はい、さらにこのエラーメッセージ。
ERROR: Service 'nginx' depends on service 'python' which is undefined.
なんでこんなエラーが出ていたかというと、python
を pithon
とミスタイプしていたから(最初のエラーメッセージ参照)。これに気づくのに1時間かかった。
このクソ計算機が!!てめえの頭はハッピーセットかよ!!
2コマ漫画みたいな展開に我ながら愕然としますが、Dvorak 配列では、y と i が上下で隣り合わせであることが原因と思われます。
次回をお楽しみに
さて、私がどれほど計算機を憎んでいるか、なぜ大学院で機械学習を専攻しようと考えたか、そしてなぜ研究者もエンジニアも辞めたか、お分かりいただけたでしょうか(本稿の趣旨とは全然違う話)。
オモチャとして遊ぶにはちょうどいいが、これを仕事にする気にはなれない。
要するに、どう見ても無才です。本当にありがとうございました。
最後は大変ハートウォーミングなオチになってしまいましたが、次回は AWS に本番環境を作っていきたいと思います。乞うご期待。