docker でアプリ環境構築

## 構築環境

docker: 19.03.1

nginx: 1.17.3

python: 3.7.4

django: 2.2.4

uwsgi: 2.0.18

mysql: 8.0.17



## 構築準備

##### 作業フォルダ

docker にて環境構築を行う前に以下の構成を作成しておく

[C:\work\app]
├─db/　…　
│  └─init.sql　…　初期化用のSQL
├─logs/　…　ログ出力フォルダ
│  ├─test/　…　アプリケーション用
│  └─web/　…　web サーバ用
├─test/　…　作成するアプリケーション
│  ├─Dockerfile　…　アプリケーション構築定義
│  └─requirements.txt　…　pip インストール対象を記載
└─web/　…　
    ├─static/　…　静的コンテンツの配置場所
    └─nginx.conf　…　nginx 設定ファイル



```init.sql
CREATE DATABASE IF NOT EXISTS manage CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'admin'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON manage.* TO 'admin'@'%';

FLUSH PRIVILEGES;
```

データベース名を manage 、ユーザ/パスワードを admin/password で初期化を行う



```Dockerfile
FROM python:3.7.4
MAINTAINER Planaria Inc.

ENV PYTHONUNBUFFERED 1
ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

RUN mkdir /app
RUN mkdir /app/logs
WORKDIR /app
ADD requirements.txt /app/

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

COPY . /app/
```

文字コードやタイムゾーンを設定しつつ、作業フォルダとログ出力フォルダを作成

ローカルの requirements.txt をコピーし、pip にてインストールを行う



```requirements.txt
Django==2.2.4
uwsgi==2.0.18
mysqlclient==1.4.2.post1
```

インストール対象は上記の通り



```nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    #include /etc/nginx/conf.d/*.conf;

    server {

        listen       80;
        server_name  localhost;
        charset     utf-8;

        location /static {
            alias /static;
        }

        location / {
            include uwsgi_params;
            uwsgi_pass test:8001;
        }

    }

    server_tokens off;

}
```

デフォルトの nginx.conf を元に conf のインクルードをコメントアウトし、 server{} 、server_tokens を追加

nginx のコンテナ作成後、上記ファイルで上書きされる



##### 初期構築

django のプロジェクトを作成するための初期構築を行う

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

services:

  test:
    build: ./test
    ports:
      - "8000:8000"
    volumes:
      - ./test:/app
      - ./logs/test:/app/logs
    command: python manage.py runserver 0.0.0.0:8000
```

初期構築用の docker-compose.ymlを C:\work\app に配置する

構築された結果の設定ファイルを変更するため、コンテナ上のフォルダをローカルフォルダとリンクさせる



```
docker-compose run --rm test django-admin.py startproject config .
```

プロジェクト作成を docker-compose から実行

rm オプションをつけないと一時的に利用したコンテナが残り続けるので、個人的には必須



実行後、 test フォルダが次の構成となっている

[C:\work\app]
└─test/
    ├─config/
    │  ├─__ init__.py
    │  ├─settings.py
    │  ├─urls.py
    │  └─wsgi.py
    ├─logs/
    ├─Dockerfile
    ├─manage.py
    └─requirements.txt



作成された settings.py を2か所修正

```settings.py
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'manage',
        'USER': 'admin',
        'PASSWORD': 'password',
        'HOST': 'app_db',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4',
        },
    }
}
```

アプリケーションからのデータベース接続定義を修正する

ライブラリをデフォルトの sqlite から mysql に

データベース名、ユーザ/パスワードは init.sql で定義した内容を設定

ホスト名は mysql コンテナ名を設定している※後述



```settings.py
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'
STATIC_ROOT = '/static'
```

静的コンテンツの設定を修正



##### 環境構築定義

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

volumes:
  db.volume:

services:

  db:
    image: mysql:8.0.17
    container_name: app_db
    ports:
      - 3306:3306
    volumes:
      - db.volume:/var/lib/mysql
      - ./db:/docker-entrypoint-initdb.d
    command: mysqld --default-authentication-plugin=mysql_native_password --skip-mysqlx
    environment:
      MYSQL_ROOT_PASSWORD: mysql_password
      MYSQL_DATABASE: manage
      TZ: 'Asia/Tokyo'

  test:
    build: ./test
    container_name: app_test
    volumes:
      - ./test:/app
      - ./web/static:/static
      - ./logs/test:/app/logs
    command: uwsgi --socket :8001 --module config.wsgi --logto /app/logs/uwsgi.log
    expose:
      - "8001"
    depends_on:
      - db

  web:
    image: nginx:1.17.3
    container_name: app_web
    ports:
      - "8080:80"
    volumes:
      - ./web/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./web/static:/static
      - ./logs/web:/var/log/nginx
    depends_on:
      - test
```

環境構築用の docker-compose.ymlを C:\work\app に配置する



```
volumes:
  db.volume:
```

