Wizard Notes

Python, JavaScript を使った音楽信号分析の技術録、作曲活動に関する雑記

Docker+Nginx+uWSGIで複数のFlaskアプリ(コンテナ)を運用

概要

Docker上での、1つのuWSGI+Flaskコンテナで複数アプリを運用するスタイルは、Blueprintなどを駆使すれば可能です。

しかし、Blueprintの仕様のせいでディレクトリ構造が複雑になってしまったり、ルーティングの際に他のサービスに影響を与えてしまう可能性があります。また、Flaskには得体の知れないのハマりどころ・バグが所々にあり*1*2無駄な時間を費やさないために複雑な構成をなるべく避けるべきだと感じていました。

今回は、今後の本格的なWebアプリ開発を見据えて、保守性の向上やプロトタイピングの高速化のためにも Flask+uWSGIコンテナをアプリ・サービスごとに作る構成*3を作ってみました。

イメージとしては以下のような感じです。

Nginxコンテナ
├ (http://example.com) - website
├ (http://sub-1.example.com) - uWSGI - Flask app_1コンテナ - …
├ (http://sub-2.example.com) - uWSGI - Flask app_2コンテナ - …
…
└ (http://sub-n.example.com) - uWSGI - Flask app_n - …

ベース:NginxコンテナとuWSGI+Flaskコンテナ

https://qiita.com/trrrrrys/items/a905f1382733dfb9c8c1qiita.com

こちらを参考に組めば、Nginxコンテナと、uWSGI+Flaskコンテナ1つを立ち上げるところまでは簡単にできます。

今回はこれをベースに、uWSGI+Flaskコンテナを複数用意することで、複数アプリの開発・運用が簡単にできるようになります。

独自ドメインサブドメイン登録

全てのドメインサブドメインで同じようにDNSレコード(A)を設定します。

※Let's EncryptはDNSサブドメインのレコードを問い合わせるため、SSL 対応より先に実施してください。

SSL 対応(Let's Encrypt)

finnian.io

Step 1を実行します。example.com"YOUR_EMAIL"は書き換えてください。

docker run --rm \  
  -p 443:443 -p 80:80 --name letsencrypt \
  -v "/etc/letsencrypt:/etc/letsencrypt" \
  -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
  certbot/certbot certonly -n \
  -m "YOUR_EMAIL" \
  -d example.com \
  --standalone --agree-tos

実行後、/etc/letsencrypt/live/の中を確認し、(サブ)ドメインディレクトリ名が存在するかどうかを確認するとよいと思います。

各設定ファイルの編集

方針

Flask アプリとして、app1 と app2を作ります。

NginxとのTCPソケット通信のため、それぞれポートを割り振ります。

  • app1: 3031
  • app2: 3032

また、subdomainは

とします。

ディレクトリ構造

flaskapp/
 ├ app1/
 │ ├ Dockerfile
 │ ├ requirements.txt
 │ ├ uwsgi.ini
 │ └ src/
 │   └ run.py
 │
 ├ app2/
 │ ├ Dockerfile
 │ ├ requirements.txt
 │ ├ uwsgi.ini
 │ └ src/
 │   └ run.py
 │
 └ nginx/
   ├ Dockerfile
   └ nginx.conf
 

docker-compose.yml

nginxuwsgi_app1uwsgi_app2の3つのコンテナを立ち上げます。

version: "3.2"

services:

  uwsgi_app1:
    build: ./app1
    volumes:
      - ./app1:/var/www/
    ports:
      - "3031:3031"

  uwsgi_app2:
    build: ./app2
    volumes:
      - ./app2:/var/www/
    ports:
      - "3032:3032"

  nginx:
    image: nginx
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
      - /usr/share/nginx/html:/usr/share/nginx/html
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    links:
      - uwsgi_app1
      - uwsgi_app2
    ports:
      - 80:80
      - 443:443

uwsgi.ini

app1/uqsgi.ini では、socket = :3031となっています。

app2/uqsgi.iniでは、socket = :3032となることに注意してください。

[uwsgi]
wsgi-file = /var/www/src/run.py
callable = app
master = true
processes = 1
socket = :3031
chmod-socket = 666
vacuum = true
die-on-term = true
py-autoreload = 1

nginx.conf

SSLへのリダイレクトもルーティングを書いています。

  • upstream
  • ssl_certificate, ssl_certificate_key
  • localionuwsgi_pass
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;
  keepalive_timeout 65;

  upstream uwsgi_app1 {
    server uwsgi_app1:3031;
  }
  upstream uwsgi_app2 {
    server uwsgi_app2:3032;
  }
  
  server {
    listen 80;
    server_name  ~^(?<subhost>.+)\.example\.com$;
    return 301 https://example.com/$subhost$request_uri;
  }

  
  server {
    listen              443 ssl;
    server_name         app1.example.com;
    ssl_certificate     /etc/letsencrypt/live/app1.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app1.example.com/privkey.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        include uwsgi_params;
        uwsgi_pass uwsgi_app1;
    }
  }
  server {
    listen              443 ssl;
    server_name         app2.example.com;
    ssl_certificate     /etc/letsencrypt/live/app2.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app2.example.com/privkey.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        include uwsgi_params;
        uwsgi_pass uwsgi_app2;
    }
  }
}

起動

docker-compose build
docker-compose up -d

app{1,2}フォルダに、ローカルで開発したflaskアプリのファイル一式を放り込めばアプリを動かせるので楽です。

補足

Let's Encrypt で複数のSSL証明書を一括取得

SSL証明書あたりの準備や記述が冗長なので、一つの証明書でいけるようにしたいところです(時間がある時にでも)。

www.infact1.co.jp

blog.apar.jp

サーバ再起動時の手順

サーバメンテナンス時の対応方法(コンテナの終了・起動)のメモです。

  1. >make stop でコンテナを終了
  2. docker ps -a で起動しているコンテナや、マウントされているイメージの確認
    1. docker rm xxxxxx yyyyy zzzzz でイメージ削除
  3. サーバ再起動
  4. >lsof -i:443, >lsof -i:80 等で、dockerで使うポートがすでに使われていないか確認
    1. Nginxが使われていたら、nginx -s stopなどで止める
  5. >makeでコンテナ起動

*1:https://methane.hatenablog.jp/entry/2012/12/24/214950

*2:サンプルコードやチュートリアルの先は暗闇の地雷原が広がっています

*3:Flaskがmicroframework なので、きっとそうするべきなのだと思います