DockerでFlask+Gunicorn+Nginxの環境構築


以前、php-fpm+Nginxの環境をDockerで作りましたが、今回はGunicorn+Nginxで環境を作ります。

Gunicorn

Gunicorn(Green Unicorn)はWSGI HTTPサーバです。GunicornはRubyのUnicornプロジェクトから移行してきたpre-fork workerモデル(HTTPリクエストを受け取った時に、事前に用意した子プロセスで処理するモデル)です。
同じレイヤーのアプリでPython系では、これ以外に(使ったことはないですが、)uWSGIというものもあります。
今回はFlaskアプリを作りますが、GunicornがFlaskアプリを実行するという形になります。

Nginx

NginxはHTTPサーバ、リバースプロキシサーバ、メールプロキシサーバ、TCP/UDPプロキシサーバのサービスを提供するOSSです。
同じレイヤーのアプリでApacheがありますが、ApacheよりNginxの方が高速に動かすことができます。
クライアントからのHTTPリクエストは、Nginxが受け、それをGunicornに中継します。

システム構成

コンテナは下記のように立てます。

外部からはNginxしかアクセスできないようにしています。
以前、Nginxとphp-fpm間の通信では、TCPソケット通信しか試さなかったので、今回は
NginxとGunicorn間は、TCPソケット通信とUNIXドメインソケット通信の二通り試しました。

非常に勉強になる他者様の記事を参考に書いていきます。

TCPソケット通信

正しくはINETドメインと呼ぶようです。
こちらは「異なるマシンで動作しているプロセス間の通信を行うためのソケット」です。
ソケットは、OSI参照モデルのセッション層のレイヤーにあたるもので、※通信はIPアドレスとポート番号によって行います。(※HTTPではありません。)
そのため、ネットワーク上でマシンを超えたプロセス間通信が行えます。また、他にもUNIXドメインソケットとは違い、1台のプロキシサーバから複数台のWebアプリケーションサーバに通信を割り当て、負荷分散をさせたり、WebサーバとWebアプリケーションサーバを別のマシンに置いても使えるというメリットがあります。

また、これは所感ですが、TCPソケットの場合、コンテナにポートを開けることで、Nginxを介さずに動作確認できるのもあり、エラーが起きたときに原因の確認がしやすいです。

UNIXドメインソケット

こちらは「同じマシン内で動作しているプロセス間の通信を行うためのソケット」です。
マシン内にbindファイルを用意し、WebサーバとWebアプリケーションサーバをこのファイルを介して通信をします。そのため、この通信は同じマシン内でしか行なえません。代わりに、ファイルの書き込み・読み込みで通信ができるため、TCPソケットによる通信より速いです。また注意点として、この通信をサポートしているサービスが限られています。詳しくは、他者様の記事

実装

今回はDocker周り、Nginx周りとgunicorn・flask周りの部分を残します。
まずはcomposeファイルから

docker-compose.yml
version: '3'

services:
    web:
        build: 
          context: ./web
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./web/public:/etc/nginx/public
          - ../ssl/certs/:/etc/pki/tls/certs/
          - ../ssl/private/:/etc/pki/tls/private/
          - ./gunicorn_socket:/tmp/gunicorn_socket
        depends_on:
          - app
        container_name: web
        restart: always
        networks:
          - network

    app:
        build: 
          context: ./app
        volumes:
          - ./app:/var/www/
          - ./gunicorn_socket:/tmp/gunicorn_socket
        depends_on:
          - db
        container_name: app
        ports:
            - 9876:9876
        networks:
          - network

volumes:
    db_data: {}

networks:
    network:
      driver: bridge

WebコンテナがNginxのコンテナ、AppコンテナがGunicorn+Flaskのコンテナです。

volumes:
  - ./gunicorn_socket:/tmp/gunicorn_socket

この部分で、UNIXドメインソケットの通信で使うソケットファイルを出力するディレクトリをマウントします。
TCPソケットを使う場合はここの記述は不要です。

ports:
  - 9876:9876

Appコンテナは9876ポートを開けています。
これはAppコンテナ内で9876ポートで立てているGunicornに外部からアクセスできるようにしているからです。
こちらがなくとも、WebコンテナはAppコンテナと通信できます。UNIXドメインソケットを使う場合はここの記述不要です。
本番環境で使う場合も不要なポートを開けるのは避けた方がいいので、ここの記載はやめた方がいいです。

