DockerとVScodeでPython環境構築

# Introduction
今までVSCodeを使ってPython環境をローカルPCに構築しデータ分析を主に行ってきた．
Dockerというものがあることは前々から知っており，必要なツールはインストールした．
色々な解説記事を見ると`Dockerfile`や`docker-compose.yml`や`devcontainer.json`などのファイルがいくつも出てきて何が必要なのかよくわからなかった．

そんな自分が5つのステップで学習を進めたらDockerの理解ができたので備忘録的にこの記事にまとめる．
- Step1:DockerHubから取得したDockerイメージを使用する
- Step2 Dockerfileを使用する
- Step3 DockerComposeを使用する
- Step4 devcontainer.jsonを使用する
- Step5 複数のコンテナを使用する


VSCodeとその拡張機能，DockderDesktop，WindowsであればWSLといった，Dockerを使うために必要なツール類のインストールに関しては他の記事を参照されたし．

自分が本記事を作成した時のスタート地点とゴール
- スタート
   - VSCodeを使ってローカルでPythonを動かしたりライブラリをインストールしたことはある．
   - VScodeを使ってDocker環境を作るために必要なツールはインストール済み．

ゴール
- DockerとVSCodeを使ってPythonの環境構築ができる．

成果物はGitHubにあげておく．

https://github.com/KiYuRo/docker-python-vscode

# Step1 DockerHubから取得したDockerイメージを使用する
## イメージを取得する
DockerHubにアクセスしpythonと検索すると使用可能なイメージの一覧が見れる．

https://hub.docker.com/

DockerHubからDockerイメージを取得する．
```
$ docker image pull python:3.10-slim
```

取得したイメージ一覧を確認する．
```
$ docker image ls

REPOSITORY   TAG         IMAGE ID       CREATED       SIZE
python       3.10-slim   b4447fd198e3   4 weeks ago   147MB
```

## コンテナの作成と起動
いくつかのオプションを指定しコンテナの作成と起動をおこなう．
- `docker container run`：コンテナを作成し起動する基本コマンド
- `-it`：対話型モードで起動するオプション
- `--rm`：コンテナ終了時にコンテナを削除するオプション．
- `python:3.10-slim`：イメージを先ほど確認した`REPOSITORY:TAG`の形式で指定する（`IMAGE ID`でも可）
- `/bin/bash`：コンテナ起動と同時にシェルを起動するオプション．これを指定しない場合はpythonインタプリンタが起動．
```
$ docker container run -it --rm python:3.10-slim /bin/bash

root@d5197617f85c:/#
```
コンテナ起動後はLinuxの基本コマンドやpythonコマンドが使える．
```
root@d5197617f85c:/# ls
bin   dev  home  lib32  libx32  mnt  proc  run   srv  tmp  var
boot  etc  lib   lib64  media   opt  root  sbin  sys  usr

root@d5197617f85c:/# python --version
Python 3.10.12
```

別のターミナルを開いて起動中のコンテナを確認する．
```
$ docker container ls

CONTAINER ID   IMAGE          COMMAND       CREATED              STATUS              PORTS     NAMES
f0cf0db8e076   b4447fd198e3   "/bin/bash"   About a minute ago   Up About a minute             wizardly_yalow
```


## VScodeでコンテナにアクセスしpythonを実行する
VSCodeの拡張機能Remote Developmentはインストールしておく．

1. VScodeの画面左下隅にある`><`のような青いアイコンをタップ
2. Attach to Running Containerをクリック
3. 起動中のコンテナ名（先ほど確認した`NAMES`の値）をクリック

ここまでの内容をおこなえば，コンテナに接続したVSCodeのウィンドウが立ち上がる．
コンテナ内には既にいくつかのフォルダが存在するが，optフォルダの中身は空なのでまずはそこに移動するとよい．
あとは普段と同じように拡張機能をインストールしたり，ディレクトリを作成したり，pythonファイルを作成して実行すればよい．

## コンテナを終了
コンテナを終了する．
※作成したディレクトリやpythonファイルはコンテナとともに消えてしまう．
```
$ exit
```

## ローカルPCの内のディレクトリをコンテナ内にマウントする．
マウントとはローカルPC内のディレクトリとコンテナ内のディレクトリを同期すること．
マウントすることでコンテナ内で作成したファイルはローカルPCにも保存されるため，コンテナを消してもファイルは残り続ける．

