Dockerひとつでリモートデバッグ可能な開発・本番環境を作る方法

- [English Version (英語版)](https://qiita.com/sebastianrettig/items/a52f6a5c36288db7b823)

MacOS、Windows、Linux等さまざまなプラットフォーム上でアプリ開発をしていると、「この環境じゃ動かない」といった問題がよくあります。Dockerの様なコンテナ技術を利用すれば、コマンド１つで簡単に開発環境を整えることができ、セットアップに無駄な時間を費やすことはありません。

ここでは、Dockerを使ったPythonアプリの環境構築方法を紹介します。
FlaskのWebアプリ開発環境を例に、開発・本番の両環境を構築しますが、[Docker Multistage Build](https://docs.docker.com/develop/develop-images/multistage-build/)を使って１つのDockerファイルで作成していきます。
また、開発環境にはデバッガーをインストールし、Dockerコンテナ上でのリモートデバッグもセットアップします。
デバッグには、[Visual Studio Code](https://code.visualstudio.com/)の [Microsoft Python Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)を使用します。

さっそく始めてみましょう！

# プロジェクトファイルの構成

まずは以下のような構成でPythonプロジェクトを作成します。

```
MyFlaskApp/
├ app.py
├ docker-compose.yaml
├ Dockerfile
└ requirements.txt
```

プロジェクトファイルの内容は、Flaskアプリのコードを書く ```app.py``` 、Dockerイメージをビルドする為の ```Dockerfile``` とコンテナで開発環境を構成する為の ```docker-compose.yaml``` 、Flaskアプリに必要なPythonモジュールを列挙する ```requirements.txt``` となります。
まずは空のファイルを作成し、それぞれに書く内容は後ほど説明していきます。

```bash
mkdir MyFlaskApp
cd MyFlaskApp
touch app.py
touch docker-compose.yaml
touch Dockerfile
touch requirements.txt
```

# アプリケーションコード

 ```app.py``` ファイルでは、簡単なデモのためにFlaskのドキュメントからHelloWorldのコードをコピペするだけです。

```python:app.py
from flask import Flask
app = Flask(__name__)


@app.route('/')
def hello_world():
    return 'Hello, World!'


if __name__ == "__main__":
    app.run(
        debug=True,
        host='0.0.0.0',
        port=5000
    )
```

以下のブロックは

```python
@app.route('/')
def hello_world():
    return 'Hello, World!'
```

ルートに ```/``` を割り当てて、"Hello World!"の文字列を返します。
最後のブロックは


```python
if __name__ == "__main__":
    app.run(
        debug=True,
        host='0.0.0.0',
        port=5000
    )
```

コマンドラインからスクリプトを実行したときにFlaskアプリを起動するコードです。
 (例： ```python app.py``` や ```python -m app```)

# Dockerfile

以下を ```Dockerfile```にコピペします。

```bash:Dockerfile
FROM python:3.7-alpine AS base

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

ENV FLASK_ENV="docker"
ENV FLASK_APP=app.py
EXPOSE 5000

# Development Stage
FROM base AS develop
RUN pip install debugpy
# Keeps Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE 1
# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED 1

# Production Stage
FROM base AS production
RUN pip install --no-cache-dir gunicorn
COPY . .
CMD ["gunicorn", "--reload", "--bind", "0.0.0.0:5000", "app:app"]
```

ここでは、１つの```Dockerfile```で複数のステージ（Docker images）をビルドできる
[Multistage Builds](https://docs.docker.com/develop/develop-images/multistage-build/)という機能を利用して、３つのステージを設定しています。
- ```base```: 依存モジュールをインストールする
- ```development```: 開発に必要な依存モジュールのインストール(デバッガーなど)
- ```production```: イメージにソースファイルをコピーし、本番環境用にプロキシサーバをインストールします。

## Baseステージ
```Dockerfile```の最初の行で ```base``` ステージを宣言します。

```bash
FROM python:3.7-alpine AS base
```

```FROM``` ではアプリ構築のベースとなる環境を指定します。
ここではpythonアプリを作成するので、python 3.7のDockerイメージを指定します。
OSの選択は開発したいものにもよりますが、ここでは軽量なUbuntuディストリビューションである```Alpine```を選択します。
```AS base```で```base```というステージ名にします。ステージ名は自由につけられます。
以降のコマンドは別の```FROM```が現れるまで、```base```ステージに適用されます。
```WORKDIR /app```は、イメージ内のアプリケーション用のフォルダを作成し、そのフォルダへ移動します。
次の2行で

```bash
COPY src/requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
```

```app/``` フォルダにPython依存モジュールのリストをコピーし、依存モジュールのインストールまで行います。
次の3行でFlask用の環境変数を定義します。

```bash
ENV FLASK_ENV="docker"
ENV FLASK_APP=app.py
EXPOSE 5000
```
```ENV FLASK_ENV="docker"```の行では、Flaskが起動時に参照する環境設定を*.env*ファイルに定義しています。
このチュートリアルでは*.env*ファイルは使用しませんが、とりあえず追加しておきます。
2行目の```ENV FLASK_APP=app.py```は、起動時にどのファイルを実行するかをFlaskに指示します。
最後の行の```EXPOSE 5000```では、Flaskアプリケーションのポートを*app.py*で設定した5000に設定しています。

## Development ステージ

developmentステージでは、```base```ステージを再利用し、デバッガーを追加でインストールします。
developmentステージは以下のように定義します。

```bash
FROM base as develop
```
以降のコマンドは別の```FROM```が現れるまで、```develop```ステージに適用されます。

pythonのデバッガーは[debugpy](https://github.com/microsoft/debugpy)をインストールします。

```bash
RUN pip install debugpy
```

次に、開発環境用にpythonのセットアップを行います。

```bash
# Keeps Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE 1
# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED 1
```
最初の行```ENV PYTHONDONTWRITEBYTECODE 1```は python のbyte-code generationを無効にします。
これは本番環境では有効にするべき対応ですが、頻繁にコードが変更される開発環境ではハードディスクのメモリが増えてしまいます。
> もし開発中に動作が遅いと感じた場合は、この設定をコメントアウトして下さい。

2行目の```ENV PYTHONUNBUFFERED 1```はログ用のバッファを無効にし、メッセージを直接 dockerコンテナのランタイムに表示します。
ソースファイルはdockerイメージにコピーせず、dockerコンテナ起動時にローカルディレクトリから直接リンクします。セットアップは```docker-compose.yaml```ファイルの説明で行います。

## Production ステージ

productionステージは、デバッガーをインストールした状態で本番稼働させたくありません。
なので、```base```ステージから作成します。

```bash
FROM base as production
```

本番環境へアクセスさせる為、HTTPサーバとして[gunicorn](https://gunicorn.org/)をインストールしています。

```bash
RUN pip install --no-cache-dir gunicorn
```

次に、本番ビルド時にはソースコードの変更はないのでイメージにコピーします。

```bash
COPY . .
```

最後の行では、起動時にgunicorn HTTPサーバを立ち上げます。サーバは```base```ステージで設定した5000番ポートをリッスンし、*app.py* を実行します。

```bash
CMD ["gunicorn", "--reload", "--bind", "0.0.0.0:5000", "app:app"]
```

# Docker Compose ファイル

ここでは[Docker Compose](https://docs.docker.com/compose/)を使って開発環境のセットアップをします。
```docker-compose.yaml```に以下をコピペして下さい。

```yaml:docker-compose.yaml
version: "3.9"

services:
  flask-app:
    image: my_flask_app-develop
    container_name: my_flask_app-develop
    build:
      context: .
      target: develop
    ports:
      - 5000:5000
      - 5678:5678
    volumes:
      - .:/app
    environment:
      - FLASK_DEBUG=1
    entrypoint: [ "python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "-m", "app",  "--wait-for-client", "--multiprocess", "-m", "flask", "run", "--host", "0.0.0.0", "--port", "5000" ]
    networks:
      - my_flask_app-develop

networks:
  my_flask_app-develop:
    name: my_flask_app-develop
```

次に各セクションの設定を説明していきます。 

## Services セクション

*services*の部分で使用するサービスを定義します。
今回はFlaskアプリを定義するだけですが、データベースなどのdockerイメージを用意し、サービスとして追加することもできます。

以下の行でFlaskアプリをサービスとして定義しています。

```yaml
services:
  flask-app:
    image: my_flask_app-develop
    container_name: my_flask_app-develop
```

```image``` ではDockerイメージのビルド時に割り当てるタグ名を指定します。
タグ名は後から```docker images```コマンドを使ってイメージのリストから対象のイメージを見つける時に役立ちます。
```container_name```ではコンテナ名を指定します。
コンテナ名を指定しない場合は、ランダムに生成されたコンテナ名が割り当てられます。

```build``` セクションでは、dockerに何をビルドするかを指示します。

```yaml
    build:
      context: .
      target: develop
```

```context``` には```docker-compose.yaml```からみたビルド対象となる```Dockerfile```の位置を指定します。
```target```では、ビルドするステージを指定します。この場合は```development```ステージです。

```ports``` セクションでは使用するポートを定義します。


```yaml
    ports:
      - 5000:5000
      - 5678:5678
```

今回は、ローカルポート*5000*と*5678*をコンテナ内の同じポート番号にマッピングしています。
ポート*5000*は Flask アプリにアクセスする為の指定で、ブラウザを開いて http://localhost:5000 からアプリを実行することができます。
ポート*5678*は、デバッガがデバッグ要求をリッスンするために使用します。

```volumes``` を使用して、コンテナとローカルアプリケーションフォルダをリンクすることができます。

```yaml
    volumes:
      - .:/app
```
これにより、ソースコードを変更する度にdocker imageを再ビルドせず、ソースコードの変更をコンテナに反映することができます。

```environment``` では環境変数を定義することができます

```yaml
    environment:
      - FLASK_DEBUG=1
```
今回は、Flaskでデバッグモードを有効にします。これにより、ブラウザ上に詳細なエラーメッセージが表示されます。

```entrypoint``` は最も重要なセクションで、起動時に何を実行するかを定義します。

```yaml
    entrypoint: [ "python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "-m", "app",  "--wait-for-client", "--multiprocess", "-m", "flask", "run", "--host", "0.0.0.0", "--port", "5000" ]
```
最初にデバッガーを実行してポート 5678 をリッスンします。
次に、Flask アプリをデバッガーに設定し、デバッガーはポート*5678*でクライアントの接続を待機させます。
*multiprocess* を指定すると、ポート*5000*でのデバッグの他に、実行中のアプリケーションにアクセスできるようになります。

## Networks セクション

*networks*では、サービスをして定義することで、異なるサービス間の通信を制御することができます。
今回はFlaskアプリ用のネットワークだけ定義しています。詳しい情報やDockerコンテナ間の接続の確立方法については、[Docker ComposeのドキュメントのNetwork](https://docs.docker.com/compose/networking/)を参照してください。

```yaml

services:
  flask-app:
    # ...
    networks:
      - my_flask_app-develop

networks:
  my_flask_app-develop:
    name: my_flask_app-develop
```

# requirements.txt ファイル

```requirements.txt``` ファイルには、Flaskアプリを実行するために必要なpythonモジュールを全て記載します。
今回必要なモジュールは*flask*のみなので、以下をコピペして下さい。

```python:requirements.txt
Flask
```
>今回はFlaskのバージョンを指定しておりません。必要な場合は適宜設定して下さい。 (例 Flask==1.1.2)

# 開発環境の起動

開発環境を実行するには、アプリケーションディレクトリ内で次のコマンドを実行するだけです。

```bash:Terminal
docker-compose up -d
```

```develop``` ステージのdockerイメージがビルドされ、アプリのdockerコンテナが立ち上がります。
コンテナを停止するには次のコマンドを実行します。

```bash:Terminal
docker-compose down
```

> docker composeをフォアグラウンドで実行したい場合は、*-d*フラグを省いて下さい。その際は、*CTRL+C*でアプリを停止できます。

# デバッグ

デバッグには[Visual Studio Code](https://code.visualstudio.com/)を使用します。
> [PyCharm](https://www.jetbrains.com/pycharm/)を利用したい場合、無償のCommunity Editionではリモートデバッグをサポートしていない為、Professional Editionライセンスが必要となります。

まずはアプリケーションフォルダで以下のコマンドを実行します。

```bash:Terminal
cd MyFlaskApp
code .
```
> ```code: command not found```のエラーが表示された場合、Visual Studio Codeをアプリアイコンから開き、
Macの場合はCommand+Shift+P（WindowsやLinuxの場合はCtrl+Shift+P）でコマンドパレットを起動します。
「ShellCommand」と入力し、「Shell Command：Install 'code' command in PATH」を選択します。

アプリケーションフォルダがVisual Studio Codeで開くはずです。

## Python Extensionのインストール

左メニューのExtensionsをクリックします。検索バーに"python"と入力し、
[Microsoft Python Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)をインストールします。
![python_extension.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/fe1351ff-8ed2-cd97-9fd2-6d7adaa2011d.png)


## リモートデバッグの設定

デバッグ設定は*.vscode/*フォルダ内の*launch.json*に保存されます。
リモートデバッグの設定手順は*launch.json*が既にプロジェクトフォルダにあるかによって異なります。

### launch.json ファイルの作成方法

*launch.json*ファイルが存在しない場合は、次の手順で生成します。
*app.py* ファイルをVisual Studio Codeで開いて、ボトムバーにPython言語が自動設定されているか、確認します。

![python_language_mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/f3e8fd5c-44a7-0021-6216-74df653744f0.png)

左メニューのDebugをクリックし、次に、**create a launch.json file**をクリックします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/79c24cbd-43dc-225a-067e-ea823323f1bd.png" alt="debug_setup1.png">
<em>launch.jsonファイルが存在しない場合</em>

Python Extensionのインストールに成功していれば、デバッグ設定のリストから**Python**を選択できるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/0763f75b-8842-93cd-5a69-ca9c662a307a.png" alt="debug_setup2.png">
<em>launch.jsonファイルを作成</em>

次に、**Remote Attach**を選択します。

![debug_setup3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/84934f77-7b92-39d9-ebba-329c8ce9ee12.png)

次に、接続するリモートデバッガーを選択します。ホスト名は*localhost*、ポートは*5678*番を使用します。

![debug_setup4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/09d4a9e7-ff6b-dbf4-9280-1586f865f72b.png)
![debug_setup5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/e26a552a-9d1f-656c-58d1-5bb9459e512f.png)

自動作成された*launch.json*は下記の通りです。

```javascript:launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Remote Attach",
            "type": "python",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 5678
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "."
                }
            ]
        }
    ]
}
``` 

### 既のlaunch.jsonに設定追加方法

プロジェクトフォルダに既にlaunch.jsonファイルがある場合は、以下の設定を追加します。
*app.py* ファイルをVisual Studio Codeで開いて、ボトムバーにPython言語が自動設定されているか、確認します。

![python_language_mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/f3e8fd5c-44a7-0021-6216-74df653744f0.png)

左メニューのDebugをクリックし、表示されたセレクトボックスから下矢印をクリック**Add Configuration...**を選択し、設定を追加することも可能です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/59cf3386-fcae-a480-f14a-a424d59a10a6.png" alt="debug_add_configuration.png">
<em>launch.jsonファイルが存在する場合</em>

Python Extensionのインストールに成功していれば、デバッグ設定のリストから**Python**を選択できるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/d443cc9b-e17b-6b68-7cf5-ff36dfe41a31.png" alt="debug_add_configuration2.png">
<em>launch.jsonファイルに設定追加の場合</em>

次に、**Remote Attach**を選択します。

![debug_setup3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/84934f77-7b92-39d9-ebba-329c8ce9ee12.png)


次に、接続するリモートデバッガーを選択します。ホスト名は*localhost*、ポートは*5678*番を使用します。

![debug_setup4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/09d4a9e7-ff6b-dbf4-9280-1586f865f72b.png)
![debug_setup5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/e26a552a-9d1f-656c-58d1-5bb9459e512f.png)

*launch.json*のconfigurationsは下記の通りです。

```javascript:launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Remote Attach",
            "type": "python",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 5678
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "."
                }
            ]
        }

        // ...他の設定

    ]
}
``` 

## ブレークポイントの設定とデバッガーのアタッチ

```app.py``` ファイルを開き、７行目にブレークポイントを設定します。次に、左側にある緑色の再生ボタンをクリックして、デバッガーに接続します。
ブラウザで http://localhost:5000 にアクセスし、アプリを起動します。
アプリはブレークポイントで停止するはずです。
![debug.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/b8f8d746-aa38-d9cd-25bc-9db1e991eaf7.png)
何度でもデバッガーに接続可能です。

# 本番ビルド

```production``` ステージをビルドするには、アプリケーションフォルダ内で次のコマンドを実行します。

```bash:Terminal
docker build --target production -t my_flask_app .
```
```--target``` でビルドするステージを指定し、```-t my_flask_app```でイメージに適当なタグ名を付けます。
最後の*.*は重要で、Dockerfileの場所、つまりカレントディレクトリを指しています。

> ビルドされたイメージには任意のタグ名がつけられますが、開発と本番で同じものを使用しない様に注意して下さい。

# ビルド済み Images の一覧

```docker images``` コマンドで、ビルドしたイメージをリスト表示することができます。

```bash:Terminal
docker images

REPOSITORY                          TAG          IMAGE ID       CREATED        SIZE
my_flask_app                        latest       a72b11d35c59   2 days ago     52.2MB
my_flask_app-develop                latest       5916db28c0ff   2 days ago     80.5MB
```

> デバッガーあり（80.5MB）とデバッガーなし（52.2MB）のイメージサイズの違いも確認できます。

# 本番 Image の起動

本番イメージからコンテナを生成して実行するには以下のコマンドを使用します。

```bash:Terminal
docker run -d --rm -p 5001:5000 --name my_flask_app my_flask_app:latest
```
ここではローカルのポート*5001*をコンテナのポート*5000*にマップすることで、本番ステージと開発ステージを同時に実行できるようにします。

| Stage       | URL                   | 
|:------------|:----------------------|
| development | http://localhost:5000 |
| production  | http://localhost:5001 |

次のコマンドで実行中のコンテナをリスト表示できます。

```bash:Terminal
docker ps

CONTAINER ID   IMAGE                 COMMAND                  CREATED         STATUS         PORTS                    NAMES
a8bc12d89d9f   my_flask_app:latest   "gunicorn --reload -…"   7 seconds ago   Up 6 seconds   0.0.0.0:5001->5000/tcp   my_flask_app
```

コンテナの停止は以下のコマンドのみです。

```bash:Terminal
docker stop my_flask_app
```

> コマンドをフォアグラウンドで実行したい場合は、*-d*フラグを省いて下さい。その際は、*CTRL+C*でアプリを停止できます。

# トラブルシューティング
## Remote Attachについて
**問題**: デバッガ切断ボタンが効かない。
![vscode_remote_attach.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/8a1bd622-09e2-e676-fb24-5ec83d51ef30.png)

**解決方法**: vscodeの設定で、``Show Sub Sessions In Tool Bar``が有効になっていることを確認してください。
![vscode_settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/38a5b622-e1dd-09ad-0d1a-93295f34ddba.png)
[Githubのチケット](https://github.com/microsoft/vscode/issues/159600#issuecomment-1232005430)