docker-composeでgunicorn+nginx+flaskを動かしてみた話

※ この記事は **[エキサイト Advent Calendar 2018](https://qiita.com/advent-calendar/2018/excite)** 20 日目の記事です ※

# ごあいさつ
こんにちわ。
また...この季節がやってきましたね
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3231313136342f66376136386133632d663637352d643437302d653736352d3862306462306437373634352e706e67 (1).png](https://qiita-image-store.s3.amazonaws.com/0/211139/0cc0cde8-318a-79ac-0b53-3961d4ca9a63.png)

去年もこうして[アドベントカレンダー](https://qiita.com/arata-honda/items/8d08f31aa7d7cbae4c91)を書き、先輩に遊びに連れて行ってもらうことによりぼっちマスを乗り越えました、新卒二年目のエンジニアの[本田](https://qiita.com/arata-honda)です！そんな先輩も今年に彼女ができてしまったようで、本当に一人になってしまいました...

そんなことはさておき、去年までは[ウーマンエキサイトアプリ](https://play.google.com/store/apps/details?id=jp.co.excite.woman.topics&hl=ja)を担当していましたが、今年から新設部署に移動になりまして、今はブロックチェーンや機械学習の開発を行っております！

そこで開発した投稿監視AIの環境をOpenStack上に立てたのですが、どうにかコンテナ化できないかと、すこし頑張ってdockerについて調べつつ奮闘している(現在進行形)記事となります。

# コンテナ化したい環境

コンテナ化したい環境は事前にまとめておきました。
[gunicorn + Flask + nginx + Systemdで動かしてみた](https://qiita.com/arata-honda/items/09e1c350b1bd8a186e9c)

こちらの記事を毎度恒例、ざっくり三行でまとめると

- Flask(pythonの軽量ウェブフレームワーク)をgunicornと呼ばれるフレームワークを包み込むWeb Server Gateway Interfaceで起動する

- gunicornはリバースプロキシができるnginxを推奨しています。

- gunicornをsystemdでサービス化してやることで、プロセス管理やデーモン化(gunicornのワーカー設定)できます。

今回はgunicorn + nginx + Flaskまでをdockerで頑張ってみました。


# dockerとは

もう最近ではk8sなどの登場でみんな浸透していると思いますが、dockerとは"コンテナ"とよばれる単位で仮想環境を構築するものです。
利点や欠点などは、Casleyさんの記事、[「Dockerのメリット・デメリット」](https://www.casleyconsulting.co.jp/blog/engineer/257/)を引用すると、

> - 構成が仮想化よりも単純なため、高密度化が可能
- 他の仮想化技術に比べ、オーバーヘッドが少ない
- 新しいマシン(コンテナ)の起動が、仮想マシン(VM)より高速

となり、最近ではオンプレからこういったコンテナ化が騒がれている？そうです。
dockerはプロセスごとにコンテナを立てるので、今回gunicornとnginxの二つのコンテナ構成(下図)になります。

```
flask-gunicorn-nginx/
      ├nginx/
      │　├ Dockerfile
      │　└ default.conf
      └ web/
         ├ Dockerfile
         └ requirements.txt
```

# まずは、Flaskでhello worldまで
とにかく参考にさせてもらったのは、[「Djangoの環境をDocker化する（Docker + Django + Gunicorn + nginx）その１」](https://qiita.com/amazipangu/items/bce228f506f894cd825d)です。

まず、コンテナを立てるためにdockerfileを書きます。

```dockerfile
FROM python:3.6
ENV PYTHONUNBUFFERED 1
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
ADD . /usr/src/app/
```

pythonの3.6をFROMでとってきており、今回これがベースコンテナとなります。
参考にさせてもらった記事どおり、今回は`/usr/src/app`下にrequirements.txtを用意して、pip installします。

```txt:requirements.txt
gunicorn==19.9.0
Flask==1.0.2
```

まずはここまで書いて、次はコンテナを立てるためにdockerfileをビルドします。

```
cd flask-gunicorn-nginx
cd web
docker build .
```

ビルドに成功したらコンテナのID(英数字の羅列)が出てくるので
`docker run -it コンテナID /bin/bash`でこのコンテナのバッシュを起動します。

<img width="718" alt="スクリーンショット 2018-12-10 9.51.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/5fadeb72-d115-0566-6475-0ba76ff5cd9a.png">
↑の感じで立てたコンテナに入ります。
<img width="442" alt="スクリーンショット 2018-12-10 9.52.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/2ccb7233-51b3-4c0d-9d28-2049f821f420.png">
<img width="315" alt="スクリーンショット 2018-12-10 9.51.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/0ab1be00-0f4a-9bcb-e1c0-0b5c484e4bd0.png">
ちゃんとFlaskもgunicornも入っていることが確認できました！

ということで、[書いた記事](https://qiita.com/arata-honda/items/09e1c350b1bd8a186e9c)のサンプルコードを早速実行してみる

```python:flask_sample.py
from flask import Flask

app = Flask(__name__)

@app.route('/hello')
def hello():
    hello = "Hello world"
    return hello

if __name__ == "__main__":
    app.run()
```
さらに、Dockerfileにこのファイルを`usr/src/app`下追加して実行するコマンドを書きます。

```dockerfile
# sampleファイルをadd
ADD flask_sample.py ./usr/src/app/
# 実行
CMD ["python", "flask_sample.py"]
```
`* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)`
が出るのがわかっているので、
そして, `docker build .` && `docker run -p 5000:5000 -it コンテナID`
とポートを指定しつつrunすると、URLが出てくるのでたたくと、
<img width="401" alt="スクリーンショット 2018-12-10 9.59.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/32cdd246-c80c-cb3a-5fee-2d8af8b414de.png">

ハマる

## ハマったポイント①：Flaskのサーバーはデフォルトだと公開されてない

公式：http://flask.pocoo.org/docs/0.12/quickstart/

>Externally Visible Server

>If you run the server you will notice that the server is only accessible from your own computer, not from any other in the network. This is the default because in debugging mode a user of the application can execute arbitrary Python code on your computer.

>If you have the debugger disabled or trust the users on your network, you can make the server publicly available simply by adding --host=0.0.0.0 to the command line:

>外部公開用サーバーについて
(Flaskの)デフォルトはデバック用で、自分のネットワークのみアクセス可能な状態になっています。
もし、あなたが信頼できるネットワークに公開したければ、 host=0.0.0.0をつけて

![image.png](https://qiita-image-store.s3.amazonaws.com/0/211139/64c6a2e9-3cc0-a15d-ccc5-5dab3a7a2641.png)
というわけで追加したらちゃんとHello world見えました。

# gunicornで起動
`CMD ["python", "flask_sample.py"]`で実行できるのは理解したので、gunicornで起動してみる

```dockerfile
# 実行
CMD ["gunicorn", "flask_sample:app"]
```
実行すると前回同様`http://127.0.0.1:8000`で起動されるので、URLをたたくと

ハマりました

## ハマったポイント②：gunicorn起動にbindすべし
http://docs.gunicorn.org/en/stable/settings.html#bind
bind指定しないと8000が開くということで、上記のflaskで設定した`5000`で開けるために、バインドして起動します。

```dockerfile
# 実行
CMD ["gunicorn", "flask_sample:app", "-b", "0.0.0.0:5000"]
```
と指定して、起動すると動きました。

<img width="400" alt="スクリーンショット 2018-12-10 10.01.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/14068695-70f9-b813-e7f0-5bbeef285b72.png">

# nginxの設定

gunicorn側の使い勝手はよくわかったので、次はnginxを設定していきます。

```dockerfile
FROM nginx:latest
COPY default.conf /etc/nginx/conf.d/default.conf
```
一番新しいnginxを公式からpullしてきて、default.confは自前ので置き換えてます。

さて、そこで一つ疑問が、

...nginxとgunicornのコンテナ別々だけど、ソケット通信できるの？

# ハマったポイント③：ソケット通信どうするのか

[書いた記事](https://qiita.com/arata-honda/items/09e1c350b1bd8a186e9c)には, gunicornで起動する際にsocketをbindすることで、生成 -> そのsocketをリバースプロキシでアクセスするという流れだったのですが、

ソケットってそもそも同サーバー内で各プロセス間のやり取りにおいて使うイメージ。(あんまり理解してないし、ソケット通信のメリットもあまり良く知っていない)

やるとしたら、
docker-composeでローカルにボリュームがマウントできるので、/tmp下をマウントして、gunicornで/tmpしたにsocket作成。nginxのコンテナも/tmp下にディレクトリ作って同じところにマウントすれば、一応できそう(これ以上案が思い浮かばないです

かなり、:thinking: になったので、簡単のため、ソケットは行わずに、ポートだけ指定してそこに飛ばすように変更しました。というわけで書いたのが下のdefault.conf

```default.conf
upstream hello_app {
    server web:8000;
}

server {
    listen       80;

    root /usr/src/app/;
    server_name  hello_world.excite.co.jp;

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

    location / {
        try_files $uri @flask;
    }

    location @flask {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_redirect off;

        proxy_pass http://hello_app;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```
nginxで`hello_world.excite.co.jp`を叩くと、`location /`にマッチして、次に`@flask`にまっちしてサーバー群(hello_app)に飛んで、web:8000 にアクセスするっていう感じになります。

どうやら、docker-composeでnetworkの設定をすると、コンテナ名を指定したら名前解決して別コンテナのネットワークにアクセスできるとのこと(すごい

公式：http://docs.docker.jp/compose/networking.html#specifying-custom-networks

これを参考にdocker-composeを書いてみる。

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

services:

  web:
    build: ./web
    networks:
      - nginx_network
    ports:
      - 8000:5000

  nginx:
    build: ./nginx
    ports:
      - 5000:80
    depends_on:
      - web
    networks:
      - nginx_network

networks:
  nginx_network:
    driver: bridge
```

versionは3で、services以下から2つのコンテナ("web", "nginx")を指定。
networksに"nginx_network"って名前をつけて、driver設定でbridgeにする。

[公式](http://docs.docker.jp/compose/compose-file.html#id6)曰く
>一般的には単一ホスト上であれば bridge でしょうし、 [Swarm](https://knowledge.sakura.ad.jp/5197/) 上であれば overlay でしょう。

なので、今回は単一なので、bridgeにしています。

あとは依存関係でgunicorn側のコンテナが上がってなかったらnginxのコンテナは挙げないみたいなdepend_on（ここはAnsibleっぽい)をつけてるくらいです。

まとめると、フロントあたり(macのPC)で5000番で受けたものを80番にポートフォワーディングして、
default.confで80番をlistenしているので、次はweb(gunicornのコンテナ)の8000にアクセスされる。
web:8000では、docker-compose.ymlが5000番にポートフォワーディングしているので、アクセスされるとgunicornの5000番バインドに引っかかり、gunicornがリクエストを受け取りFlaskのアプリケーションの中身が実行されて返却される。

という流れです。

最終的な構成図は

```
flask-gunicorn-nginx/
      ├nginx/
      │　├ Dockerfile
      │　└ default.conf
      ├docker-compose.yml
      └ web/
         ├ Dockerfile
         └ requirements.txt
```

こんな感じです

`docker-compose build`してみると、

<img width="506" alt="スクリーンショット 2018-12-13 11.01.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/fe84a5a7-711a-35d9-7fb4-a781ebb0c910.png">

通っております...!!!

ここで、
<img width="800" alt="スクリーンショット 2018-12-13 10.10.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/797a536f-6afe-9e7a-ff0e-9a12eb3185c3.png">
このエラーが出たあなた。

docker-compose.ymlを【空の状態】でビルドするとこのようなエラーになります。今すぐ[ctr + s]を押して保存しましょう。

*そうです。私は30分これにハマりました。*

ビルドできたら`docker-compose up -d`で起動して
http://hello_world.excite.co.jp:8000/hello
を叩くと....


<img width="400" alt="スクリーンショット 2018-12-13 11.05.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/c59c7f87-6456-5f0a-f638-37b40d22753b.png">

できました！！！！

#まとめ・感想

- docker-composeでnginxとgunicornの連携を行いました。
- docker-composeでネットワーク設定するとコンテナ名で名前解決できました。
- 来年はもっと高度なことを書きたい(k8sとか)

#参考

- [Docker Compose with NginX, Django, Gunicorn and multiple Postgres databases](http://pawamoy.github.io/2018/02/01/docker-compose-django-postgres-nginx.html)
- [docker-compose で別の docker-compose.yml で作ったコンテナとリンクする (ネットワークを繋げる)](https://qiita.com/reneice/items/20e981062b093264cd0a)
- [空ビルドに気づかず頑張って読んだ記事](https://github.com/docker/compose/issues/5319)
- [Djangoの環境をDocker化する（Docker + Django + Gunicorn + nginx）その１](https://qiita.com/amazipangu/items/bce228f506f894cd825d)

#最後に
つくったものは[git](https://github.com/arata-honda/flask-gunicorn-nginx
)においておきます。

明日は【ゲーム製作超入門】、気になります！
ではまた！

#2018年01月06日追記
上記の画像を見てると、8000番ポートでアクセスしてますね...
よくdocker-compose.ymlをみてると、gunicorn側で8000番ポートを5000番(gunicornのbindポート)にポートフォワーディングしているので、実際はnginx通ってないです。(確認不足申し訳ないです

##変更内容

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

services:

  web:
    build: ./web
    networks:
      - nginx_network
    expose:
      - 5000

  nginx:
    build: ./nginx
    ports:
      - 8080:80
    depends_on:
      - web
    networks:
      - nginx_network

networks:
  nginx_network:
    driver: bridge
```
と、gunicorn側のコンテナの設定は、外部公開する[ports](http://docs.docker.jp/compose/compose-file.html#ports)ではなく内部公開する[expose](http://docs.docker.jp/compose/compose-file.html#expose)を使うように修正しました。これで8000番叩いてもgunicorn側には届かない。

次に、default.confは

```default.conf
upstream hello_app {
    server web:5000;
}
```
これでクライアント(8080)→nginx(80)→upstreamを経てgunicorn側のip(5000)→hello worldの表示です。

あとは
http://localhost:8080/hello
か、
http://hello_world.excite.co.jp:8080/hello

でhello worldが確認できればおｋです。
申し訳ないです！

###謎ポイント

docker psでnginx側のコンテナをしらべて
docker run -it nginxのコンテナ名 bin/bash
で入ってnginx -tをすると、

```
2019/01/06 15:04:59 [emerg] 7#7: host not found in upstream "web:5000" in /etc/nginx/conf.d/default.conf:2
nginx: [emerg] host not found in upstream "web:5000" in /etc/nginx/conf.d/default.conf:2
nginx: configuration file /etc/nginx/nginx.conf test failed
```

と出るのですが、これはgunicorn側のコンテナを立ち上げる前に、nginxのコンテナが立ち上がってhost名画解決できないということなのでしょうか。
強い人がいれば教えてください...涙
(ただ、これでもコンテナが落ちてない&nginxとgunicornの通信もできているので謎が深まります)