Django+MySQLの開発環境をdocker-composeで構築する

# はじめに

既に書き尽くされたような記事のタイトルですが、 **自分がわかりやすいように** 書いた備忘録です。
独学でやってて詰まったポイントや設定ファイルの記述内容など、コメントを付けつつ書いていきます。
Djangoチュートリアルのpollsアプリをマイグレートするところまでやります。

注意：結構長いです
最終的なソースコードは[こちら（github）](https://github.com/bakupen/docker-django-mysql)

# 環境
- OS: Mac
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
    - Docker version 19.03.12
    - docker-compose version 1.26.2

エディタはVSCodeで、[Docker拡張機能](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)を使用しています。
コンテナへのログインや、コンテナ再起動などGUIで操作できて便利です。

# 開発環境を構築する

webサービス（Django）とdbサービス（MySQL）のコンテナを立てていきます。

### 1.作業ディレクトリ作成

*最初はターミナルで操作します。*

docker-compose.ymlファイルが置いてあるディレクトリ名が、コンテナ名やボリューム名の接頭辞になります。
コンテナ名が既にあるものだとうまく立ち上がらないため、他で使ってないプロジェクト名などにします。

```code:bash
$ mkdir sample-pj
$ cd sample-pj/       # 作業ディレクトリに移動
```

### ファイルの配置

作った作業ディレクトリの中に、Dockerfile、docker-compose.yml、requirements.txtを配置します。

```code:bash
$ touch Dockerfile docker-compose.yml requirements.txt
```

### Dockerfile編集

*ここからはVSCodeで操作していきます。*
*[cmd + shift + m] -> [ターミナルタブ]でターミナル操作も一緒にできます。*

webサービス（Django）用のdockerイメージを作成するためのコマンドを書いていきます。
Docker Hubで提供されているpython:3実行環境のイメージに、いろいろ付け加えた新しいイメージを作ります。

```code:Dockerfile
FROM python:3            # 元となるdockerイメージを指定
ENV PYTHONUNBUFFERED 1   # この環境変数に値を入れることでバッファを無効化する('1'じゃなくてもいい)
RUN mkdir /code          # codeディレクトリを作成
WORKDIR /code            # codeディレクトリに移動
COPY requirements.txt /code/   # txtファイルをcodeディレクトリに配置
RUN pip install --upgrade pip && pip install -r requirements.txt
          # pipコマンドを最新にし、txtファイル内のパッケージ（後述）をpipインストール
COPY . /code/            # sample-pj/配下のファイルをcodeディレクトリにコピー
```

### requirements.txt編集

webサービスにインストールするパッケージを指定

```code:requirements.txt
Django==3.1   # Django3.1
mysqlclient   # pythonでMySQLに接続するためのドライバ
```

### docker-compose.yml編集

コンテナ作成の命令

```code:docker-compose.yml
version: '3'       # 1
services:          # 2
  db:
    image: mysql:5.7    # 3
    environment:                  # 4
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: django-db
      MYSQL_USER: django
      MYSQL_PASSWORD: django
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
                                  # 5
  web:
    build: .       # 6
    command: python3 manage.py runserver 0.0.0.0:8000 # 7
    volumes:                  # 8
      - .:/code
    ports:                    # 9
      - "8000:8000"
    depends_on:               # 10
      - db
```

10箇所、解説します。

1. version: '3'
    - docker-compose.ymlのファイル形式のバージョンを指定します
2. services
    - dbサービスとwebサービス、2つのコンテナが起動します
    - サービス名は好きな名前で大丈夫です。`[docker-compose.ymlがあるディレクトリの名前]_[サービス名]`という名前のコンテナが立ち上がります
3. `image: mysql:5.7`
    - Docker Hubで提供されているmysql:5.7のイメージをそのまま使うため、Dockerfileは使いません
    - 最新でなく5.7にしているのは、mysqlの認証方式が8.0から変わっており、webサーバからうまく接続できなかったためです
    - 参考記事：[PythonからMySQLへの接続でcaching_sha2_passwordのエラーが出た場合](https://qiita.com/satoryosato@github/items/611809ebfb28c689ae8e)
4. environment
    - `MYSQL_ROOT_PASSWORD: root`
        - ルートユーザのパスワードを設定します
        - ルートユーザ自体は自動で作成されているようです
    - `MYSQL_DATABASE: django-db`
        - Djangoのプロジェクトで使うDBの名前を設定します
    - `MYSQL_USER: django`
        - Djangoのプロジェクトから接続する際のユーザ名を設定します
    - `MYSQL_PASSWORD: django`
        - 上で設定したユーザのパスワードを設定します
    - `TZ: 'Asia/Tokyo'`
        - dbサーバのタイムゾーンを設定します
5. `command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci`
    - コンテナ起動時に実行されるコマンドです
    - MySQLのデフォルト文字コードがlatin1なので、utf8に変換します
    - 参考記事：[Docker公式イメージのMySQLで文字コードを指定する](https://qiita.com/neko-neko/items/de8ea13bbad32140de87)
6. `build: .`
    - Dockerfileをもとにイメージをビルドします
    - `.`はDockerfileがある場所です
7. `command: python3 manage.py runserver 0.0.0.0:8000`
    - コンテナ起動時に（引数にコマンドを指定しなかったときだけ）実行されるコマンドです
    - Djangoの軽量サーバーを、ポート指定して立ち上げます
8. volumes
    - .:/code
    - カレントディレクトリを`/code`にバインドマウントします
9. ports
    - "8000:8000"
    - 左は公開用のポート、右は転送先コンテナのポート
    - 転送先ポートは、7で指定したDjangoサーバーのポートに合わせる必要があります
10. depends_on:
    - 先にdbサービスを起動してからwebサービスを起動するように設定します

これで、2つのコンテナを立ち上げるための準備が完了しました。

### Djangoのプロジェクトを作成

webサービスのコンテナを起動し、Djangoのプロジェクト作成コマンドを実行

```code:bash
docker-compose run web django-admin.py startproject djangopj .
```

- docker-compose run web
    - webサービスだけを指定します
- django-admin.py startproject djangopj .
    - djangopjという名前のプロジェクトを作成します
    - このカレントディレクトリは、Dockerfileで指定した`/code`です

コマンドを実行すると、プロジェクトディレクトリとmanage.pyが作られています。
コンテナの`/code`ディレクトリと、現在のカレントディレクトリがバインドマウントされているためです。

```code:bash
$ ls
Dockerfile         docker-compose.yml requirements.txt
djangopj           manage.py
```

### DBの設定を編集

`djangopj/settings.py`を開き、DATABASESの項目を編集します。

```code:settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'django-db',
        'USER': 'django',
        'PASSWORD': 'django',
        'HOST': 'db',
        'PORT': '3306'
    }
}
```

NAME、USER、PASSWORDにはdocker-compose.ymlで設定したものを記載します。
HOSTにはサービス名を記載します。

### 各サービスを起動

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

-dオプションはデタッチドモードです。
後ろでコンテナが動き続けます。

http://localhost:8000 にアクセスすると、Djangoのトップページが表示されると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408585/02aacfbe-2b83-ba95-a39b-97cc567e6f22.png)

表示されない場合、コンテナの実行ログを確認してみましょう。
VSCodeのDocker拡張機能を入れている場合、左のDockerタブから簡単にログにアクセスできます。


確認したいコンテナの上で右クリック -> `View Logs`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408585/353eadac-507c-1076-6682-39894d570d6a.png)

dbサービスの起動完了が遅く、webサービスの起動がこけることがあるので
webサービス右クリックで `Restart` をかけると解決します。

アクセスできたら、MySQL側にDjango用のDBが作られているかどうかも確認してみましょう。

同じようにdbサービスのコンテナの上で右クリックし、
`Attach Shell` をクリックします。

すると、ターミナル画面でdbサービスに入った状態になるので、
mysqlにログインします。

```code:mysql
# mysql -u root -proot
```

DB一覧を表示させると、、

```code:mysql
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| django-db          |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)
```

無事、django-dbが作成されていました！

# pollsアプリでデータ連携の確認

webサービスとdbサービスが連携できているか確認するために、
Djangoの公式チュートリアルを参考に小さなアプリを作ります。
https://docs.djangoproject.com/ja/3.1/intro/tutorial01/#creating-the-polls-app

### アプリケーションをつくる

docker-compose.ymlのあるディレクトリ（sample-pj）に移動し、以下コマンドを叩きます。

```code:bash
$ docker-compose run web python3 manage.py startapp polls
```

`ls`して、pollsというディレクトリができていれば作成完了です。

### モデルを作る

pollsディレクトリにあるmodels.pyを編集

```models.py
from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
```

djangopj/settings.pyのINSTALLED_APPSにpollsアプリケーションを追加します。

```settings.py
INSTALLED_APPS = [
    'polls.apps.PollsConfig',     # 追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```

### モデルをDBに適用

docker-compose.ymlのあるディレクトリ（sample-pj）に戻り、以下コマンドを叩きます。

```code:bash
$ docker-compose run web python3 manage.py makemigrations polls
$ docker-compose run web python3 manage.py migrate
```

これでDBにモデルが適用されました。MySQL側で確認してみます。

```code:mysql
mysql> use django-db;
mysql> show tables;
+----------------------------+
| Tables_in_django-db        |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
| polls_choice               |
| polls_question             |
+----------------------------+
12 rows in set (0.00 sec)
```

polls_choice、polls_questionテーブルが作成されました。

[こちら](https://docs.djangoproject.com/ja/3.1/intro/tutorial02/#creating-an-admin-user)を参考に、管理画面からデータを入れてみると、
テーブルの中身にデータが登録されているのも確認できます。

```code:mysql
mysql> select * from polls_question;
+----+---------------+----------------------------+
| id | question_text | pub_date                   |
+----+---------------+----------------------------+
|  1 | test          | 2020-08-21 08:58:07.000000 |
+----+---------------+----------------------------+
1 row in set (0.00 sec)
```

これで、webサービスとdbサービスでデータのやりとりができていることが確認できました。

# あとがき

docker-compose.ymlの記述内容と、settings.pyのDATABASESの書き方が謎で
ずっとモヤモヤしていたのが、最近やっとすっきりしたので記事にしてみました。

個人的には、webコンテナをリスタートしないといけないのがイケてないので
改善の余地ありかなと思います。

VSCode拡張機能の良さが伝わってたら幸いです。