Docker コンテナ内の Django デバッグ解説

Django アプリケーションをデバッグするには、通常、デバッグサーバーがアプリケーションコードの実行を監視し、開発者のエディタと通信する必要があります。Docker コンテナ内で実行されるアプリケーションの場合、この通信経路を確立することが課題となります。

デバッグが成功する主な要素は以下の通りです：

まず、`debugpy` というPythonデバッグライブラリがアプリケーションの起動プロセスの最初期に初期化されています。これにより、Djangoフレームワークが読み込まれる前からデバッグ機能が有効になり、アプリケーション全体の実行フローを監視できるようになります。

```python 
# manage.py の先頭での初期化
import debugpy

# デバッグモードの制御
if not debugpy.is_client_connected():
    try:
        debugpy.listen(("0.0.0.0", 5678))
        print("✅ Debugger is listening on port 5678")
    except RuntimeError:
        # 既に listen が呼び出されている場合は無視
        print("⚠️ Debugger is already listening")
```

次に、Docker環境では、コンテナ内のプロセスとホストマシン上のエディタ間の通信経路が必要です。Docker Composeファイルで適切にポートフォワーディングを設定することで、コンテナ内のデバッグサーバーとホスト上のエディタが通信できるようになります。

さらに、ファイルシステムのマッピングも重要な要素です。コンテナ内のファイルパスとホスト上のファイルパスは異なりますが、`launch.json` の `pathMappings` 設定により、エディタはブレークポイントを正確に対応付けることができます。

## Docker Compose と ボリュームマウントの詳細解説
### Docker Compose の役割と仕組み
Docker Compose は、複数のコンテナを定義し、連携させるためのツールです。`docker-compose.yml` ファイルには、アプリケーションを構成するサービス（コンテナ）の設定が記述されています。

一般的な Django プロジェクトの `docker-compose.yml` ファイルは以下のようになります：

```yaml 
version: '3.8'  # Docker Compose のバージョン指定

services:       # サービス（コンテナ）の定義開始
  web:          # サービス名（任意の名前）
    build: .    # Dockerfile がカレントディレクトリにあることを指定
    tty: true   # 擬似TTYを割り当て（コンテナがバックグラウンドで実行されても終了しないようにする）
    volumes:    # ボリュームマウントの設定
      - .:/code  # ホスト側のカレントディレクトリをコンテナ内の /code にマウント
    ports:      # ポートマッピングの設定
      - "8000:8000"  # ホストの8000ポートをコンテナの8000ポートに接続
      - "5678:5678"  # デバッグ用のポート
    expose:     # コンテナ間通信用にポートを公開
      - 8000
      - 5678
```

この設定により、以下のことが実現されています：

1. **イメージのビルド:**
     `build: .` の部分で、カレントディレクトリにある Dockerfile を使ってイメージをビルドするよう指定しています。これにより、アプリケーションの実行環境が構築されます。
    <br>
    
2. **コンテナの作成と実行:** 
    `web` サービスの定義全体が、このサービス用のコンテナを作成・実行する設定です。`tty: true` の設定により、コンテナがバックグラウンドでも終了せず、ログ出力が可能になります。
    <br>
    
3. **ファイルシステムの共有:** 
    `volumes: - .:/code` の部分で、ホスト側のカレントディレクトリ（`.`）をコンテナ内の `/code` ディレクトリにマウントしています。これにより、ホスト上でファイルを編集すると、即座にコンテナ内に反映されます。
    <br>

4. **ネットワークポートの共有:** 
    `ports` セクション:
    `ports` セクションではホストマシンからコンテナへのポート接続を設定します。外部（ホストマシンやインターネット）からコンテナにアクセスするために必要です。
    
    - `- "8000:8000"`: 
        ホストの8000ポートへのアクセスがコンテナの8000ポートに転送されます。これにより、ブラウザで `http://localhost:8000` にアクセスすると、コンテナ内で実行されているDjangoアプリケーションにアクセスできます。
    - `- "5678:5678"`: 
        デバッグ用のポートも同様に接続されます。ホスト側のデバッグクライアント（VSCode/Cursor）からコンテナ内のデバッグサーバー（debugpy）に接続するために使用されます。

    `expose`セクション：
    コンテナ間の通信用にポートを公開します。同じDocker Compose内の他のサービス（コンテナ）からアクセスできますが、ホストマシンからは直接アクセスできません。
    - `- 8000`: 
        Django アプリケーションのポートを他のコンテナ（例：Nginx や Redis など）が直接アクセスできるようにします。
    - `- 5678`:
    デバッグポートも同様に Docker ネットワーク内で公開されます。これにより、例えば別のデバッグツールコンテナがこのポートに接続できます。
    
この例では両方を設定していますが、実際には `ports` だけで十分な場合も多いです。`expose` は複数のコンテナ（例：Webサーバーとデータベース）間の通信を設定する場合に特に役立ちます。

### ボリュームマウントの詳細
Docker Compose ファイルの中で特に重要なのが volumes セクションです：

```yaml 
volumes:
  - .:/code
```

この一行は非常に重要な役割を果たしています：

- `.` はホスト側のパスで、Docker Compose を実行しているディレクトリ（プロジェクトのルートディレクトリ）を指します
- `:/code` はコンテナ内のパスで、ホスト側のディレクトリをマウントする場所を指定しています
- コロン (`:`) はホスト側とコンテナ側のパスを区切る記号です

このマウント設定により、以下のような対応関係が生まれます：
```terminal
ホスト側                        コンテナ側
/path/to/project/              →  /code/
/path/to/project/manage.py     →  /code/manage.py
/path/to/project/myapp/        →  /code/myapp/
```

この対応関係によって、以下のメリットが得られます：

1. **開発の即時反映**: 
ホスト側でファイルを編集すると、その変更がリアルタイムでコンテナ内に反映されます。これにより、コードを変更するたびにコンテナを再ビルドする必要がなくなります。

2. **永続性**: 
コンテナを停止または削除しても、データはホスト側に残ります。次回コンテナを起動したときも同じファイルが利用できます。

3. **デバッグの一貫性**: 
エディタでブレークポイントを設定したファイルと、コンテナ内で実行されるファイルが同一になるため、デバッグが正確に機能します。

### パスマッピングとボリュームマウントの関係
VSCode/Cursor の launch.json で設定するパスマッピング（**`pathMappings`**）は、ボリュームマウントと密接に関連しています：

```json
  "pathMappings": [
    {
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/code"
    }
  ]
```

<details><summary>launch.jsonの全体</summary>

```json 
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker Django Debug",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}",
          "remoteRoot": "/code"
        }
      ],
      "django": true,
      "justMyCode": true
    }
  ]
}
```
</details>

この設定は、デバッグ時にエディタとデバッガー間でファイルパスを変換するためのものです：

- `localRoot:` エディタ上でのプロジェクトのルートパス（`${workspaceFolder}` は VSCode/Cursor の変数で、開いているフォルダを指します）
- `remoteRoot:` コンテナ内でのアプリケーションのルートパス

例えば、ホスト側の `/path/to/project` が `${workspaceFolder}` に対応し、コンテナ内の `/code` がアプリケーションのルートディレクトリになっています。

重要なのは、Docker Compose のボリュームマウント（`.:/code`）とデバッグのパスマッピング（`${workspaceFolder}` → `/code`）が一致していることです。これにより、エディタでブレークポイントを設定した場所が、コンテナ内の正しい場所に変換されます。

## ディレクトリ構造とマウントの具体例
一般的な Django プロジェクトの構造は以下のようになります：
```
project-root/                  # プロジェクトルート
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── manage.py                  # Django の管理スクリプト
├── project/                   # Django プロジェクト設定
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── app1/                      # Django アプリケーション
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py
```
Docker Compose のボリュームマウント（`.:/code`）により、プロジェクトルートディレクトリ全体がコンテナ内の `/code` にマウントされます。そのため、コンテナ内のディレクトリ構造は以下のようになります：
```
/code/                          # コンテナ内のマウントポイント
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── manage.py
├── project/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── app1/
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py
```
デバッグ時には、`launch.json` のパスマッピングにより、エディタ上の `${workspaceFolder}` がコンテナ内の `/code` に対応付けられます。

### より複雑なプロジェクト構造の場合
より複雑なプロジェクト構造では、Django アプリケーションがサブディレクトリに配置されることがあります：
```
project-root/                  # プロジェクトルート
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── django_app/                # Django アプリケーションディレクトリ
    ├── manage.py
    ├── project/
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── app1/
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── models.py
        ├── tests.py
        └── views.py
```
この場合、Docker Compose のボリュームマウントは同じですが、パスマッピングを調整する必要があります：