まずローカル内に以下のようなディレクトリ構成を作る．
```
step1/
  workspace/
    hello.py
```

コンテナの作成と起動を行うコマンドを実行する．
- `--mount`：マウントするためのオプション
- `type=bind`：マウントの種類を指定
- `source=＜絶対パス＞/workspace`：ローカルPC内のマウントしたいディレクトリを絶対パスで指定
- `target=/workspace`：コンテナ内のディレクトリを指定
```
$ docker container run -it --rm --mount type=bind,source=＜絶対パス＞/workspace,target=/workspace python:3.10-slim /bin/bash
```
```
root@e9783105d4bf:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  workspace
root@e9783105d4bf:/# ls workspace
hello.py
```
コンテナ内にworkspaceディレクトリとhello.pyが既に存在することが確認できる．
コンテナ内のworkspace内に新しいディレクトリやファイルを作成するとローカルPCのworkspaceにも同じものが作成される．
※workspace外で作成したものはコンテナとともに消えてしまう．


# Step2 Dockerfileを使用する
## Dockerfileを作成する
Dockerfileを使用することで，DockerHubから取得したベースイメージをカスタマイズできる．
まず以下のようなディレクトリ構成を作る．
```
step2/
  workspace/
    /src
      hello.ipynb
    Dockerfile
    requirements.txt
```

Dockerfileを作成する．
- FROM：ベースイメージを指定
- WORKDIR：作業ディレクトリを指定
- COPY：ローカルPCからコンテナにコピーするディレクトリやファイルを指定
- RUN：イメージ作成時に実行するコマンドを指定
- CMD：コンテナ起動時に1度だけ実行するコマンドを指定
```:Dockerfile
# DockerHubのベースとなるイメージを指定
FROM python:3.11-slim

# 作業ディレクトリを変更
WORKDIR /workspace

# ローカルPCのファイルをコンテナのカレントディレクトリにコピー
COPY requirements.txt ${pwd}

# pipのアップデート
RUN pip install --upgrade pip

# pythonパッケージをインストール
RUN pip install -r requirements.txt

# コンテナ起動時に実行するコマンドを指定
CMD ["/bin/bash"]
```

Dockerfileと同じ階層にrequirements.txtを作成する．
```:requirements.txt
jupyter
numpy
pandas
```

## イメージを作成する
step2フォルダに移動し以下のコマンドでDockerfileからイメージを作成する．
- `-t python:step2`：`-t REPOSITORY名:TAG名`の形式で指定
- ` workspace/.`：Dockerfileの場所をカレントディレクトリからの相対パスで指定
```
$ docker image build -t python:step2 ./workspace

[+] Building 33.6s (10/10) FINISHED
```

```
# docker image ls
REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
python       step2       ebf33e38b104   43 hours ago   534MB
python       3.10-slim   b4447fd198e3   4 weeks ago    147MB
```

## コンテナの作成と起動
あとはStep1と同じようにコンテナの作成と起動をおこなえばよい．

Step1と異なる点
- /bin/bashを指定しなくてもよい（DockerfileのCMDで指定しているため）
- シェルのカレントディレクトリがルートではなくworkspaceとなっている（DockerfileのWORKDIRで指定しているため）
- pythonライブラリがインストールされている（DockerfileのRUNでインストールを実行しているため）
```
$ docker container run -it --rm --mount type=bind,source=＜絶対パス＞/workspace/src,target=/workspace/src python:step2

root@3e1b626c3542:/workspace# ls
requirements.txt  src

root@3e1b626c3542:/workspace# pip list
Package                   Version
------------------------- --------
...
jupyter                   1.0.0
numpy                     1.25.1
pandas                    2.0.3
pip                       23.2
...
```

# Step3 DockerComposeを使用する
Docker Composeを使用することで，コンテナの作成や起動を簡潔におこなうことができる．
以下のディレクトリ構成を作成
```
step3/
  workspace/
    .devcontainer/
      Dockerfile
      requirements.txt
      docker-compose.yml
    src/
      hello.ipynb
```

## docker-compose.ymlを作成する
docker-compose.ymlはコンテナの作成や起動を管理する設定ファイル．
Docker関連のファイルが増えてきたので.devcontainerフォルダににまとめる．
docker-compose.yml以外はStep2と同じものを使用する．

