ローカルで開発していたアプリをDocker化してみた（nginx+Flask+postgres）

# はじめに
ローカルで作った簡易的なブログサービス（[こちらの記事](https://qiita.com/kiyokiyo_kzsby/items/b873291dea93a9f66ce0)を参照）をDocker化してみました。正直この規模だと単一コンテナで十分だと思いますが、折角dockerをお勉強する機会なので、`nginx（Webサーバ）`と`gunicorn+flask（APサーバ）`と`postgre（DBサーバ）`でコンテナを分けて、`docker-compose`で全コンテナの統合管理をします。

# docker構成
![docker構成.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/d488b45e-ceba-e61a-ada7-1919f64c9ae6.jpeg)

# ディレクトリ構成
ローカルの任意の場所にこの構成で作ります。

```
TutorialBlog
 ├docker-compose.yml
 ├nignx/
 │　├nginx.conf
 │　└Dockerfile
 ├app/
 │　├templates/
 │　│　└(略)
 │　├static/
 │　│　└(略)
 │　├app.py
 │　├models.py
 │　└Dockerfile
 ├postgres/
 │　├initdb
 │　│　└createdb.sql
 │　└Dockerfile
 ├Pipfile
 ├Pipfile.lock
 └run.py
```

# ブラウザ⇆localhost:80⇆nginx:80
ブラウザからのリクエストをlocalhostの80番ポートで受けてnginxコンテナ（nginx-server）の80番ポートに流す設定は`docker-compose.yml`に書きます。

```docker-compose.yml
services:
  nginx-server:
    ports:
     - 80:80  #[localhost側のポート]:[nginx-server側のポート]
```

# nignx:80⇆gunicorn:4000
nginxコンテナの80番ポートに流れてきたリクエストをgunicornコンテナの4000番ポートに流すためには何段階か設定が必要です。
なお、スペースの都合上、関係のある箇所だけ抜き出して書いています。最終形は一番最後にまとめて記載するので、解説に興味ない方は読み飛ばしてください。

## docker-compose.yml
gunicornコンテナ（gunicorn-server）を定義して、4000番ポートを解放します。localhostと接続したいわけではないので、`ports`ではなく`expose`で指定しています。
※参照 [Docker-docs-ja expose](http://docs.docker.jp/compose/compose-file.html#expose)

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    expose:
     - "4000"
```

## nginx.conf
`upstream`ブロック内でnginxからリクエストを受け流す先のサーバーを定義します。gunicorn-serverの4000番ポートを解放するよう設定したので、`server gunicorn-server:4000;`と書きます。
`server`ブロック内で、nginxの80番ポートで受けたリクエストを、upstreamで定義したサーバーに流すよう設定しています。
（ここのlocation部分を色々いじると、リクエストURLに応じて受け流す先のサーバーを制御できるようになるっぽいです。詳細は「[nginx連載5回目: nginxの設定、その3 - locationディレクティブ](https://heartbeats.jp/hbblog/2012/04/nginx05.html)」を参照。）

```nginx.conf
http {
    upstream application {
        server gunicorn-server:4000;
    }
    server {
        listen 80;
        location / {
            proxy_pass http://application/;
        }
    }
}
```

## run.py
flaskのデフォルトポートは5000番になっているので、4000番で受けるよう変更します。（※4000番にした意図は特にありません。適当です。デフォルトの5000番のままでいいと思います。）
あと、gunicornを介してリクエストを受け取るために、`host="0.0.0.0"`を指定します。
※参考 [docker-composeでgunicorn+nginx+flaskを動かしてみた話 - ハマったポイント①：Flaskのサーバーはデフォルトだと公開されてない](https://qiita.com/arata-honda/items/e22c9df83df8ee0e9c4c#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88flask%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AF%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%A0%E3%81%A8%E5%85%AC%E9%96%8B%E3%81%95%E3%82%8C%E3%81%A6%E3%81%AA%E3%81%84)

```run.py
from app.app import app

if __name__ == "__main__":
    app.run(host="0.0.0.0",port=4000)
```

# gunicorn⇆postgres:5432
gunicorn(flask)とpostgresの通信にも何段階か設定が必要です。

## docker-compose.yml
postgersコンテナ（postgres-server）を定義して、5432番ポートを解放します。localhostと接続したいわけではないので、`ports`ではなく`expose`で指定しています。
また、postgresに接続するためのユーザ名とパスワードを`environment`で定義します。（認証情報なので、ハードコーディングしないで、別の場所に格納した方がベターかもしれません。）

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    #(略)
  postgres-server:
    expose:
     - "5432"
    environment:
     - POSTGRES_USER=[ユーザ名]
     - POSTGRES_PASSWORD=[パスワード]
```

## models.py
先ほど定義した[ユーザ名][パスワード]を使用して、`app.config['SQLALCHEMY_DATABASE_URI']`にpostgresへの接続情報を記載します。
末尾の`/tutorial_blog`は、tutorial_blog DBへの接続を定義しています。tutorial_blog DBをPostgres内に作成する部分については後述します。

```models.py
from flask_sqlalchemy import SQLAlchemy
from app.app import app

app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://[ユーザ名]:[パスワード]@postgres-server:5432/tutorial_blog'
db = SQLAlchemy(app)
```

# postgres⇆databaseボリューム
postgresコンテナだけだと、コンテナを削除した際にデータが消滅してしまうので、コンテナ外の「ボリューム」と呼ばれる領域にデータを格納する必要があります。一番下の`volumes:`でdatabaseボリュームを使用することを宣言し、`postgres-server:`内の`volumes:`で、databaseボリュームと、postgresのデータ格納領域である`/var/lib/postgresql/data`を繋げています。
※参考 [Docker、ボリューム(Volume)について真面目に調べた](https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32)

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    #(略)
  postgres-server:
    #(略)
    volumes:
      - database:/var/lib/postgresql/data
volumes:
  database:
    driver: local
```

# postgresコンテナの設定

## Postgres起動時処理
Postgresを起動時に、コンテナ内の`/docker-entrypoint-initdb.d`以下に置かれているファイルが実行されます。そこにローカルの`/postgres/initdb`をマウントしておいて、`initdb`以下に実行させたい処理を書きます。
※参考 [dockerでPostgreSQLのコンテナ作成と初期化](https://qiita.com/fukafuka_dev/items/17e655d8369c19affbc3)

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    #(略)
  postgres-server:
    #(略)
    volumes:
      - ./postgres/initdb:/docker-entrypoint-initdb.d
      - #(略)
```
今回はtutorial_blog DBをPostgresに（無ければ）作る、という処理を初期化処理として組み込みたいため、`createdb.sql`に、そのSQL文を記載して、initdb下に格納しておきます。

```createdb.sql
create database tutorial_blog
```

## Dockerfile

`postgres/Dockerfile`に、Dockerイメージ作成のためのコマンドを書いていきます。
Postgresコンテナはデフォルトのまま利用するので、参照元のイメージ指定だけ行います。

```dockerfile:postgres/Dockerfile
FROM postgres
```

また、`docker-compose.yml`側で、どのDockerfileを利用してイメージのビルドを行うか定義します。

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    #(略)
  postgres-server:
    build: ./postgres
    #(略)
```

# gunicornコンテナの設定

## flaskアプリファイルのマウント
ローカルでの開発物をgunicorn-serverにマウントして、コンテナ内でも使えるようにします。
今回のflaskアプリでは、依存パッケージを記載している`Pipfile`、`Pipfile.lock`、アプリ本体である`app/`以下全てのファイル、アプリ起動用の`run.py`をコンテナ側でも使いたいので、この4つをコンテナ側の`/var/www/`以下にマウントしていきます。

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    #(略)
    volumes:
     - ./Pipfile:/var/www/Pipfile
     - ./Pipfile.lock:/var/www/Pipfile.lock
     - ./app:/var/www/app/
     - ./run.py:/var/www/run.py
  postgres-server:
    #(略)
```

## Dockerfile
`app/Dockerfile`に、Dockerイメージ作成とコンテナ起動時処理のためのコマンドを書いていきます。

```dockerfile:app/Dockerfile
# 参照元イメージの指定
FROM python:3.7 
# ワーキングディレクトリの指定
WORKDIR /var/www
# コンテナ起動時の実行コマンド
CMD ["bash","-c","pip install pipenv && pipenv install --system && gunicorn run:app -b 0.0.0.0:4000"]
```
`CMD`にコンテナ起動時の実行コマンドを記載しています。
（※本当はCMDとENTRYPOINTの違いを理解しなきゃなんだろうけど、一旦これで動いたので。詳細はこちら「[DockerfileのCMDとENTRYPOINTを改めて解説する](https://qiita.com/uehaj/items/e6dd013e28593c26372d)」を参照。）
コンテナ起動後にやりたいことは、
1. pipenvのインストール
2. 依存パッケージのインストール
3. gunicornを介してのアプリ起動
なので、それらを順次行えるようにコマンドを記載しています。コンテナの中で以下のコマンドを実行するイメージですね。

```
$ pip install pipenv
$ pipenv install --system
$ gunicorn run:app -b 0.0.0.0:4000
```
`pipenv install --system`では、`Pipfile`、`Pipfile.lock`から、依存パッケージのインストールを行なっています。コンテナ内でわざわざpython仮想環境を立てる必要はないので、`--system`をつけて、コンテナ内に直接パッケージのインストールを行なっています。
`gunicorn run:app`で、`run.py`の`app`変数を渡してgunicornを介してのflaskアプリ起動を行なっています。その際の`-b 0.0.0.0:4000`オプションで、gunicornで受け入れるポートを指定しています。
※参考 [Running Gunicorn](http://docs.gunicorn.org/en/latest/run.html)
※参考 [docker-composeでgunicorn+nginx+flaskを動かしてみた話 - ハマったポイント②：gunicorn起動にbindすべし](https://qiita.com/arata-honda/items/e22c9df83df8ee0e9c4c#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88gunicorn%E8%B5%B7%E5%8B%95%E3%81%ABbind%E3%81%99%E3%81%B9%E3%81%97)

また、`docker-compose.yml`側で、どのDockerfileを利用してイメージのビルドを行うか定義します。

```docker-compose.yml
services:
  nginx-server:
    #(略)
  gunicorn-server:
    build: ./app
    #(略)
  postgres-server:
    #(略)
```

## db.create_all()
models.pyで定義されたテーブル/カラム情報をもとに、SQLAlchemyのcreate_all()を走らせることで、postgresのtutorial_blog DBにテーブル/カラムの初期設定を行います。
`gunicorn run:app`で、app.pyが実行されるので、その中に`create_all()`を仕込んでおきます。

```models.py
from flask_sqlalchemy import SQLAlchemy
from app.app import app
db = SQLAlchemy(app)
```

```app.py
from flask import Flask
app = Flask(__name__)
from app.models import db
db.create_all()
db.session.commit()
```

# nginxコンテナの設定
## nginx.confのマウント
ローカルの`nginx/nginx.conf`に格納しているnginx設定ファイルをコンテナ内でも使えるようにするため、マウント設定を行います。

```docker-compose.yml
services:
  nginx-server:
    #(略)
    volumes:
     - ./nginx/nginx.conf:/etc/nginx/nginx.conf
  gunicorn-server:
    #(略)
  postgres-server:
    #(略)
```

## Dockerfile
`nginx/Dockerfile`に、Dockerイメージ作成とコンテナ起動時処理のためのコマンドを書いていきます。

```dockerfile:nginx/Dockerfile
# 参照元イメージの指定
FROM nginx
# コンテナ起動時の実行コマンド
CMD ["nginx", "-g", "daemon off;","-c","/etc/nginx/nginx.conf"]
```
コンテナ起動後に以下のコマンドを実行しています。

```
$ nignx -g daemon off; -c /etc/nginx/nginx.conf
```
nginxをバックグラウンド実行するとコンテナが停止してしまうようなので、`-g daemon off;`で、フォアグラウンド実行を指定しています。
※参考 [Docker 事始め - dockerハマりポイント](https://qiita.com/taka4sato/items/d3240438d5fa4caf99db#docker%E3%83%8F%E3%83%9E%E3%82%8A%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88)
また、`-c /pass/to/configfile`で、nginx設定ファイルの指定を行なっています。
※参考 [CommandLine | NGINX](https://www.nginx.com/resources/wiki/start/topics/tutorials/commandline/)

さらに、`docker-compose.yml`側で、どのDockerfileを利用してイメージのビルドを行うか定義します。

```docker-compose.yml
services:
  nginx-server:
    build: ./nginx
    #(略)
  gunicorn-server:
    #(略)
  postgres-server:
    #(略)
```

# コンテナ起動順序の指定
postgresコンテナ→gunicornコンテナ→nginxコンテナの順に起動したいので（nginxコンテナよりgunicornコンテナが先にたってないとupstream指定ができない、gunicornコンテナよりpostgresコンテナが先に立ってないとpostgres接続ができない）、`docker-compose.yml`に起動順序を記載していきます。
`depends_on`を記載すると、指定されたコンテナが起動してから自分のコンテナを起動する、という制御をかけることができます。

```docker-compose.yml
services:
  nginx-server:
    #(略)
    depends_on:
     - gunicorn-server
  gunicorn-server:
    #(略)
    depends_on:
     - postgres-server
  postgres-server:
    #(略)
```

# コンテナの起動・動作確認・停止・削除
本当は[公式ドキュメント](https://docs.docker.com/compose/reference/)読み込まないといけないところなんですけど、時間がなかったので日本語でまとまっているこちら「[docker-compose コマンドまとめ](https://qiita.com/wasanx25/items/d47caf37b79e855af95f)」を参考にさせていただきました。
## dockerイメージのビルド

```
$ docker-compose build
```

## コンテナの起動

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

## ブラウザ表示確認
ブラウザで`localhost`にアクセスすると、メインページが表示されるはずです。
<img width="1439" alt="スクリーンショット 2019-07-28 18.24.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214279/614f8fd1-0fdd-032e-6728-ac414abd9021.png">


## コンテナの停止とdockerイメージ削除

```
$ docker-compose down --rmi all
```

## dockerイメージ一覧の確認

```
$ docker images
```

## コンテナ一覧の確認

```
$ docker ps -a
```

## ボリューム一覧の確認

```
$ docker volume ls
```

## 各コンテナのログ取得

```
$ docker logs [コンテナ名]
```

# 最終的なファイル内容
## docker-compose.yml

```docker-compose.yml
version: "3"
services:
  nginx-server:
    build: ./nginx
    volumes:
     - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    ports:
     - 80:80
    depends_on:
     - gunicorn-server
  gunicorn-server:
    build: ./app
    volumes:
     - ./Pipfile:/var/www/Pipfile
     - ./Pipfile.lock:/var/www/Pipfile.lock
     - ./app:/var/www/app/
     - ./run.py:/var/www/run.py
    expose:
     - "4000"
    depends_on:
     - postgres-server
  postgres-server:
    build: ./postgres
    expose:
     - "5432"
    environment:
     - POSTGRES_USER=user
     - POSTGRES_PASSWORD=password
    volumes:
      - ./postgres/initdb:/docker-entrypoint-initdb.d
      - database:/var/lib/postgresql/data
volumes:
  database:
    driver: local
```

## nginx/Dockerfile

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

## app/Dockerfile

```dockerfile:app/Dockerfile
FROM python:3.7
WORKDIR /var/www
CMD ["bash","-c","pip install pipenv && pipenv install --system && gunicorn run:app -b 0.0.0.0:4000"]
```

## postgres/Dockerfile

```dockerfile:postgres/Dockerfile
FROM postgres
```

## nginx.conf
解説した部分以外はこちら「[Flask+uwsgi+nginxの環境が作りたい？それ、Dockerなら1コマンドで出来るよ。](https://qiita.com/trrrrrys/items/a905f1382733dfb9c8c1)」をかなり参考にしました。

```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;
    upstream application {
        server gunicorn-server:4000;
    }
    server {
        listen 80;
        charset utf-8;
        location / {
            proxy_pass http://application/;
        }
    }
}
```

# おわりに
元となるdockerイメージがあるとはいえ結構自前で設定しなきゃいけない部分が多かったので、今まで[herokuさん](https://jp.heroku.com/)がよしなにやってくれていたWebサーバ（nginx）やDBサーバ（postgres）、wsgi（gunicorn）周りがどう動いているのかちょっとだけ理解できました。
今後の方向性としては
1. 他のDockerミドルウェア/アプリと繋ぐ（とりあえず[ElasticSearch](https://www.elastic.co/jp/products/elasticsearch)を使ってみたい）
2. クラウドサーバにデプロイしてサービス公開（[EC2](https://aws.amazon.com/jp/ec2/)に乗せてみたい）
3. コンテナオーケストレーション（[kubernetes](https://kubernetes.io/ja/)使ってみたい）
という感じで進めたいと思います！

# 参考まとめ
[Docker-docs-ja expose](http://docs.docker.jp/compose/compose-file.html#expose)
[nginx連載5回目: nginxの設定、その3 - locationディレクティブ](https://heartbeats.jp/hbblog/2012/04/nginx05.html)
[docker-composeでgunicorn+nginx+flaskを動かしてみた話](https://qiita.com/arata-honda/items/e22c9df83df8ee0e9c4c)
[Docker、ボリューム(Volume)について真面目に調べた](https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32)
[DockerfileのCMDとENTRYPOINTを改めて解説する](https://qiita.com/uehaj/items/e6dd013e28593c26372d)
[dockerでPostgreSQLのコンテナ作成と初期化](https://qiita.com/fukafuka_dev/items/17e655d8369c19affbc3)
[Running Gunicorn](http://docs.gunicorn.org/en/latest/run.html)
[Docker 事始め](https://qiita.com/taka4sato/items/d3240438d5fa4caf99db)
[CommandLine | NGINX](https://www.nginx.com/resources/wiki/start/topics/tutorials/commandline/)
[docker-compose コマンドまとめ](https://qiita.com/wasanx25/items/d47caf37b79e855af95f)
[Flask+uwsgi+nginxの環境が作りたい？それ、Dockerなら1コマンドで出来るよ。](https://qiita.com/trrrrrys/items/a905f1382733dfb9c8c1)