devcontainerでフロントとバックの環境を構築してコンテナ開発アレルギーを克服する

## はじめに
Docker開発が苦手！俺は業務ロジックを作りたい！しかしこの手の開発手法は当面なくなりそうにないので、とにかく仕事に近い感じのアレンジをしながら作ってみる。アレルギーを克服しろ！記事の量は最低限に！

## 最低限のファイル構成
こんな感じ（モノRepositoryはベストプラクティスではない。現場がそうだってだけ）

```txt:ファイル構成
docker_world
├─.devcontainer
│   ├─ devcontainer.json
│   └─ Dockerfile
├─.vscode
│   └─ launch.json
├─dev
│   └─ docker-compose.yml
├─src
│   ├─ main.py
│   └─tests
│       └─ test_main.py
├─ .gitignore
├─ README.md
├─ requirements.txt
└─ setup.sh
```

## gitの設定
### 改行コードのLF統一
Git による改行コード管理が自動化され（ローカルでコミットされる際に **CRLF を LF に自動変換**します。）、**リポジトリ内の改行コードをすべて LF で統一**できるようになる。仕事でも環境面で大ハマリする筆頭だと思う

```
git config --global core.autocrlf input
```
### whoami
```
whoami
```

## ※Dockerfileを試行錯誤するときは sleep infinity を使え
こうすると $PATH に追加すべきパスがなにかわかんねーってときにコンテナの世界にい続けたまま `which uvicorn` などで調査できる
```Dockerfile:事故ってた時期のDockerfileのかけら
  :
# uvicorn を起動コマンドとして設定
# CMD ["python", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["sleep", "infinity"]
```

## ※Dockerfileにおける WORKDIR を /workspace に統一する理由
:::note warn
`/workspace` 自体はルートに作った任意のディレクトリです
:::


`Dockerfile` において `WORKDIR` を `/home/vscode` など一般ユーザの「ホームディレクトリ」に設定していた時期があった。なぜかというとさくらのVPSでログインするとホームディレクトリでスタンバイされるから。

ホームディレクトリには 意外に多くの隠しファイル が生成される。
```
.cache/
.vscode-server/
.ssh/
.gitconfig
.bash_history
```

これらのファイルは環境依存の設定や一時的なデータであり、プロジェクトのコード管理には不要である。これらのファイルを `.gitignore` に追加するのは思考の妨げになる。

そこで
- `/workspace` を `WORKDIR` に指定する [公式: WORKDIR](https://docs.docker.jp/engine/reference/builder.html?utm_source=chatgpt.com#workdir)
  - コンテナはホストOSとは独立した環境であるため、Linuxシステムに従ったディレクトリ配置をしなくてもよい
  - `/workspace` 配下にはプロジェクトに必要なファイルのみが存在するため、コンテナ内の環境をシンプルに保つことができる

以上の理由から、ホームディレクトリを直接 `WORKDIR` に設定するのは避けたほうが良い。

## 基本的な Docker-desktop の調査方法
### Containers
#### logs
コンテナのログをみることができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/1c14e203-adb6-4824-ba04-6aa6e5dc0093.png)

#### inspect
コンテナの環境変数を見ることができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ea03ccb2-c562-48fc-afee-929633e1596d.png)

#### exec
コンテナのなかでコマンドを打って調査することができる
（使えるときとアップグレードしろって言われる時がある）

#### files
コンテナのファイルをツリーで見ることができる

### Builds
ビルド中にエラーで落ちたときに途中経過を見ることができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/6ead55d7-2224-41bb-82cd-acaa06d1895d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/4d3443b7-c4f2-4597-a695-60ee47599285.png)

## backend
なんべんもやり直してこの図を書いたよ... :sob:
コンテナの `ライフタイム` を意識すると、activateが2回ある理由などの解像度が深まる

