Help us understand the problem. What is going on with this article?

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

はじめに

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

docker構成

docker構成.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

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ディレクティブ」を参照。)

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のサーバーはデフォルトだと公開されてない

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)について真面目に調べた

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のコンテナ作成と初期化

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コンテナはデフォルトのまま利用するので、参照元のイメージ指定だけ行います。

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アプリでは、依存パッケージを記載しているPipfilePipfile.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イメージ作成とコンテナ起動時処理のためのコマンドを書いていきます。

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を改めて解説する」を参照。)
コンテナ起動後にやりたいことは、
1. pipenvのインストール
2. 依存パッケージのインストール
3. gunicornを介してのアプリ起動
なので、それらを順次行えるようにコマンドを記載しています。コンテナの中で以下のコマンドを実行するイメージですね。

$ pip install pipenv
$ pipenv install --system
$ gunicorn run:app -b 0.0.0.0:4000

pipenv install --systemでは、PipfilePipfile.lockから、依存パッケージのインストールを行なっています。コンテナ内でわざわざpython仮想環境を立てる必要はないので、--systemをつけて、コンテナ内に直接パッケージのインストールを行なっています。
gunicorn run:appで、run.pyapp変数を渡してgunicornを介してのflaskアプリ起動を行なっています。その際の-b 0.0.0.0:4000オプションで、gunicornで受け入れるポートを指定しています。
※参考 Running Gunicorn
※参考 docker-composeでgunicorn+nginx+flaskを動かしてみた話 - ハマったポイント②:gunicorn起動にbindすべし

また、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イメージ作成とコンテナ起動時処理のためのコマンドを書いていきます。

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ハマりポイント
また、-c /pass/to/configfileで、nginx設定ファイルの指定を行なっています。
※参考 CommandLine | NGINX

さらに、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:
    #(略)

コンテナの起動・動作確認・停止・削除

本当は公式ドキュメント読み込まないといけないところなんですけど、時間がなかったので日本語でまとまっているこちら「docker-compose コマンドまとめ」を参考にさせていただきました。

dockerイメージのビルド

$ docker-compose build

コンテナの起動

$ docker-compose up -d

ブラウザ表示確認

ブラウザでlocalhostにアクセスすると、メインページが表示されるはずです。
スクリーンショット 2019-07-28 18.24.45.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

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

app/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

postgres/Dockerfile
FROM postgres

nginx.conf

解説した部分以外はこちら「Flask+uwsgi+nginxの環境が作りたい?それ、Dockerなら1コマンドで出来るよ。」をかなり参考にしました。

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さんがよしなにやってくれていたWebサーバ(nginx)やDBサーバ(postgres)、wsgi(gunicorn)周りがどう動いているのかちょっとだけ理解できました。
今後の方向性としては
1. 他のDockerミドルウェア/アプリと繋ぐ(とりあえずElasticSearchを使ってみたい)
2. クラウドサーバにデプロイしてサービス公開(EC2に乗せてみたい)
3. コンテナオーケストレーション(kubernetes使ってみたい)
という感じで進めたいと思います!

参考まとめ

Docker-docs-ja expose
nginx連載5回目: nginxの設定、その3 - locationディレクティブ
docker-composeでgunicorn+nginx+flaskを動かしてみた話
Docker、ボリューム(Volume)について真面目に調べた
DockerfileのCMDとENTRYPOINTを改めて解説する
dockerでPostgreSQLのコンテナ作成と初期化
Running Gunicorn
Docker 事始め
CommandLine | NGINX
docker-compose コマンドまとめ
Flask+uwsgi+nginxの環境が作りたい?それ、Dockerなら1コマンドで出来るよ。

kiyokiyo_kzsby
新卒2年目。 ソフトウェアエンジニア×ITコンサル。 業務→フロント〜サーバサイド。 インフラにも手を出したい。
http://kiyokiyo.s3-website-ap-northeast-1.amazonaws.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした