【Docker x Django】docker composeのVolumesとDockerFileのCOPYのマウントが競合した場合

# 概要
Dockerを使ってDjangoアプリケーションのコンテナを起動していたところ、`docker-compose.yaml`の`volumes`によるマウントと、`DockerFile`の`COPY`コマンドが競合していたことがわかりましたので解説します。

# ディレクトリ構造

```
.
├── myprojects
│   ├── myproject
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── constant.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── db.sqlite3
│   ├── hello.py
│   ├── manage.py
├── docker-compose.yaml
├── Dockerfile
├── good_morining.py
└── requirements.txt
```

# 修正前のコード

`DockerFile`は以下の通り。

```DockerFile
# ベースイメージの指定
FROM python:3.8

# 作業ディレクトリの設定
WORKDIR /code

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

COPY good_morining.py ./good_morining.py

COPY . /code/
```

`docker-compose.yaml`の内容は以下の通り。

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

services:

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: myproject_db
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d myproject_db"] 
      interval: 5s
      timeout: 5s
      retries: 5

  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./myprojects:/code
    ports:
      - "8000:8000"
    depends_on:
      - db
    depends_on:
      db:
        condition: service_healthy
```

`healthcheck`を入れているのは、DjangoのWebコンテナがPostgresのDBコンテナより先に起動しないようにするため。以下の記事でまとめたので気になる方はご参考までに。
[【Django x Docker】エラー「Is the server running on that host and accepting TCP/IP connections?」の原因](https://qiita.com/Ryo-0131/items/25d75106b2f5e082be37)



この状態でwebコンテナの中に入って`code`以下を見てみます。
`hello.py`はありますが、`good_morining.py`がないことがわかります。つまり、`myprojects`ディレクトリ以下のファイルがマウントされています。

```
# ls -a
.  ..  db.sqlite3  hello.py  manage.py  myproject
```

## 動作順を確認
何が起きたか？を時系列にしてみます。

①`DockerFile`：`WORKDIR /code`によって、コンテナ内での作業ディレクトリを`/code/`ディレクトリに設定する
②`DockerFile`：`COPY good_morining.py ./good_morining.py`によってホストのカレントディレクトリからコンテナ内の`/code/`ディレクトリに`good_morining.py`がコピーされる
③`docker-compose.yaml`：`volumes:`セクションによってホストの`./myprojects`をコンテナ内の`/code`にマウントする

大事な点は、`volumes:`のマウント（③）は、`DockerFile`の上記`COPY`（②）よりも後に行われる、という点です。つまり上書きされる可能性がある、ということなのです。これは以下の記事を見て理解しました。

[DockerfileのCOPYとDocker-Composeのマウントが競合した話
](https://www.northtorch.co.jp/archives/1792)

# 修正後のコード

上記記事と同じ解決方法になりますが、以下のように`DockerFile`での`COPY`先を別の場所にしてあげます。

```DockerFile
# ベースイメージの指定
FROM python:3.8

# 作業ディレクトリの設定
WORKDIR /code

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

COPY good_morining.py ./good_morining.py

COPY . /var/code/　## ここを修正!
```


同じようにコンテナ内をみます。ここはマウントされたことで先ほどと同じ状態です。

```
/code# ls -a
.  ..  db.sqlite3  hello.py  manage.py  myproject
```

次に、`COPY`先に指定した`/var/code`を見てます。

```
/var/code# ls -a
.  ..  Dockerfile  docker-compose.yaml  good_morining.py  myprojects  requirements.txt
```

すると、`good_morining.py`がありますね。
`docker-compose.yaml`と同階層のファイルたちがいることがわかります。

`volumes`のマウントによって上書きされる場所から別のディレクトリに`COPY`先を変えたことで、競合によって上書きされることがなくなったのです。

## `docker-compose.yaml`と同階層をマウントしたい場合
マウントの順番をわかりやすく説明するために上記のようにしましたが、単純に`docker-compose.yaml`があるディレクトリをマウントしたい場合は以下のようにすればOkです。

`DockerFile`は以下の通り。

```DockerFile
# ベースイメージの指定
FROM python:3.8

# 作業ディレクトリの設定
WORKDIR /code

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

COPY good_morining.py ./good_morining.py

COPY . /code/
```

`docker-compose.yaml`で`volumes:`セクションを`.:/code`のように書き換えます。これだけとDjangoの`runserver`コマンドのところで
```
python: can't open file 'manage.py': [Errno 2] No such file or directory
```
というエラーになります。なので、`command: python myprojects/manage.py runserver 0.0.0.0:8000`といった形で`manage.py`までの場所を指定してあげる形にすれば問題なくコンテナが立ち上がります。

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

services:

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: myproject_db
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d myproject_db"] 
      interval: 5s
      timeout: 5s
      retries: 5

  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: python myprojects/manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - db
    depends_on:
      db:
        condition: service_healthy
```

あとは`DockerFile`の場所とビルドコンテキストをうまく使えば、色々やりようはありますね。