```json 
"pathMappings": [{
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/code"
}]
```
そして、コンテナ内で Django を実行する際には、正しいディレクトリに移動する必要があります：

```bash 
cd /code/django_app
python manage.py runserver 0.0.0.0:8000
```

## Docker コンテナ内でのコマンド実行とパス
Docker コンテナ内でコマンドを実行する際、作業ディレクトリとパスの関係も重要です。一般的な Dockerfile では：

```dockerfile 
# 作業ディレクトリの作成・設定
WORKDIR /code

# 依存関係のインストール
COPY requirements.txt .
RUN pip install -r requirements.txt

# アプリケーションのコピー
COPY . .
```

この設定により、コンテナ内のデフォルトの作業ディレクトリが `/code` に設定されています。そのため、`docker-compose exec web bash` でコンテナに接続すると、最初に `/code` ディレクトリにいることになります。

<!--

-->

## デバッグの設定と実行
### デバッグ用の manage.py の修正
Django アプリケーションをデバッグするには、manage.py ファイルの先頭にデバッグコードを追加します：

```python 
#!/usr/bin/env python
import os
import sys
import debugpy

# デバッグサーバーの初期化と制御
if not debugpy.is_client_connected():
    try:
        debugpy.listen(("0.0.0.0", 5678))
        print("✅ Debugger is listening on port 5678")
    except RuntimeError:
        # 既に listen が呼び出されている場合は無視
        print("⚠️ Debugger is already listening")

def main():
    # Django の標準的な初期化コード
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

if __name__ == '__main__':
    main()
```

### VSCode/Cursor のデバッグ設定
Docker コンテナ内の Django アプリケーションをデバッグする方法には、主に2つのアプローチがあります。
- ホストからのリモートデバッグ
- コンテナにアタッチしたウィンドウでのデバッグ

それぞれの手順について見ていきます。

#### ホストからのリモートデバッグ
エディタでホストの環境からリモートのデバッグサーバーにアクセスしてデバッグを行います。
`.vscode/launch.json` ファイルを作成して、デバッグ設定を追加します：
```json 
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker Django Debug",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}",
          "remoteRoot": "/code"
        }
      ],
      "django": true,
      "justMyCode": true
    }
  ]
}
```
    
**デバッグの実行手順**
1. Docker コンテナを起動します：

    ```bash 
    docker-compose up -d
    ```
    dockerがデスクトップアプリなどで起動できる場合は，そちらからでも問題ありません。
    <br>
    
2. デバッグモードで Django を実行します：

    ```bash 
    docker-compose exec web bash -c "DEBUG=True python manage.py runserver 0.0.0.0:8000"
    ```
    `web`というのはdocker-compose.yml内の `web # サービス名（任意の名前）` の部分を指定します。
    <br>
3. VSCode/Cursor でデバッグセッションを開始します：
    - デバッグパネルを開く
    - 「Docker Django Debug」設定を選択
    - 「デバッグの開始」ボタンをクリック
    <br>
4. ブレークポイントを設定して、アプリケーションにアクセスします。

#### コンテナにアタッチしたウィンドウでのデバッグ
dockerコンテナをアタッチしたウィンドウからデバッグを行います。
```json 
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Docker Django Debug",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 5678
      },
      "pathMappings": [
        {
          "localRoot": "${workspaceFolder}",
          "remoteRoot": "${workspaceFolder}"
        }
      ],
      "django": true,
      "justMyCode": true
    }
  ]
}
```
**デバッグの実行手順**
1. Docker コンテナを起動
    
    ```bash 
    docker-compose up -d
    ```
    ホストでのデバッグと同じです。
    <br>
2. エディタのリモートウィンドウ機能でコンテナにアタッチ

    - VSCode/Cursor の「リモートウィンドウ」または「コンテナにアタッチ」機能を使用
    <br>
3. アタッチしたウィンドウでプロジェクトを開く
    「フォルダを開く」から適切なディレクトリ（例：/app）を選択
    <br>
4. デバッグモードで Django を実行
    アタッチしたウィンドウのターミナル上で以下のコマンドを実行します。
    ```bash 
    DEBUG=True python manage.py runserver 0.0.0.0:8000
    ```
    <br>
5. アタッチしたウィンドウでデバッグセッションを開始

    - デバッグパネルを開く
    - 「Docker Django Debug」設定を選択
    - 「デバッグの開始」ボタンをクリック
    <br>
6. アタッチしたウィンドウでブレークポイントを設定して、アプリケーションにアクセスします。