Nginxコンテナ

Dockerfileは下記です。

FROM nginx:latest

COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./server.conf /etc/nginx/conf.d/server.conf


CMD ["nginx", "-g", "daemon off;"]

build時に、コピーするファイルは下記です。

nginx.conf
user nginx;

worker_processes auto;
pid /var/run/nginx.pid;

events{
    worker_connections 512;
    multi_accept on;
    use epoll;
}

http {
    charset UTF-8;
    server_tokens off;

    include /etc/nginx/mime.types;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ssl_protocols TLSv1.1 TLSv1.2;

    default_type  text/html;

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

    include /etc/nginx/conf.d/server.conf;
}

server.conf
upstream app {
    # UNIXドメインソケットを使う場合
    server unix:/tmp/gunicorn_socket/gunicorn_flask.sock fail_timeout=0;

    # TCPソケットを使う場合
    server app:9876 fail_timeout=0;
}

server {
        listen 80;
        server_name  localhost;

        root /var/www/public;

        access_log /var/log/nginx/access.log;
        error_log  /var/log/nginx/error.log;

        location / {
            try_files $uri @flask;
        }

        location @flask {
            proxy_pass_request_headers on;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;
            proxy_pass http://app;
        }
}

upstreamという部分で、UNIXドメインソケットか、TCPソケットを使うかを選択します。
※使わない方をコメントアウトして使ってください。

Gunicorn+Flaskコンテナ

Dockerfileは下記です。

FROM python:3.8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

RUN apt update -y

RUN mkdir -p /var/www
COPY ./requirements.txt /var/www
RUN pip install -r /var/www/requirements.txt

COPY ./flask_app.py /var/www
COPY ./gunicorn.py /var/www

WORKDIR /var/www

CMD ["gunicorn", "flask_app:app", "--config", "/var/www/gunicorn.py" ]
requirements.txt
Flask==1.1.2
gunicorn==20.0.4

GunicornとFlaskはpipでinstallします。
今回、Gunicornはsystemdサービスのものは使いません。(ネットではsystemdサービスを使う方法が多いですが、)
やることは同じなので、お好きな方を選択して使ってください。
あとは、flaskアプリのファイルとgunicornのconfigファイルを用意します。

flask_app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World'
gunicorn.py
import os

# UNIXドメインソケット
socket_path = 'unix:/tmp/gunicorn_socket/gunicorn_flask.sock'
# TCPソケット
socket_path = '0.0.0.0:' + str(os.getenv('PORT', 9876))
bind = socket_path

# Debugging
reload = True

# Logging
accesslog = '-'
#loglevel = 'info'
loglevel = 'debug'
logfile = './log/app.log'
logconfig = None

# Proc Name
proc_name = 'Infrastructure-Practice-Flask'

# Worker Processes
workers = 2
worker_class = 'sync'

gunicorn.pyのsocket_pathでどっちの通信を受け付ける設定にするかを指定します。こちらも両方書いているので、どちらかをコメントアウトして使ってください。

また、補足ですが、コンテナを使うとエラーが起きた時に原因がわかりにくいことが多いです。

$ docker logs -f ${コンテナ名}

でコンテナのログを見れます。
また、Gunicornを使ってのFlaskアプリの起動は

$ gunicorn flask_app:app --config /var/www/gunicorn.py

で行っていますので、コンテナの起動に失敗している方は一度コンテナを使わない環境で動作確認された方が解決が早いかもしれません。

以上の設定で下記のコマンドでbuild+コンテナ起動をさせますと

$ docker-compose build
$ docker-compose up -d

UNIXドメインソケットを使われる方は
docker-compose.ymlのディレクトリに、gunicorn_socketというディレクトリができています。ここにgunicorn_flask.sockというファイルができており、通信できます。

エラーの確認がしやすい、負荷分散できることを考えると、個人的にはやはりTCPソケットですかね。
皆さんはどっちがいいとかありますか?

参考

調べなきゃ寝れない!と調べたら余計に寝れなくなったソケットの話

INETドメインとUNIXドメイン