DockerでNGINX + NGINX Unit + MySQLの環境を構築


## Dockerの導入

以前、NGINX + NGINX Unit + Flaskの環境を構築しました。

[NGINX + NGINX Unit + Flask で PythonのWeb アプリを動かす](https://qiita.com/t_okkan/items/738832a87c3e157bddec)

前回はVagrantとVirtualBoxで構築しましたが、今回はDockerで構築していこうと思います。
前回と同様に、Webサーバーに`NGINX`を、APサーバーに`NGINX Unit`を、フレームワークに`Flask`を使用します。
更に今回はデータベースとして`MySQL`を追加し、`WEB <-> AP <-> DB`の環境を構築をしていきます。

以下の図のようなイメージです。(本来コンテナはホスト内で起動しますが、わかりやすように分けています)

![docker1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/c04a2090-cfac-0a79-9579-e82aebb213ef.png)

順を追って構築していくため、できたものをみたい方はGitHubに上げてありますのでそちらをどうぞ。

[https://github.com/txkxyx/docker-web](https://github.com/txkxyx/docker-web)

## 環境

以下の環境で構築していきます。

- ホスト
    - OS : macOS Catalina 10.15.3
    - Docker : 19.03.5
- コンテナ
    - Python : 3.7.3
    - Flask : 1.1.1
    - NGINX : 1.17.7
    - NGINX Unit : 1.14.0
    - MySQL : 8.0.18
    - Flask SQLAclchemy : 2.4.1

ディレクトリ構成は以下のようにします。

```console
./web
    |- db                   // DB用
    |- nginx                // NGINX用
    |- python               // NGINX Unit・ソースファイル用
    |    |- src
    |- docker-compose.yml
```

では始めていきます。

## Dockerfiletとdocker-composeの設定値

Dokerfileとdocker-composeで使用する設定値を簡単にまとめておきます。

### Dockerfileの設定値

詳しくは公式のDockerfileリファレンスを参照してください。

[https://docs.docker.com/engine/reference/builder/](https://docs.docker.com/engine/reference/builder/)

| 設定値 | 概要 |
| --- | --- |
| FROM | 使用するイメージを指定する。 |
| WORKDIR | 作業ディレクトリを指定する。この宣言以降はコンテナ内の指定したパスで作業を行う。 |
| COPY | ホストからコンテナに、指定したディレクトリやファイルをコピーする。`ホスト コンテナ`の順で指定する。`.dockerignore`で指定したファイルは対象外となる。 |
| RUN | 指定したコマンドを現時点のコンテナ内で実行する。（ビルド時に実行するコマンド） |
| CMD | コンテナ起動時に実行するコマンドを指定する。（起動時に実行するコマンド） |

### docker-composeの設定値

詳しくは公式のリファレンスを参照してください。

[https://docs.docker.com/compose/compose-file/](https://docs.docker.com/compose/compose-file/)

| 設定値 | 概要 |
| --- | --- |
| version  |  Docker Engineが対応するファイルフォーマットのバージョン |
| services |  アプリケーションを構成する各要素 |
| build  |  起動するコンテナの`Dockerfile`があるディレクトリを指定。子要素で、context(DockerfileのあるディレクトリまたはGithubURL)args(Dockerfileに渡す引数)などを指定できる 。|
| image  |  起動するコンテナが使用するイメージを指定する。 |
| command | docker-compose up を実行した際に実行されるコマンド  |
| ports  | コンテナが公開するポートを指定します。`ホスト：コンテナ`もしくはコンテナのポートのみ指定します。  |
| expose |  リンクするコンテナのみに公開するコンテナのポートを指定します。ホストには公開されません。 |
| environment  | 起動するコンテナの環境変数を指定します。  |
| volumes  | コンテナにマウントするホストのディレクトリを指定します。`ホスト:コンテナ`の形式でパスを指定します。  |
| container_name  | 起動するコンテナのコンテナ名を指定します。  |
| depends_on  | サービス間の依存関係を指定します。指定したサービス名が先に起動します。  |

## DBコンテナの構築

まずはMySQLのコンテナを構築していきます。
イメージはこんな感じです。

![docker2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/9cca6666-a1ea-3d29-3ff7-e56b48b38e0f.png)


`docker-compose.yml`を作成します。

```yaml:web/docker-compose.yml
version: "3"

services:
    db:
        image: mysql
        command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
        ports:
            - "33306:3306"
        expose:
            - "3306"
        environment:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_USER: test
            MYSQL_PASSWORD: test
        volumes:
            - ./db/init:/docker-entrypoint-initdb.d
        container_name: app_db
```

コンテナの初期起動時にデータベースを作成するように、`db`ディレクトリ内に`init`ディレクトリを作成し、createdatabase.sqlを作成します。

```sql:web/db/init/createdatabase.sql
CREATE DATABASE app;
USE app;

CREATE TABLE users(
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    email VARCHAR(255)
);

INSERT INTO users(name,email) VALUES('sample','sample@sample.com');
INSERT INTO users(name,email) VALUES('test','test@test.com');
INSERT INTO users(name,email) VALUES('app','app@app.com');

GRANT ALL ON app.* TO test;
```

以上の設定でdocker-composeでMySQLのコンテナを起動します。

```cosole
$ docker-compose up
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                NAMES
bef9a864276c        mysql               "docker-entrypoint.s…"   4 minutes ago       Up 4 minutes        33060/tcp, 0.0.0.0:33306->3306/tcp   app_db
```

`docker ps`の結果に`app_db`が表示されればコンテナを起動できています。
一度コンテナ内に入って、データベースが作成されているかを確認します。

```console
$ docker exec -it app_db bash
root@00000000000:/# mysql -u test -p
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| app                |
| information_schema |
+--------------------+
2 rows in set (0.00 sec)
mysql> use app;
mysql> select * from users;
+----+--------+-------------------+
| id | name   | email             |
+----+--------+-------------------+
|  1 | sample | sample@sample.com |
|  2 | test   | test@test.com     |
|  3 | app    | app@app.com       |
+----+--------+-------------------+
3 rows in set (0.01 sec)
```

`app`というデータベースが作成されていることが確認できます。更に、`createdatabase.sql`のテーブルとデータが作成されていることが確認できます。
MySQLの構築は以上です。

## APコンテナの構築

APサーバーとして`NGINX Unit`を、実行環境に`Python3`を、フレームワークとして`Flask`を使用したコンテナを構築します。
NGINX Unitの[公式ドキュメント](https://unit.nginx.org/howto/docker/)を参考に構築していきます。
イメージはこんな感じです。

![docker3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/beea8744-a119-fc65-e924-65f3c595f4cf.png)

### NGINX Unitのコンテナの起動

まずは、NGINX UnitのイメージからPython3とFlaskの環境を構築します。開発環境の最小単位はこれでいいかもしれません。
`web/python`ディレクトリに`Dockerfile`を追加します。

```Docker:web/python/Dorckerfile
FROM nginx/unit:1.14.0-python3.7

WORKDIR /usr/src/app

COPY src .

RUN apt update && apt install -y python3-pip                               \
    && pip3 install --no-cache-dir -r ./requirements.txt                            \
    && rm -rf /var/lib/apt/lists/* 

CMD ["sleep","infinity"]
```

Docker Hubの[nginx/unitのサイト](https://hub.docker.com/r/nginx/unit)から、NGINX UnitのPython3.7用のイメージを使用します。

次に、`pip`でライブラリを一括でインストールできるように、`web/python/src`ディレクトリに`requirements.txt`を作成します。

```txt:web/python/src/requirements.txt
Flask == 1.1.1
flask-sqlalchemy == 2.4.1
PyMySQL == 0.9.3
```

`docker-compose.yml`にNGINX Unitのコンテナの設定を追記します。

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

services:
    db:
        image: mysql
        ports:
            - "33306:3306"
        expose:
            - "3306"
        environment:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_USER: test
            MYSQL_PASSWORD: test
        volumes:
            - ./db/init:/docker-entrypoint-initdb.d
        container_name: app_db
    # ↓↓追記
    ap:
        build: ./python
        ports:
            - "8080:8080"
        environment:
            TZ: "Asia/Tokyo"
        container_name: app_ap
        depends_on:
            - db
```

作成した'Dockerfile'は`web/python`のディレクトリに存在するので、buildでその場所を指定します。
サーバーのポートは`8080`をホストに公開するようにします。
起動しているDockerコンテナを停止してから、`docker-compose build`でビルドしてからコンテナを起動します。

```console
$ docker-compose down
$ docker-compose build --no-cache
$ docker-compose up
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                NAMES
daf4ddc7c11a        web_ap              "sleep infinity"         41 seconds ago      Up 40 seconds       0.0.0.0:8080->8080/tcp               app_ap
565eb32e6a39        mysql               "docker-entrypoint.s…"   43 seconds ago      Up 41 seconds       33060/tcp, 0.0.0.0:33306->3306/tcp   app_db
```

MySQLのコンテナの`app_db`と、NGINX Unitのコンテナの`app_ap`が起動していることが確認できます。
NGINX Unitのコンテナに入り、`requirements.txt`のライブラリがインストールされているかを確認します。

```console
$ docker exec -it app_ap bash
root@00000000000:/# python3 -V
Python 3.7.3
root@00000000000:/# pip3 freeze
Flask==1.1.1
Flask-SQLAlchemy==2.4.1
PyMySQL==0.9.3
```

上記のライブラリ以外にも、`SQLAlchemy`や`Jinja2`などがインストールされています。
ここまでNGINX Unitのコンテナの起動は完了です。続いて、Flaskの実装を行います。

### Flaskアプリケーションの実装

Flaskアプリケーションの実装をします。作成するファイルとディレクトリは以下のようになります。

```console
./web
    |- db
    |   |- init
    |       |- createdatabase.sql
    |- nginx  
    |- python
    |   |- src
    |   |   |- app.py           ←　追加
    |   |   |- config.json      ←　追加
    |   |   |- config.py        ←　追加
    |   |   |- run.py           ←　追加
    |   |   |- users.py         ←　追加
    |   |   |- requirements.txt
    |   |   |- templates        ←　追加
    |   |       |- list.html    ←　追加
    |   |- Dockerfile           ←　更新
    |- docker-compose.yml
```

各ファイルは以下のように実装します。

#### config.py

まずは、DBの接続先などの設定クラスを実装する`config.py`です。
ホスト先は、DBコンテナのコンテナ名`app_db`で指定します。

```python:web/python/src/config.py

class Config(object):
    '''
    Config Class
    '''
    # DB URL
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://test:test@app_db:3306/app?charset=utf8'
```

#### app.py

次にFlaskアプリケーションを起動する`app.py`です。
`config.from_object()`でアプリケーションの設定クラスを呼び込み、`SQLAlchemy()`でFlaskアプリケーションでSQLAchemyが使えるように初期化を行います。

```python:web/python/src/app.py
from config import Config
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# Create Flask Application
application = Flask(__name__)

# Set Config Class
application.config.from_object(Config)

# Set DB
db = SQLAlchemy(application)
```

#### users.py

次にusersテーブルのModelクラスを作成します。
`db.Model`クラスを継承したUsersクラスを作成します。

```python:web/python/src/users.py
from app import db

class Users(db.Model):
    '''
    Users Table Model
    '''
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255))
    email = db.Column(db.String(255))

    def __init__(self,name,email):
        self.name = name
        self.email = email
```

#### run.py

次に、Flaskアプリケーションの起動とルーティングのモジュール`run.py`です。
レスポンスにテンプレートファイルを使用するので、`render_template()`でテンプレートファイルとオブジェクトを指定します。

```python:web/python/src/run.py
from app import application
from users import Users
from flask import render_template

@application.route('/list')
def index():
    users = Users.query.order_by(Users.id).all()
    return render_template('list.html', users=users)

if __name__ == '__main__':
    application.run(host='0.0.0.0', port='8080')
```

#### list.html

次にテンプレートファイルとなる`list.html`を作成します。
`render_template()`で`users`オブジェクトが渡されるので、テンプレートエンジンである`Jinja2`を使用して実装します。

```html:web/python/src/templates/list.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flask Sample</title>
</head>
<body>
    <h1>Flask Sample</h1>
    <table border="1" style="border-collapse: collapse">
        <thead>
            <tr>
                <th >Id</th>
                <th >Name</th>
                <th >EMail</th>
            </tr>
        </thead>
        <tbody>
            {% for user in users %}
                <tr>
                    <td>{{user.id}}</td>
                    <td>{{user.name}}</td>
                    <td>{{user.email}}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>
```

#### Dockerfile

`Dockerfile`を更新します。

```Docker:web/python/Dorckerfile
FROM nginx/unit:1.14.0-python3.7

WORKDIR /usr/src/app

COPY src .

RUN apt update && apt install -y python3-pip                               \
    && pip3 install --no-cache-dir -r ./requirements.txt                            \
    && rm -rf /var/lib/apt/lists/*
# ↓↓削除
```

#### config.json

最後にNGINX Unitの設定ファイル`config.json`を追加します。

```json:web/python/src/config.json
{
  "listeners": {
    "*:8080": {
      "pass": "applications/app"
    }
  },

  "applications": {
    "app": {
      "type": "python",
      "processes": 2,
      "path": "/usr/src/app/",
      "module": "run"
    }
  }
}
```

実装は以上です。

ビルドしてからコンテナを起動してみましょう。

```console
$ docker-compose down
$ docker-compose build --no-cache
$ docker-compose up
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                NAMES
daf4ddc7c11a        web_ap              "sleep infinity"         41 seconds ago      Up 40 seconds       0.0.0.0:8080->8080/tcp               app_ap
565eb32e6a39        mysql               "docker-entrypoint.s…"   43 seconds ago      Up 41 seconds       33060/tcp, 0.0.0.0:33306->3306/tcp   app_db
```

`app_ap`のコンテナの起動後に、コンテナにアクセスしてNGINX Unitの設定ファイルを設定します。

```
$ docker exec -it app_ap bash
root@00000000000:/# curl -X PUT --data-binary @config.json --unix-socket /var/run/control.unit.sock http://localhost/config
{
	"success": "Reconfiguration done."
}
```

ブラウザで、`http://localhost:8080/list`にアクセスして画面が表示されます。

![docker4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/5d25490b-2382-848d-b013-f7ac7e03c4fc.png)


これでAPコンテナの構築は終了です。

## Webコンテナの構築

最後にWEBサーバーのNGINXのコンテナを構築していきます。
これで` NGINX <-> NGINX Unit <-> Flask <-> MySQL`の構成になります。

![docker1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/fdaca8da-5f11-1095-b390-b4bf9875891e.png)

追加、更新するファイルは以下のようになります。

```
./web
    |- db
    |   |- init
    |       |- createdatabase.sql
    |- nginx
    |   |- Dockerfile       ←　追加
    |   |- index.html       ←　追加
    |   |- nginx.conf       ←　追加
    |- python
    |   |- src
    |   |   |- __init__.py
    |   |   |- app.py
    |   |   |- config.json
    |   |   |- config.py
    |   |   |- run.py
    |   |   |- users.py
    |   |   |- requirements.txt
    |   |   |- templates
    |   |       |- index.html
    |   |- Dockerfile
    |- docker-compose.yml   ←　更新
```

まずはトップページとなる`index.html`を作成します。

```html:web/nginx/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Index</title>
</head>
<body>
    <h1>Index</h1>
    <a href="/list">List</a>
</body>
</html>
```

次に`NGINX`の`Dockerfile`を作成します。

```Dockerfile
FROM nginx

WORKDIR /var/www/html

COPY ./index.html ./

CMD ["nginx", "-g", "daemon off;","-c","/etc/nginx/nginx.conf"]
```

次に、NGINXの設定ファイルを作成します。前回の記事で紹介した設定ファイルから、APサーバーのホストをDocker用に変更します。

```conf: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;
    server_tokens off;

    keepalive_timeout  65;

    #gzip  on;

    upstream unit-python {
        server app_ap:8080; # container_nameで指定
    }
    server {
        listen 80;
        server_name localhost;

        # トップページを表示
        location  / {
            root /var/www/html;
        }

        # /listはAPコンテナにルーティング
        location  /list {
            proxy_pass http://unit-python;
            proxy_set_header Host $host;
        }
    }
}
```

最後にdocker-composeを更新します。

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

services:
    db:
        image: mysql
        ports:
            - "33306:3306"
        expose:
            - "3306"
        environment:
            MYSQL_ROOT_PASSWORD: root
            MYSQL_USER: test
            MYSQL_PASSWORD: test
        volumes:
            - ./db/init:/docker-entrypoint-initdb.d
        container_name: app_db

    ap:
        build:
            context: ./python
            args:
                project_directory: "/src/"
        # ↓↓更新
        expose:
            - "8080"
        volumes:
            - "./python/src:/projects"
        environment:
            TZ: "Asia/Tokyo"
        container_name: app_ap
        depends_on:
            - db
    # ↓↓追加
    web:
        build: ./nginx
        volumes:
            - ./nginx/nginx.conf:/etc/nginx/nginx.conf
        ports:
            - "80:80"
        environment:
            TZ: "Asia/Tokyo"
        container_name: "app_web"
        depends_on:
            - ap
```

ビルドしてからコンテナを起動してみましょう。

```console
$ docker-compose down
$ docker-compose build --no-cache
$ docker-compose up
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                NAMES
5b0f06b89db4        web_web             "nginx -g 'daemon of…"   2 minutes ago       Up 23 seconds       0.0.0.0:80->80/tcp                   app_web
625f3c025a82        web_ap              "/usr/local/bin/dock…"   2 minutes ago       Up 2 minutes        8080/tcp                             app_ap
fe5bf54411a2        mysql               "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes        33060/tcp, 0.0.0.0:33306->3306/tcp   app_db
```

app_apのコンテナの起動後に、コンテナにアクセスしてNGINX Unitの設定ファイルを設定します。

```
$ docker exec -it app_ap bash
root@00000000000:/# curl -X PUT --data-binary @config.json --unix-socket /var/run/control.unit.sock http://localhost/config
{
	"success": "Reconfiguration done."
}
```

ブラウザで、`http://localhost:80`にアクセスするとトップページの`index.html`が表示されます。(8080ポートではアクセスできません)

![docker5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/6643762e-d171-8038-7023-d5259d89acf2.png)


リンクとなっている`List`を押下すると、Flaskアプリケーションの`list.html`が表示されます。

![docker6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239179/8c45aa16-b419-3115-b531-cc7808d4c18a.png)


以上でNGINXの構築は終了です。

## まとめ

DockerでNGINX + NGINX Unit + MySQLの環境を構築することができました。
あとはアプリケーションを作り込むだけです。