docker-compose.ymlのbuild設定はとりあえずcontextもdockerfileも埋めとけって話


docker-composeを使った開発では以下の2つのディレクトリ構成になっていることが多いです。

サービスディレクトリ直下にDockerfile
.
├ app
│ ├ Dockerfile
│ └ ...その他ファイル群
├ api
│ ├ Dockerfile
│ └ ...その他ファイル群
├ nginx
│ ├ Dockerfile
│ └ ...その他ファイル群
└ docker-compose.yml
dockerディレクトリ下にサービス毎にDockerfile
.
├ app/
├ api/
├ nginx/
├ docker
│ ├ app
│ │ └ Dockerfile
│ ├ api
│ │ └ Dockerfile
│ └ nginx
│   └ Dockerfile
└ docker-compose.yml  

Dockerの コンテキスト という概念を知っていないと、ディレクトリ構成が違うだけで何度もコンテキスト周りのエラーで悩まされることがあります(1敗)。
なので自分的結論を出してみました。

TL;DR

docker-compose を使った開発では

docker-compose.yml
version: "3"
services:
  nginx:
    build: ./docker/nginx/

のようにDockerfileがあるディレクトリを指定するだけでなく

docker-compose.yml
services:
  nginx:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile

のようにコンテキストをルートディレクトリに指定して、Dockerfileの場所も直接指定しておけばおk

Dockerの「コンテキスト」とは?

docker build コマンドを実行したときの、カレントなワーキングディレクトリのことを ビルドコンテキスト(build context)と呼びます。 デフォルトで Dockerfile は、カレントなワーキングディレクトリにあるものとみなされます。 ただしファイルフラグ(-f)を使って別のディレクトリとすることもできます。 Dockerfile が実際にどこにあったとしても、カレントディレクトリ配下にあるファイルやディレクトリの内容がすべて、ビルドコンテキストとして Docker デーモンに送られることになります。

Dockerfile 記述のベストプラクティス

これをさらに要約すれば 「docker buildコマンドを実行した場所」ってことですね。

docker build コマンドを実行した場所ってことなので、docker buildコマンドはDockerfileがあるディレクトリで実行すれば問題なさそうですね。

しかし、docker-compose コマンドを使って開発している場合はどうでしょうか?
Dockerfileがあるディレクトリでコマンドを実行することってほとんど無いと思います。その場合はコンテキストについてどう考えればいいのでしょうか?

サービス直下にDockerfileを置く場合

例として「Laravel, Nginx」というよくあるプロジェクトの構成で考えてみます。
ディレクトリ構成は以下の様になります。

.
├ api
│ ├ Dockerfile
│ ├ app
│ ├ bootstrap
│ └ ...その他のLaravelファイル
├ nginx
│ ├ Dockerfile
│ └ default.conf
└ docker-compose.yml

このディレクトリ構成の場合、api/Dockerfilenginx/Dockerfileのコンテキストはそれぞれどこになるか分かりますか?
docker buildコマンドを実行するapi/nginx/ディレクトリ?

nginx/ディレクトリがコンテキストだとするとnginx/Dockerfileは以下のようになります。

nginx/Dockerfile
FROM nginx:1.18-alpine
ADD ./default.conf /etc/nginx/conf.d/default.conf

RUN mkdir -p /var/www/public
ADD ../api/public /var/www/public

以下の1文に注目してください。コンテキストが nginx/なのに、 ../api/public で分かる通り、コンテキストのディレクトリから外れたファイルを参照していますね。

nginx/Dockerfile
ADD ../api/public /var/www/public

このまま実行すると

ERROR: Service 'php' failed to build: COPY failed: stat /var/lib/docker/tmp/docker-builder115741816/api: no such file or directory

のようなエラーが出ます。

Dockerはコンテキスト(カレントディレクトリ)の外のファイルにはアクセスできない仕様なのです。そこら辺に関しては以下の記事で詳しく説明されています。

ではどうやってnginx/のコンテキストからapi/のファイルにアクセスすればいいのでしょうか?
答えは簡単です。

コンテキストをルートディレクトリにすればいいのです

docker-compose.ymlで「コンテキスト」と「Dockerfileのある場所」を直接指定してみましょう。

docker-compose.ym;
version: "3"
services:
  nginx:
    build:
      context: .
      dockerfile: ./nginx/Dockerfile
    ports:
      - 8080:80
    depends_on:
      - php
  php:
    build:
      context: .
      dockerfile: ./api/Dockerfile
    ports:
      - 9000:9000

コンテキストはどちらのサービスも

build:
      context: .

docker-compose.ymlがあるルートディレクトリに設定。

Dockerfileの場所は

dockerfile: ./nginx/Dockerfile
dockerfile: ./api/Dockerfile

でそれぞれ指定。

dockerディレクトリにDockerfileをまとめた場合

では次にdocker というディレクトリを作って、その中に各サービスのDockerfileをまとめた構成を考えてみます。

.
├ api
│ └ Laravelのファイル群
├ nginx
│ └ default.conf
├ docker
│ ├ php
│ │ └ Dockerfile
│ └ nginx
│   └ Dockerfile
└ docker-compose.yml

先ほどと同様にコンテキストを docker/phpdocker/nginxと考えた場合、どうやってもうまくいきません。
このディレクトリ構成の場合は、そもそもDockerfileからコンテキスト外のサービスのファイル群が入っている api/nginx/に一切アクセスできません。

ここでも同様docker-compose.ymlでコンテキストをルートディレクトリに、Dockerfileの位置も直接指定する必要がありそうです。

docker-commpose.yml
version: "3"
services:
  nginx:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    ports:
      - 8080:80
    depends_on:
      - php
  php:
    build:
      context: .
      dockerfile: ./docker/api/Dockerfile
    ports:
      - 9000:9000

注意点

ルートディレクトリをDockerのコンテキストにすることで、Dockerfileはどんなファイルにもアクセスできるようになりました。
一方で、build時はその分Dockerデーモンという奴にそれだけ多くのファイルを送ることになるので遅くなることがあるようです。