docker-compose.ymlを作成する
```yml
services:                           # サービスを定義しますという決まり文句
  python:                           # pythonという名前でサービスを定義
    image: python:step3             # イメージのREPOSITORY名:TAG名を指定
    build: .                        # Dockerfileを相対パスで指定
    container_name: python-step3    # 作成されるコンテナ名を指定
    working_dir: /workspace/src     # 作業ディレクトリを指定
    volumes:                        # マウントするファイルを指定する
      - ../src:/workspace/src       # ローカルPCのsrc:コンテナのworkspace/src
    tty: true                       # コンテナを起動し続ける
```

# コンテナを起動する
docker-compose.ymlがあるディレクトリに移動し以下のコマンドを実行する．
イメージの作成，コンテナの作成，起動が一発で完了する．
```
$ docker compose up -d

[+] Running 0/1
 - python Warning                                                                      2.4s
[+] Building 1.6s (10/10) FINISHED
...
[+] Running 1/1
 - Container python-step3  Started
```

## 起動中のコンテナに接続する
まずは起動しているコンテナを確認する．
```
$ docker compose ps

NAME                IMAGE               COMMAND             SERVICE             CREATED             STATUS              PORTS
python-step3        python:step3        "/bin/bash"         python              21 seconds ago      Up 20 seconds
```

Step2までとは異なりローカルPCのターミナルがコンテナに接続していない状態で起動される．
上で表示したSERVICE名を指定してコンテナのシェルを起動する．
```
$ docker compose exec python /bin/bash

root@eeaa12bfb21a:/workspace/src#
```

## コンテナの終了
Step2までとは異なりシェルに`exit`を入力してもコンテナは動き続ける．
以下のコマンドでコンテナの終了と削除をおこなう．
```
$ docker compose down

 - Container python-step3        Removed
 - Network devcontainer_default  Removed
```

# Step4 devcontainer.jsonを使用する
Step3までの内容でDockerを使用した開発環境構築は十分だといえる．
このステップではコンテナの起動や停止をコマンドを使わずVScode上で完結させる．
ディレクトリ構成は以下の通り．
今回追加するdevcontainer.json以外はStep3と同じ．
```
step4/
  workspace/
    .devcontainer/
      Dockerfile
      requirements.txt
      docker-compose.yml
      devcontainer.json
    src/
      hello.ipynb
```

## devcontainer.jsonを作成する
- `"extensions"`の名前はVScode上で拡張機能を右クリックし`Copy Extension ID`をクリックすることでコピーできる．
- VScodeの設定はSettings画面でGUI操作で設定した内容がJSONファイルに記載されている．
設定内容が記載されたJSONファイルはコマンドパレットを開き`>open user settings(JSON)`を開くと見ることができるため，その内容を`"settings"`に書けばよい．
```json
{
    "name": "Python VScode",                    // VScodeがコンテナに接続したときのタイトルを自由に設定
    "dockerComposeFile": "docker-compose.yml",  // docker-conpose.ymlファイルを指定
    "service": "python",                        // docker-conpose.ymlファイル内に記載したサービス名を指定
    "workspaceFolder": "/workspace/src",         // VScode接続時に開くフォルダを指定
    "customizations": {
        "vscode": {
            "extensions": [                     // コンテナ作成時にインストールする拡張機能を記載　
                "ms-python.python",             // python拡張機能
                "ms-toolsai.jupyter"            // jupyter拡張機能
            ],
            "settings": {                       // VScodeの設定を記載
                "files.autoSave": "afterDelay"  // ファイルのオートセーブ
            }
        }
    }
}
```

## コンテナの起動と終了
1. workspaceディレクトリをVScodeで開く
2. VScodeの画面左下隅にある`><`のような青いアイコンをタップ
3. Reopen in Containerをクリックするとコンテナが起動しリモート接続した状態になる
4. VScodeを閉じればコンテナが終了する

# Step5 複数のコンテナを使用する
このステップではWEBサーバーとDBサーバーなど複数のコンテナを利用する方法について紹介する．
ディレクトリ構成は以下の通り．
```
step5/
  workspace/
    .devcontainer/
      db/
        Dockerfile
      web/
        Dockerfile
        requirements.txt
      devcontainer.json
      docker-compose.yml
    db/
      init/
        01_create_tables.sql
        02_insert_data.sql
    web/
      src/
        app.py
```

