Python製WebアプリケーションをDockerで運用する

前回：[EmbyをDockerで運用する](https://qiita.com/F_clef/items/8563fbcc8da3edc9c81c)
これはそろそろDockerを使えるようになりたい筆者の試行錯誤の軌跡です。

# Python製WebアプリケーションをDockerで
前回はメディアストリーミングサービスのEmbyをコンテナに移行しました。
![Untitled(11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227222/e79881af-36e0-7fce-a34f-acf98441a895.png)

今回はPython製Webアプリケーションを展開できるDockerイメージを作り、
Web Socket使用のアプリケーションとDjangoアプリケーションをDockerに移行します。
ついでに静的コンテンツもコンテナに移動し、VMサーバーを閉じます。
![Untitled(12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227222/c6337a2e-9a24-e86e-7416-83ff21030d48.png)

# 静的コンテンツ
VMサーバーにあったものをDockerホストに移動し、コンテナ起動時に配信ディレクトリへバインドしました。

```diff:docker-compose.yml
    deploy:
        image: nginx
        restart: always
        ports:
            - 80:80
            - 443:443
        volumes:
            - /home/fclef/docker/server/deploy/nginx/conf:/etc/nginx/conf.d
+           - /home/fclef/docker/server/deploy/content/html:/usr/share/nginx/html
```

# Python製アプリケーションのDocker運用
Web Socket使用アプリケーションもPython製(bottle)なので、Python製アプリケーションを簡単に展開できるコンテナを目指します。

## コンテナ設計
僕はよくPythonでWebアプリケーションを作ります。
なので、アプリケーションごとイメージに固めてしまうというよりは、
どんなPythonアプリケーションでも動かせる土台をイメージ化し、
具体的なアプリケーション自体はgitの情報をコンテナ起動時に渡して、コンテナ内でクローン、展開させます。
![Untitled(9) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227222/bca53a33-4acd-bd29-ebd3-36b19de5d892.png)

## Dockerfile
僕がPythonで何かを作るときは必ずPipenvを使います。
また、Webアプリケーションのときはソケット化してnginxで配信するので、
イメージの時点でPipenv, nginxをインストールしておきます。

```dockerfile:Dockerfile
FROM python:3.8-slim-buster

# 依存パッケージのインストール
RUN apt-get -y update
RUN apt-get -y upgrade
RUN apt-get install -y git make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libs
qlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev nginx

# Pyenvのインストール
RUN git clone https://github.com/yyuu/pyenv.git /.pyenv
ENV PYENV_ROOT /.pyenv
ENV PATH $PYENV_ROOT/bin:$PATH
RUN pyenv init - | bash
# Pipenvのインストール
RUN pip install pipenv
ENV PIPENV_VENV_IN_PROJECT 1
ENV PIPENV_YES true

RUN mkdir /app # アプリケーション配備ディレクトリ
RUN mkdir /script # 各種スクリプト配置ディレクトリ

COPY up.sh /script # 起動スクリプト

# 起動スクリプト内でgitプロジェクトをappにクローンするので移動しておく
WORKDIR /app
# Webアプリケーションの配信はメインWebサーバを介するリバースプロキシなので、80ポートだけ空けておく
EXPOSE 80
RUN chmod +x /script/up.sh
ENTRYPOINT [ "/script/up.sh" ]　# コンテナ起動時、起動スクリプトup.shを実行する
```

## コンテナ起動スクリプト
アプリケーションのgit情報はコンテナ起動時に環境変数で渡します。
僕の環境ではなぜかsshでのgitのクローンができないため、httpsでクローンする前提です。
クローンしたら、プロジェクトルートディレクトリにあるPipfileを解析し、必要なPythonバージョンを決定します。
Pythonライブラリの中には、依存するプログラムがインストールされている必要があるケースがあるので、
それらをインストールするためのスクリプト(dependencies.sh)をコンテナ起動時にバインドしておき、ここで呼び出します。
Python製Webアプリケーションのソケット化はgunicornを使うので、gunicornもインストールしておきます。

```bash:up.sh
#!/bin/bash

# 環境変数からgit情報を取得する
gitCloneUrl=${GIT_CLONE_URL}
gitUserName=${GIT_USER_NAME}
gitPassword=${GIT_PASSWORD}
gitCloneTarget=`echo ${gitCloneUrl} | sed -r "s#^(https?://)(.*)#\1${gitUserName}:${gitPassword}@\2#g
"`

# プロジェクト名を取得する
projectName=`echo ${gitCloneUrl} | sed -r "s#.*/(\w+)\.git#\1#g"`
echo "■ ■ ■ PROJECT NAME <${projectName}> ■ ■ ■"

git clone ${gitCloneTarget}

# Pipfileからpythonバージョンを取得する
cd ${projectName}
pythonVersion=`grep python_version Pipfile | sed -r 's/python_version = "(.+)"/\1/g'`
echo "■ ■ ■ PYTHON VERSION <${pythonVersion}> ■ ■ ■"

# インストールするPythonライブラリの中に他プログラムに依存するものがある場合は、ここでインストールしておく。
if [ -f /script/dependencies.sh ]; then
    source /script/dependencies.sh
fi

pipenv --python ${pythonVersion}
pipenv install
pipenv install gunicorn

curPath=`pwd`
export APP_ROOT=${curPath}

chmod +x /script/run.sh
service nginx start
/script/run.sh # アプリケーション起動スクリプト
while true; do sleep 1000; done
```



## コンテナの起動
例として、Djangoアプリケーションを起動する構成をお見せします。

```yml:docker-compose.yml抜粋
django:
    image: pipenv-gunicorn
    restart: always
    environment:
        GIT_CLONE_URL: https://xxxxxxxx/user/project.git
        GIT_USER_NAME: user
        GIT_PASSWORD: password
    volumes:
        - /home/fclef/docker/server/app/dependencies.sh:/script/dependencies.sh
        - /home/fclef/docker/server/app/run.sh:/script/run.sh
        - /home/fclef/docker/server/app/app.conf:/etc/nginx/conf.d/nginx_gunicorn.co
nf
    depends_on:
        - deploy
        - gitlab
```

```bash:dependencies.sh
apt-get -y --no-install-recommends install libpq-dev
```

```bash:run.sh
cd /app/project

source .venv/bin/activate

python manage.py collectstatic --noinput
python manage.py makemigrations
python manage.py migrate

deactivate

/app/project/.venv/bin/gunicorn \
          --access-logfile /var/log/socket_success.log \
          --error-logfile /var/log/socket_error.log \
          --workers 1 \
          --bind unix:/run/socket.sock \
          config.wsgi:application
```

```nginx:app.conf
server {
    listen       80;
    listen [::]:80;
    server_name xxxx.xxx.xxx;

    root /app;

    location /static {
        alias /app/project/static;
    }

    location / {
        include /etc/nginx/proxy_params;
        proxy_pass http://unix:/run/socket.sock;
    }

    location ~ ^/apple-touch-icon(.*)\.png$ {
        root /app/project/;
        rewrite ^/apple-touch-icon(.+)\.png$ /static/img/apple-touch-icon.png break;
    }
}
```

アプリケーション本体はコンテナ起動時にgitからクローン、
データは別コンテナのPosgreSQLに保存してあるので、
このコンテナは何度作成し直しても正常に運用状態のサービスを展開できます。

基本的に上記イメージと起動方法で大抵のPythonアプリケーションは動かせるようになりました。
gitクローンからデプロイまでの手順が単純なのでアプリケーションを外出しして再利用しやすいイメージが作成できましたが、
環境構築が複雑なアプリケーションの場合は、アプリケーションごとイメージにしてしまった方が保守性があがるかもしれません。

以上、VMサーバー群をコンテナに移行するシリーズでした。