データベースの永続化するために db.volume を作成、mysql の volumes に設定される



```
  db:
    image: mysql:8.0.17
    container_name: app_db
    ports:
      - 3306:3306
    volumes:
      - db.volume:/var/lib/mysql
      - ./db:/docker-entrypoint-initdb.d
    command: mysqld --default-authentication-plugin=mysql_native_password --skip-mysqlx
    environment:
      MYSQL_ROOT_PASSWORD: mysql_password
      MYSQL_DATABASE: manage
      TZ: 'Asia/Tokyo'
```

container_name は settings.py の HOST に設定（サービス名 db でも認識される）

volumes として db.volume を指定、データを永続化する

init.sql（初期化用のSQL）を有効にするため、docker-entrypoint-initdb.d とリンクする

command で mysql の認証方式を mysql_native_password にするよう設定

※ mysql 8 を利用する場合のみ必要、mysql 5.7 の場合はデフォルトでnative password 方式なので不要

environment として MYSQL_ROOT_PASSWORD は必須（同意の設定が、何れか必要ぽい）



```
  test:
    build: ./test
    container_name: app_test
    volumes:
      - ./test:/app
      - ./web/static:/static
      - ./logs/test:/app/logs
    command: uwsgi --socket :8001 --module config.wsgi --logto /app/logs/application/uwsgi.log
    expose:
      - "8001"
    depends_on:
      - db
```

uwsgi 経由での設定とする為 command で設定



```
  web:
    image: nginx:1.17.3
    container_name: app_web
    ports:
      - "8080:80"
    volumes:
      - ./web/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./web/static:/static
      - ./logs/web:/var/log/nginx
    depends_on:
      - test
```

コンテナ 80 port を 8080 port として公開

nginx.conf（nginx 設定ファイル）を有効にするため、コンテナの nginx.conf とリンク

静的コンテンツの配置場所もリンクさせる



ココまでで構築準備が完了



## 環境構築

##### データベースの構築と接続設定

```
docker-compose run --rm test ./manage.py makemigrations
```

migrate の定義を作成する

test コンテナと db コンテナが依存関係であるため、まず db コンテナの作成が行われる

```
Can't connect to MySQL server on 'app_db' (115)
```

db コンテナの生成は正常終了するが、後続の makemigrations が上記エラーとなる

migrate の定義は作成されている模様



念のため、以下を順に確認していく（省略可

確認１：db コンテナの作成時のログを確認

```
docker logs app_db
```

 mysql のバージョンによって出力結果は全然異なるので注意

別用途で作成した volume を参照してしまっていた場合は、読み込みエラーになる場合がある

（何度か実行していたので、以前作成した volume を消し忘れたときに発生した）



確認２：ユーザと認証方式

```
docker-compose exec db mysql -uroot -pmysql_password
mysql> SELECT user, host, plugin FROM mysql.user;
+------------------+-----------+-----------------------+
| user             | host      | plugin                |
+------------------+-----------+-----------------------+
| admin            | %         | mysql_native_password |
| root             | %         | mysql_native_password |
| mysql.infoschema | localhost | caching_sha2_password |
| mysql.session    | localhost | caching_sha2_password |
| mysql.sys        | localhost | caching_sha2_password |
| root             | localhost | mysql_native_password |
+------------------+-----------+-----------------------+
6 rows in set (0.00 sec)
```

mysql 8 の場合の追加確認：root と admin が mysql_native_password になっていることを確認



確認３：接続先データベースの確認

```
docker-compose exec db mysql -uroot -pmysql_password
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| manage             |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)
```

manage が存在する



問題がなければ migrate を実行する

```
docker-compose run --rm test ./manage.py migrate
```

成功していれば、mysql にテーブルが作成されている

```
docker-compose exec db mysql -uroot -pmysql_password
mysql> show tables from manage;
+----------------------------+
| Tables_in_manage           |
+----------------------------+
| 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             |
+----------------------------+
10 rows in set (0.01 sec)
```



管理ユーザの作成

```
docker-compose run --rm test ./manage.py createsuperuser
Starting app_db ... done                                                                                                                                                                     Username (leave blank to use 'root'): admin
Email address: admin@example.com
Password:
Password (again):
The password is too similar to the username.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
```

パスワードが単純すぎますと怒られてるのはスルー



##### 静的コンテンツの配置

管理画面で使用する静的コンテンツを配置する

```
docker-compose run --rm test ./manage.py collectstatic
```

C:\work\app\web\static 以下に展開される



##### 動作確認

```
docker-compose up -d
```

上記でサーバ起動

ブラウザで localhost:8080 にアクセスすることで、django の初期ページが表示される

![docker01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464760/1fe33547-b08b-8904-4cce-5d6975082271.png)


管理画面は localhost:8080/admin/

![docker02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464760/6ca9c6df-1529-bd54-93f5-a117b77137ae.png)



管理画面からユーザ（test）を追加した場合、mysql にデータが登録されている

```
mysql> select id, username, email from manage.auth_user;
+----+----------+-------------------+
| id | username | email             |
+----+----------+-------------------+
|  1 | admin    | admin@example.com |
|  2 | test     |                   |
+----+----------+-------------------+
2 rows in set (0.00 sec)
```



## アプリケーションの追加

[はじめての Django アプリ作成](https://docs.djangoproject.com/ja/2.2/intro/tutorial01/)を参考にアプリケーションの追加を行う



##### アプリケーションの作成

```
docker-compose run --rm test ./manage.py startapp polls
```

test 内に追加するため、docker-compose から startapp でアプリケーションを追加する



以降はチュートリアル通りに作成できる

チュートリアルでのアプリ作成中に実行するコマンドは docker-compose 経由ですべて実行可能

```
# アプリケーションの追加
docker-compose run --rm test ./manage.py startapp polls
# コンテナの再起動
docker-compose restart test
# マイグレーション
docker-compose run --rm test ./manage.py makemigrations polls
docker-compose run --rm test ./manage.py migrate
# 静的コンテンツの配置
docker-compose run --rm test ./manage.py collectstatic
# インタプリタ実行
docker-compose run --rm test ./manage.py shell
# テスト実行
docker-compose run --rm test ./manage.py test polls
# テンプレート配置
docker-compose exec test python -c "import django; print(django.__path__)"
docker-compose exec test cp /usr/local/lib/python3.7/site-packages/django/contrib/admin/templates/admin/base_site.html /app/polls/templates/admin/

```



##### 自動テスト実行の設定変更

```
C:\work\app>docker-compose run --rm test ./manage.py test polls
Starting app_db ... done
Creating test database for alias 'default'...
Got an error creating the test database: (1044, "Access denied for user 'admin'@'%' to database 'test_manage'")
```

admin ユーザに自動テスト用のデータベースを作成する権限がないため発生する



テスト用のデータベースを作成し、自動テスト実行時にそれを参照するよう設定する

```settings.py
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'manage',
        'USER': 'admin',
        'PASSWORD': 'password',
        'HOST': 'app_db',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4',
        },
        'TEST': {
            'NAME': 'test_manage',
        },
    }
}
```

作成したテストデータベースへの権限付与

```
docker-compose exec db mysql -uroot -pmysql_password
mysql> GRANT ALL PRIVILEGES ON test_manage.* TO 'admin'@'%';
```



## python と django と MySQL

今回の環境構築では python 3.7.4 、django 2.2.4と mysql 8.0.17 を利用しているが、環境構築に至るまでに紆余曲折があったため、備忘録として記載する



原因としては２つ、

- django 2.2 が PyMySQL に対応できていない こと

-  mysql 8 のデフォルト認証方式が mysql 5.7 までの mysql_native_password  から caching_sha2_password に変更されたこと

なので、シンプルに構成する場合は python 3 、django 2.1 、 mysql 5.7 とすれば良い（はず...）



##### django 2.2 を利用して MySQL に接続する

django 2.2 から PyMySQL で MySQL に接続すると以下エラーが発生する

```
mysqlclient 1.3.13 or newer is required
```

[公式ドキュメント](https://docs.djangoproject.com/ja/2.2/ref/databases/#mysql-db-api-drivers)にもある通り、django 2.2 では mysqlclient の利用が推奨されているのでそちらを利用する



##### MySQL 8 を利用する

認証方式が caching_sha2_password の場合に MySQL に接続すると以下エラーが発生する

```
caching_sha2_password.so: cannot open shared object file: No such file or directory
```

フォーラム等も色々覗いてみたが、現状 caching_sha2_password への対応は現実的ではない模様

なので、認証方式を旧式の mysql_native_password  に戻してあげる必要がある



現状確認として以下 SQL を実行してみる

```
SELECT user, host, plugin FROM mysql.user;
```

plugin の結果が MySQL にログインするユーザの認証方式となる

caching_sha2_password と表示されている場合は、先のエラーが発生する



今回の環境構築では MySQL のコンテナ作成時にパラメータを指定することで認証方式の変更を行っている

```docker-compose.yml
command: mysqld --default-authentication-plugin=mysql_native_password --skip-mysqlx
```

mysql 起動時に認証方式が mysql_native_password に指定されるため、init.sql で作成されたユーザの認証方式は mysql_native_password となる



もし、すでに作成されているユーザに対して認証方式を変更したい場合は、以下SQLを実行

```
ALTER USER 'admin' IDENTIFIED WITH mysql_native_password BY 'password';
```

※上記はあくまで例なので admin/password は変更してネ



## リンク

http://docs.docker.jp/
http://docs.docker.jp/engine/reference/commandline/index.html
http://docs.docker.jp/compose/toc.html
https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html
https://docs.djangoproject.com/ja/2.2/