はじめに
最近Docker上でアプリを動かしてみたいと思い、1~2週間ほど格闘してようやくタイトルにあるような構成でアプリを動かすことに成功しました。ほかの記事でも結構見かけるような構成ではあるのですが、コンテナ起動時のテーブル構築やデータの永続化といったテーマにも触れていきたいとおもいます。
環境
・Window10 Home 64bit
・WSL2
・Docker Desktop for Windows
ディレクトリ構成
flask/
├ app/
│ ├ config/
│ │ ├ requirements.txt
│ │ └ uwsgi.ini
│ ├ Dockerfile
│ └ run.py
├ database/
│ ├ data/
│ ├ docker-entrypoint-initdb.d/
│ │ └ 1_create_table.sql
│ └ Dockerfile
├ nginx/
│ ├ config/
│ │ └ nginx.conf
│ └ Dockerfile
└ docker-compose.yml
Githubはこちらです。
app
まずDockerfileです。
ワークディレクトリは/var/www/appにしていますが、任意です。
アプリケーションに必要なライブラリはrequirements.txtで管理するのでこれをインストールしています。
FROM python:3.9
# コンテナ内の作業ディレクトリを指定(以降のカレントディレクトリ)
WORKDIR /var/www/app
RUN apt-get update \
&& apt-get install -y vim \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
# ホストのカレントディレクトリをコンテナ内のカレントディレクトリにコピー
COPY . .
# ライブラリインストール
RUN pip install -U pip \
&& pip install --no-cache-dir -r /var/www/app/config/requirements.txt
ライブラリ管理はrequirements.txtで行っています。
uwsgi
flask
psycopg2
uwsgi.iniはnginxからsocketで通信を受け取る設定にしておきます。
[uwsgi]
master = True
socket = :3031
# http = :3031
chdir = /var/www/app/
wsgi-file = run.py
callable = app
logto = /var/log/uwsgi.log
アプリに関してはHello World
を返す簡単なものを作っていきます。
from flask import Flask, redirect, url_for, render_template
import psycopg2
from psycopg2.extras import DictCursor
# from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
# app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1, x_port=1, x_proto=1)
def pg_conn():
setting = {
'host': 'flask_db', # dbコンテナ名を指定
'port': '5432',
'dbname': 'postgres',
'user': 'postgres',
'password': 'postgres'
}
return psycopg2.connect(**setting)
@app.get('/')
def index():
return 'Hello World'
@app.get('/red')
def red():
with pg_conn() as conn:
with conn.cursor(cursor_factory=DictCursor) as cur:
sql = "INSERT INTO users(name) values('jessy')"
cur.execute(sql)
conn.commit()
return redirect(url_for('index'))
if __name__ == "__main__":
app.run()
database
Dockerfileです。イメージを取得するだけなのでdocker-compose.ymlで直接取得しても構わないのですが、あとからDockerfileに定義すべき事項が出てきたときに少しめんどくさいと思ったのでDockerfileを使っています。
FROM postgres:latest
次にコンテナ起動時に必要なテーブルが作成されるように.sqlファイルを作っておきます。コンテナ内の/docker-entrypoint-initdb.d
に置いておくとコンテナ起動時に実行してくれます。コンテナの中に入ってデータベースやテーブルを作成するといった作業を省略できます。実行対象ファイルは「.sql」「.sh」「.sql.gz」のようです。また、ファイル名のイニシャルに数字をつけておくことでDockerが順番に実行してくれます。
create table if not exists users (
id serial not null
, name character varying not null
, constraint users_PKC primary key (id)
) ;
最後に/data
という空のディレクトリを作成しておきます。これはこの後出てきますが、データを永続化するために使います。
nginx
FROM nginx:latest
# vimをインストールしています。必要がなければコメントアウトしてください。
RUN apt-get update \
&& apt-get install -y vim \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
設定
次にnginxの設定ファイルです。おもにnginx-uWSGI間の通信方法についてみていきますので、他の設定については言及しません。今回はuwsgiとHTTPプロトコルの2つを試してみました。
nginxとuWSGIを同一のコンテナで運用する場合unixソケットで通信することが一般的だと思いますが、今回は別々のコンテナで運用するのでunixソケットは使えません。
uwsgi
以下はuwsgiプロトコルで通信する場合の設定例です。
user nginx;
worker_processes auto;
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 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"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
upstream app_server {
# コンテナ名を使用
server flask_app:3031;
}
# map $http_x_forwarded_proto $real_scheme {
# default $scheme;
# https "https";
# }
# map $http_host $port {
# default $server_port;
# "~^[^\:]+:(?<p>\d+)$" $p;
# }
server {
listen 80 default;
charset utf-8;
server_name _;
# proxy_set_header X-Forwarded-For $remote_addr;
# proxy_set_header X-Forwarded-Host $host;
# proxy_set_header X-Forwarded-Port $port;
# proxy_set_header X-Forwarded-Proto $real_scheme;
location / {
client_max_body_size 1m;
client_body_buffer_size 8k;
include uwsgi_params;
# uwsgi(TCP Socketを使用)
uwsgi_pass app_server;
# HTTP
# proxy_pass http://app_server;
# proxy_redirect off;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
uwsgiを使う場合は、uwsgi_passディレクティブに通信先コンテナとポート(TCPソケット)を記載します。ここでは通信先コンテナにコンテナ名を利用しています(upstreamディレクティブ)。コンテナ名をコンテナのIPアドレスに変えても動くはずです。
HTTP
uWSGIと通信する場合、uwsgiプロトコル(uwsgi_pass)を使うことが推奨されているのですが、HTTP(proxy_pass)で通信することもあるかもしれないのでこちらでも設定してみました。
以下のように設定を変更します。(httpモジュールのみ抜粋)
http {
include /etc/nginx/mime.types;
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"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
upstream app_server {
# コンテナ名を使用
server flask_app:3031;
}
# 追加
map $http_x_forwarded_proto $real_scheme {
default $scheme;
https "https";
}
# 追加
map $http_host $port {
default $server_port;
"~^[^\:]+:(?<p>\d+)$" $p;
}
server {
listen 80 default;
charset utf-8;
server_name _;
# 追加
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $port;
proxy_set_header X-Forwarded-Proto $real_scheme;
location / {
client_max_body_size 1m;
client_body_buffer_size 8k;
include uwsgi_params;
# TCP Socket
# uwsgi_pass app_server;
# HTTP通信
proxy_pass http://app_server;
proxy_redirect off;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
またuwsgi.iniもHTTPにします。
[uwsgi]
master = True
# socket = :3031
http = :3031
chdir = /var/www/app/
wsgi-file = run.py
callable = app
logto = /var/log/uwsgi.log
さらにアプリ側も設定が必要です。
from flask import Flask, redirect, url_for, render_template
import psycopg2
from psycopg2.extras import DictCursor
# 追加
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
# 追加
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1, x_port=1, x_proto=1)
....
....
いろいろ設定が必要なのはこれらがないとflaskのredirect時にLocationヘッダURLがproxy_passディレクティブに設定されているURL(appコンテナ)になってしまいます。
そうならないようにwerkzeug.middleware.proxy_fix
というモジュールを使っています。これはアプリケーションの前にあるプロキシ(ここではnginx)が設定するX-Forwarded-に基づいて、WSGI環境を調整するモジュールです。(ドキュメント)
LocationヘッダにはnginxコンテナホストへのURLを設定したいので、X-Forwarded-にはnginxコンテナホストの情報を設定しています(nginx.conf)。
参考:https://github.com/sjmf/reverse-proxy-minimal-example
docker-compose.yml
ここまでのところがイメージの土台です。
ここからはイメージからコンテナを起動する際の設定について定義になります。
version: '3'
services:
database:
container_name: flask_db
build: ./database
expose:
- "5432"
volumes:
# 1_create_table.sqlをコンテナ内のdocker-entrypoint-initdb.dに置くため
- ./database/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
- ./database/data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES-DATABASE=postgres
- DATABASE_HOST=localhost
app:
container_name: flask_app
build: ./app
expose:
- "3031"
volumes:
- ./app:/var/www/app
depends_on:
- database
command: uwsgi --ini /var/www/app/config/uwsgi.ini
nginx:
container_name: flask_nginx
build: ./nginx
restart: always
volumes:
- ./nginx/config/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
ports:
- "8080:80"
データ永続化
ホストのdatabase/data
をコンテナ内の/var/lib/postgresql/data
にマウントしていますが、これはデータベースの内容が保存される/var/lib/postgresql/data
の内容をホスト上に同期するためです。
したがって、仮にコンテナを削除したとしても新たなコンテナの/var/lib/postgresql/data
にホストのdatabase/data
をマウントさせることで以前のデータを再び同期させ、データを引き継ぐことができます。ちなみに今回のマウント方法をバインドマウントといいます。
Dockerにはもう一つボリュームマウントというマウント方法があります。こちらはバインドマウントのようにホストのディレクトリをマウントさせるのではなく、DockerEngine管理下にあるボリュームと呼ばれる記憶領域をコンテナのディレクトリにマウントするものです。
こちらの方法でもボリュームが存続する限りデータを維持できることになります。ボリュームマウントの方法はトップレベルに明示的にボリュームを作り、バインドマウントのようにコンテナのディレクトリにマウントさせるだけです。
今回の場合であれば、以下のように修正すればよいです。
....
services:
database:
build: ./database
container_name: flask_db
expose:
- "5432"
volumes:
- ./database/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
# 作成したボリュームをコンテナにマウント
- volume_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES-DATABASE=postgres
- DATABASE_HOST=localhost
.....
.....
# 明示的にvolume_dataというボリューム(ボリューム名は任意)を作成
volumes:
volume_data:
実はもう一つ一時ファイルシステムマウントと呼ばれるマウント機能があるようです。今回は試さなかったので書いてませんが興味のある方は調べてみてください。
おわりに
これで一応タイトルのような構成でアプリを動かせます。Githubに上げていますのでぜひ参考にしてみてください。