## Docker構築用のファイルを用意
データベース用のDockerfile
```:db/Dockerfile
# ベースイメージを指定
FROM mysql:5.7

# タイムゾーンを指定
ENV TZ=Asia/Tokyo
```

webサーバー用のDockerfile
```:web/Dockerfile
# DockerHubにあるpythonのイメージ指定する
FROM python:3.11-slim

# 作業ディレクトリをworkspaceに変更
WORKDIR /workspace/.devcontainer/web

# ローカルPCのrequirements.txtをコンテナのカレントディレクトリ（上で指定した場所）にコピー
COPY requirements.txt ${pwd}

# pipのアップデート
RUN pip install --upgrade pip

# pythonパッケージをインストール
RUN pip install -r requirements.txt
```

flaskとpythonでmysqlに接続するためのライブラリを定義
```:requirements.txt
flask
mysql-connector-python
```

コンテナに接続するVScodeの設定を記述
```:devcontainer.json
{
    "name": "flask mysql",                      // VScodeがコンテナに接続したときのタイトルを自由に設定
    "dockerComposeFile": "docker-compose.yml",  // docker-conpose.ymlファイルを指定
    "service": "web",                           // docker-conpose.ymlファイル内に記載したサービス名（webサーバー）を指定
    "workspaceFolder": "/workspace/web/src",    // VScode接続時に開くフォルダを指定
    "customizations": {
        "vscode": {
            "extensions": [                     // コンテナに接続するVScodeにインストールする拡張機能を記載　
                "ms-python.python"              // python
            ],
            "settings": {                       // VScodeの設定を記載
                "files.autoSave": "afterDelay"  // ファイルのオートセーブ
            }
        }
    }
}
```

db用とwebサーバー用の2つのコンテナを作成するよう定義
```yml:docker-compose.yml
services:                                     # サービスを定義するための決まり文句
  db:                                         # DB用のコンテナ（名前は自由に定義できる）
    image: mysql:step5                        # イメージのREPOSITORY名:TAG名
    build: ./db                               # Dockerfileの相対パス  
    container_name: mysql-step5               # コンテナ名
    expose:                                   # webサーバー側のコンテナからアクセスするために必要
      - "3306"                                # webサーバー側のコンテナに公開するポート番号
    ports:                                    # ローカルPCからもアクセスするために必要
      - "3307:3306"                           # ローカルPCのポート番号:コンテナのポート番号
    environment:
      - MYSQL_ROOT_PASSWORD=root              # rootユーザのパスワード
      - MYSQL_DATABASE=step5                  # データベース名
      - MYSQL_USER=step5                      # ユーザー名
      - MYSQL_PASSWORD=step5                  # ユーザーのパスワード
    volumes:
    - ../db/database:/var/lib/mysql           # データベースの情報を保持するために定義
    - ../db/init:/docker-entrypoint-initdb.d  # コンテナ初回作成時に実行するsqlファイルをマウントする
  web:                                        # webサーバー用のコンテナを定義
    image: python:step5                       # イメージのREPOSITORY名:TAG名を指定
    build: ./web                              # Dockerfileの相対パス
    container_name: python-step5              # コンテナ名
    depends_on:                               # サービスの依存関係を指定する
      - db                                    # webがdbに依存するよう指定
    working_dir: /workspace/web/              # 作業ディレクトリを指定
    volumes:                                  # マウントするファイルを指定する
      - ../web:/workspace/web                 # ローカルPCのwebをコンテナのworkspace/webにマウント
    tty: true                                 # コンテナを起動し続けるための定義
```

コンテナ初回作成時にテーブル作成やレコードの挿入などを行うクエリをsqlファイル内に定義する
ファイル名の順番でsqlファイルが実行されるため先頭に数字などつけるとよい
```sql:01_create_tables.sql
create table users(id int, name varchar(10), create_date datetime);
```

```sql:02_insert_records.sql
INSERT INTO users(id, name, create_date) VALUES
(1, 'sena', now()),
(2, 'taro', now());
```