```mermaid
sequenceDiagram
    participant DevContainer as devcontainer.json
    participant Docker as Dockerfile
    participant DevContainerInstance as （devcontainer）
    participant SetupScript as setup.sh
    DevContainer->>DevContainer: build context を `..` に設定
    DevContainer->>Docker: Dockerfile をビルド
    activate Docker
    activate DevContainerInstance
    Docker->>DevContainerInstance: イメージ作成 & devcontainer 起動
    Docker->>DevContainerInstance: workdirを `/workspace` に設定
    Docker->>DevContainerInstance: 必要なfileを $WORKDIR に転送
    Docker->>DevContainerInstance: `vscode` ユーザー作成
    Docker->>DevContainerInstance: パッケージをインストール
    Docker->>DevContainerInstance: 作業ディレクトリの所有者を設定
    Docker->>DevContainerInstance: `vscode` にユーザー変更
    deactivate Docker
    deactivate DevContainerInstance
    DevContainer->>SetupScript: postCreateCommand 実行
    activate SetupScript
    SetupScript->>DevContainerInstance: 仮想環境を作成 & activate
    activate DevContainerInstance
    SetupScript->>DevContainerInstance: pip install -r requirements.txt
    deactivate DevContainerInstance
    deactivate SetupScript
    
    DevContainer->>DevContainerInstance: workspaceFolder で workdirを `/workspace` に設定
    activate DevContainerInstance
    DevContainer->>DevContainerInstance: python.defaultInterpreterPath で仮想環境を activate
    deactivate DevContainerInstance
```

### .devcontainer\devcontainer.json
:::note warn
最初にハマりがちなのが、親ディレクトリに遡ることはできませんという仕様

`Dockerfile` 内では、親ディレクトリに遡ることはできません。これはセキュリティとコンテナの隔離を保つためです。具体的には、以下のような制限があります：

#### ビルドコンテキストの制限:
`Dockerfile` は、指定されたビルドコンテキスト内でのみファイルをコピーしたり、アクセスしたりできます。ビルドコマンドを実行する際に指定したディレクトリ（例えば、`docker build -t myapp .` で `.` を指定した場合、そのディレクトリがビルドコンテキストになります）に制限されます。
:::

```json:.devcontainer\devcontainer.json
{
	"name": "FastAPI Dev Container",
	"build": {
		"dockerfile": "./Dockerfile",// TODO: のちほど 親ディレクトリの本番用 ../Dockerfile を利用 + features でセットアップする形にする
		"context": ".."  // 必要: buildの基準位置を示す
	},
	// "runArgs": ["--network=dev_network"],
	"containerEnv": {
		"SHELL": "/bin/bash"
	},
	"postCreateCommand": "/workspace/setup.sh",
	"workspaceFolder": "/workspace",  // 必要: セットアップ完了後に cd する
	"customizations": {
		"vscode": {
			"settings": {
				"python.defaultInterpreterPath": "/workspace/.venv/bin/python",  // 必要: セットアップ完了後に activate する
				"editor.defaultFormatter": "ms-python.black-formatter",
				"editor.formatOnSave":true,
				"editor.formatOnPaste": true
			},
			"extensions": [
				"ms-python.black-formatter",
				"ms-python.flake8",
				"humao.rest-client"
			]
		}
	}
}
```

### .devcontainer\Dockerfile
わからないと大ハマリするのが linux のファイル構造。`workspaces` のなかには `docker_world` というフォルダがあって、どうやらここが host - コンテナ間 の窓口らしい
```
(.venv) vscode@01d3e9f0e986:/workspace$ ls -la /
total 100
drwxr-xr-x   1 root   root   4096 Mar  5 21:08 .
drwxr-xr-x   1 root   root   4096 Mar  5 21:08 ..
-rwxr-xr-x   1 root   root      0 Mar  5 21:08 .dockerenv
drwxr-xr-x   2 root   root   4096 Feb 24 09:00 bin
drwxr-xr-x   2 root   root   4096 Aug 15  2024 boot
  :
drwxr-xr-x   3 root   root   4096 Mar  5 21:08 vscode
drwxr-xr-x   1 vscode vscode 4096 Mar  5 21:08 workspace　←つくったディレクトリ
drwxr-xr-x   3 root   root   4096 Mar  5 21:08 workspaces
```
```Dockerfile:.devcontainer\Dockerfile
FROM python:3.13.3-slim-bullseye

# 共通変数
ARG WORKDIR=/workspace
ARG USER_NAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

ENV TZ=Asia/Tokyo

# `WORKDIR` で指定したカレントディレクトリは、ビルドセッション中のみ適用されます。
WORKDIR $WORKDIR

# 開発用なので $WORKDIR にすべてコピーする（本番では必要なものだけに絞ること！）
COPY . $WORKDIR

# ユーザー作成
RUN groupadd --gid $USER_GID $USER_NAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USER_NAME

# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y \
    bash curl git openssh-client zip

# 作業ディレクトリの所有者を設定
RUN chown -R $USER_NAME:$USER_NAME $WORKDIR

# ユーザー変更
USER $USER_NAME

# TODO: のちに本番用Dockerfileに移すと良い
#CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
```