flaskでWebAPIを作成するソース
```py:app.py
from flask import Flask, jsonify
import mysql.connector

app = Flask(__name__)

db = mysql.connector.connect(
    host="mysql-step5",  # hostはdocker-compose.ymlファイルで定義したDBのコンテナ名を指定
    user="step5",
    password="step5",
    database="step5"
)

@app.route('/')
def get_data():
    cursor = db.cursor()
    cursor.execute("SELECT * FROM users")
    data = cursor.fetchall()  
    cursor.close()
    return jsonify( data)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=500)
```

## コンテナの起動からAPIの動作確認まで
step4と同様の手順でVSCodeからReopen in Containerでコンテナの作成および起動をおこなう．
app.pyファイルを実行．
```sh
/usr/local/bin/python /workspace/web/src/app.py
root@ea6494b0cbad:/workspace/web/src# /usr/local/bin/python /workspace/web/src/app.py
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:500
 * Running on http://172.21.0.3:500
Press CTRL+C to quit
```

[http://127.0.0.1:500](http://127.0.0.1:500)にアクセスし以下のように表示されれば成功！
```
[[1,"sena","Mon, 17 Jul 2023 13:08:23 GMT"],[2,"taro","Mon, 17 Jul 2023 13:08:23 GMT"]]
```

## データベースの確認
データベースの中身を確認する方法として2つ紹介する．
1. mysqlコンテナのシェルに接続して確認
2. ローカルPCのターミナルから直接確認

### 1. mysqlコンテナのシェルに接続して確認
まずは起動中のコンテナを確認してみる．
```
$docker container ls

CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                               NAMES
ea6494b0cbad   python:step5   "/bin/sh -c 'echo Co…"   7 minutes ago   Up 7 minutes                                       python-step5
9244be7034ad   mysql:step5    "docker-entrypoint.s…"   7 minutes ago   Up 7 minutes   33060/tcp, 0.0.0.0:3307->3306/tcp   mysql-step5
```

mysqlコンテナのシェルに接続する．
```
$ docker exec -it mysql-step5 /bin/bash
```

あとは普段通りsqlコマンドを実行していけばよい．
```
$ mysql -u root -proot

mysql> select * from step5.users;

+------+------+---------------------+
| id   | name | create_date         |
+------+------+---------------------+
|    1 | sena | 2023-07-17 13:32:10 |
|    2 | taro | 2023-07-17 13:32:10 |
+------+------+---------------------+
2 rows in set (0.02 sec)
```

### 2. ローカルPCのターミナルから直接確認
docker-compose.yml内でローカルPCの`3307`ポートを経由して，コンテナ内のMySQLサーバーのポート`3306`にアクセスできるよう定義している．
なので以下のように`3307`ポートを指定することでコンテナのmysqlに直接アクセスできる．
```
$ mysql -h 127.0.0.1 --port 3307 -u root -proot

MySQL [(none)]> INSERT INTO step5.users(id, name, create_date) VALUES (3, 'ken', now());
Query OK, 1 row affected (0.007 sec)
```

## dbフォルダの中身について
コンテナを起動するとdbフォルダにはdatabaseフォルダが作成されているはず．
databaseフォルダにコンテナ内のDBの内容を保存しておくことで情報が失われない．
一度コンテナを停止し再度起動すると先ほどinsertしたデータが追加されているはず．
```
$ mysql -h 127.0.0.1 --port 3307 -u root -proot

MySQL [(none)]> select * from step5.users;
+------+------+---------------------+
| id   | name | create_date         |
+------+------+---------------------+
|    1 | sena | 2023-07-17 13:32:10 |
|    2 | taro | 2023-07-17 13:32:10 |
|    3 | ken  | 2023-07-17 13:45:41 |
+------+------+---------------------+
```

initフォルダ内のsqlはいくら書き換えてもdatabaseフォルダが存在すると反映されない．
反映させるにはローカルのdatabaseフォルダを一度削除してからコンテナを起動すればよい．

# Appendix
## 便利なコマンド
停止中のコンテナを一括削除
```
$ docker container prune
```
不要なイメージを一括削除
```
$ docker image prune -a
```

# Conclusion
Dockerを使うことで，ローカル環境を汚すことなく様々な言語やライブラリ，DBを使用した開発が気軽に試せる点が魅力的に感じた．
データ分析やアプリ開発などにどんどん活用していきたい．