### .vscode\launch.json
`F5` でサーバー起動と `pytest` が打てるようになる
```json:.vscode\launch.json
{
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Run Serve",
        "type": "debugpy",
        "request": "launch",
        "module": "uvicorn",
        "args": [
          "src.main:app",
          "--reload",
          "--port", "8000",
          "--host", "0.0.0.0",
        ],
      },
      {
        "name": "Run Tests",
        "type": "debugpy",
        "request": "launch",
        "module": "pytest",
        "args": ["-vv"]
      }
    ]
  }

```

### dev\docker-compose.yml
（これは今回使っていない。バックエンドとフロントエンドを連動させるようなときに使う）
```yml:dev\docker-compose.yml
services:
  fastapi:
    build:
      context: .
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - .:/workspace
    ports:
      - "8000:8000"

networks:
  default:
    name: dev_network
```

### src\tests\test_main.py
```py:src\tests\test_main.py
from src.main import reverse


def test_reverse():
    assert reverse("hello") == "olleh"
    assert reverse("") == ""
    assert reverse("123") == "321"
    assert reverse("あいう") == "ういあ"
```

### src\main.py
```py:src\main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}


def reverse(text: str) -> str:
    """
    与えられた文字列を逆順にして返す。

    この関数はpytestのテスト用に作成されています。

    Args:
        text (str): 逆順にする文字列。

    Returns:
        str: 逆順になった文字列。
    """
    return text[::-1]
```

### .gitignore
```.gitignore:.gitignore
__pycache__/
.pytest_cache/
*.log
*.swp

.env
.venv/
```

### requirements.txt
```txt:requirements.txt
fastapi==0.115.8
uvicorn==0.34.0
pytest==8.3.4
```

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

# 共通変数
BASE_DIR="/workspace"
VENV_DIR="$BASE_DIR/.venv"

# 仮想環境の作成
if [ ! -d "$VENV_DIR" ]; then
    echo "仮想環境を作成中..."
    python -m venv $VENV_DIR
else
    echo "仮想環境は既に存在します。"
fi

# 仮想環境をアクティベート（現在のシェルセッション内でのみ有効）
source $VENV_DIR/bin/activate

# 仮想環境の確認
echo "現在のPython環境: $(which python)"

# 仮想環境が有効か確認
if [ "$VIRTUAL_ENV" != "" ]; then
    echo "仮想環境が有効です(\$VIRTUAL_ENV): $VIRTUAL_ENV"
else
    echo "仮想環境が有効ではありません。"
    exit 1
fi

# requirements.txt の内容をインストール
if [ -f "$BASE_DIR/requirements.txt" ]; then
    echo "requirements.txt からパッケージをインストール中..."
    pip install --upgrade pip
    pip install --no-cache-dir -r $BASE_DIR/requirements.txt
else
    echo "requirements.txt が見つかりませんでした。"
fi

echo "仮想環境のセットアップが完了しました。"
```

### README.md
```md:README.md
# docker_world
## 処理の流れ
以下は、`devcontainer.json` を基点に Docker コンテナを利用して Python 開発環境をセットアップするプロセスを示しています。
### 1. **`devcontainer.json` の読み込み**
- Visual Studio Code の Dev Container 定義ファイル。
- 定義された設定に基づき、コンテナのビルド・起動、およびセットアップを行います。

### 2. **Dockerfile のビルド**
- `devcontainer.json` の `"build"` セクションに従い、`Dockerfile` を使用して Docker イメージをビルドします。
- 必要なスクリプト（例: `setup.sh` など）がビルド時にコンテナ内にコピーされます。

### 3. **Docker コンテナの起動**
- ビルドした Docker イメージからコンテナを作成します。
- この段階で必要な環境変数設定（例: `containerEnv`）、作業ディレクトリ設定（例: `workspaceFolder`）が反映されます。

### 4. **作業ディレクトリの設定**
- コンテナ内の作業ディレクトリを `/workspace` に設定します。
- これは `devcontainer.json` の `"workspaceFolder": "/workspace"` によって制御されています。

### 5. **`setup.sh` の実行**
- `devcontainer.json` の `"postCreateCommand"` オプションを使用して、起動されたコンテナ内で `setup.sh` を実行します。
- スクリプト内で行われる主な処理:
    - 仮想環境 (`.venv`) の作成とシェル内での一時的な有効化。
    - `requirements.txt` に記載された Python パッケージのインストール。

### 6. **仮想環境の再度有効化**
- 仮想環境は `setup.sh` 実行時に一時的に有効化されますが、シェルセッション終了後は無効になります。
- そのため、`devcontainer.json` の以下の設定により、VS Code 内で仮想環境を再度有効化します:

  "python.defaultInterpreterPath": "/workspace/.venv/bin/python"

- 開発環境において、この設定により `.venv` が自動的に Python インタープリタとして認識され、デフォルトで使用可能になります。

### 7. **VS Code のカスタマイズ設定の適用**
- 以下の拡張機能とエディタ設定が自動的に適用されます:
    - **フォーマッタ**: `ms-python.black-formatter`
    - **静的解析ツール**: `ms-python.flake8`
    - **API テストツール**: `humao.rest-client`

- 開発効率を高めるために、以下の VS Code 設定も反映されます:
    - **コード整形**: `"editor.formatOnSave": true, "editor.formatOnPaste": true`

## シーケンス図
以下に、上記プロセスをマーメイド記法でシーケンス図として示します。

sequenceDiagram
    participant DevContainer as devcontainer.json
    participant Docker as Dockerfile
    participant DevContainerInstance as （devcontainer）
    participant SetupScript as setup.sh

    DevContainer->>DevContainer: build context を `..` に設定
    DevContainer->>Docker: Dockerfile をビルド
    activate Docker
    activate DevContainerInstance
    Docker->>DevContainerInstance: イメージ作成 & devcontainer 起動
    Docker->>DevContainerInstance: workdirを `/workspace` に設定
    Docker->>DevContainerInstance: 必要なfileを $WORKDIR に転送
    Docker->>DevContainerInstance: `vscode` ユーザー作成
    Docker->>DevContainerInstance: パッケージをインストール
    Docker->>DevContainerInstance: 作業ディレクトリの所有者を設定
    Docker->>DevContainerInstance: `vscode` にユーザー変更
    deactivate Docker
    deactivate DevContainerInstance

    DevContainer->>SetupScript: postCreateCommand 実行
    activate SetupScript
    SetupScript->>DevContainerInstance: 仮想環境を作成 & activate
    activate DevContainerInstance
    SetupScript->>DevContainerInstance: pip install -r requirements.txt
    deactivate DevContainerInstance
    deactivate SetupScript
    
    DevContainer->>DevContainerInstance: workspaceFolder で workdirを `/workspace` に設定
    activate DevContainerInstance
    DevContainer->>DevContainerInstance: python.defaultInterpreterPath で仮想環境を activate
    deactivate DevContainerInstance

## 設定の重要ポイント
1. **仮想環境のセットアップ**:
    - `devcontainer.json` の `"postCreateCommand": "/workspace/setup.sh"` により仮想環境が作成され、必要な Python パッケージがインストールされます。

2. **仮想環境の自動有効化**:
    - `"python.defaultInterpreterPath": "/workspace/.venv/bin/python"` によって、仮想環境がデフォルトの Python インタープリタとして設定されます。

3. **カスタマイズ設定**:
    - `devcontainer.json` に定義された拡張機能やエディタ設定を利用することで、開発作業を即座に始められる環境が用意されます。

```

## 確認
### devcontainerを起動
左下のへんなマークを押して `Reopen in Conteiner`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/b0048ac3-a074-4ad9-8bd2-7ea5dd88a7b1.png)

### venv起動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/14240300-2039-496e-b022-b2461f896612.png)

### uvicorn起動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/5828c29a-c919-48b0-83ff-ca49d7a891b1.png)

### pytest起動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/6dd85947-5f5c-451d-90cb-e6514acc81be.png)