0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dockerfileとcompose.ymlに何を書いているのか理解したい

0
Posted at

はじめに

既存プロジェクトのDockerfilecompose.ymlをコピーして動かしてきました。動いているので困りませんが、どの行が何のためにあるのかは説明できませんでした。

今回は小さな題材を1つ用意して、どちらに何を書くのか、両方に書けるものはどちらが効くのかを上から順に確かめました。

題材はPHPとPostgreSQLの2コンテナ

PHPの組み込みサーバーとPostgreSQLを1つずつ立てるだけです。

Dockerfile
FROM php:8.4-cli

RUN apt-get update \
 && apt-get install -y --no-install-recommends libpq-dev \
 && docker-php-ext-install pdo_pgsql \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /work

ENV APP_ENV=local
ENV ONLY_IN_DOCKERFILE=yes

EXPOSE 8080

CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]
compose.yml
services:
  php:
    build:
      context: .
    working_dir: /work
    volumes:
      - .:/work
    ports:
      - "8080:8080"
    environment:
      APP_ENV: docker
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: handson
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d handson"]
      interval: 2s
      timeout: 3s
      retries: 30

volumes:
  pgdata:

配信する中身は、環境変数とDBのバージョンを出すだけにしました。

public/index.php
<?php
header('Content-Type: text/plain; charset=utf-8');

echo 'APP_ENV=', getenv('APP_ENV'), PHP_EOL;

$pdo = new PDO('pgsql:host=db;dbname=handson', 'postgres', 'postgres');
echo $pdo->query('select version()')->fetchColumn(), PHP_EOL;

まず動くところまで確かめます。docker compose up -d --buildのあとの結果です。

$ curl -s http://localhost:8080/
APP_ENV=docker
PostgreSQL 16.15 (Debian 16.15-1.pgdg13+2) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit

境界線はビルド時と起動時

どちらもコンテナの設定に見えるので、起動のたびに両方が読み直されると認識していました。Dockerfileが読まれるのはビルドのときで、compose.ymlは起動のたびに読まれます。

Dockerfile compose.yml
読まれるとき ビルドのとき docker compose upのたび
結果 イメージに残る 残らない
決めるもの イメージの中身 起動のしかた

そもそもDockerfileが作るのはイメージで、コンテナではありません。build:は「このディレクトリのDockerfileからイメージを作れ」という指示で、ここがcompose.ymlとDockerfileの接点です。

compose.yml
  services:
    php:
      build:
        context: .        ──▶ ./Dockerfile ──▶ イメージ ──▶ php コンテナ

    db:
      image: postgres:16  ─────────────────▶ イメージ ──▶ db コンテナ

イメージまでの行き方が2通りあります。build:はDockerfileから自分で作り、image:は既製のものを取ってくる。どちらもその先はコンテナになります。題材が2コンテナなのはservicesの下に名前が2つあるからで、Dockerfileを通っているのはphpだけでした。

では、そのビルドはいつ走るのか。docker compose upはイメージが無いときだけビルドします。--buildを付けると、あっても作り直しにいきます。

$ docker compose up -d          # イメージがある状態、--build なし
 Network dockerfile-compose-lab_default Creating 
 Network dockerfile-compose-lab_default Created 
 Container dockerfile-compose-lab-db-1 Creating 
 Container dockerfile-compose-lab-db-1 Created 

$ docker compose up -d --build  # Dockerfile は変えていない
 Image dockerfile-compose-lab-php Building 
#6 [2/3] RUN apt-get update  && apt-get install -y --no-install-recommends libpq-dev  && docker-php-ext-install pdo_pgsql  && rm -rf /var/lib/apt/lists/*
#6 CACHED
#7 [3/3] WORKDIR /work
#7 CACHED

上はCreatingしか出ず、ビルドに入っていません。下は--buildがあるので毎回入りますが、Dockerfileが変わっていないので全部CACHEDで、apt-getは実行されていません。手順を最初から辿り直すのは、Dockerfileのその行から下を書き換えたときだけでした。

$ docker compose ps --format 'table {{.Service}}	{{.Image}}	{{.State}}'
SERVICE   IMAGE                        STATE
db        postgres:16                  running
php       dockerfile-compose-lab-php   running

--formatは出す列を選ぶオプションです。IMAGEの列が、そのコンテナがどのイメージから起動したかを指しています。イメージ自体は動きません。buildで作るかpullで取ってくるかした結果が保存されているもので、それを起動したのがコンテナです。

この線を引いて上から読み直します。

Dockerfileに書いたことはイメージに焼かれる

題材のDockerfileに出てくるのはこの6つです。

キー 何をする
FROM 土台にするイメージを決める
RUN buildのときだけ走り、結果がイメージに残る
WORKDIR 無ければ作って移動する。以降の行とCMDの基準になる
ENV 環境変数をイメージに焼く
EXPOSE 待ち受けるポートをイメージに書き残す。公開はしない
CMD イメージに焼かれるが、走るのは起動のたび

土台にしたイメージにWebサーバーごと入っていると考えていましたが、入っていないのでCMDで自分で起動しています。

CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]

0.0.0.0にしているのは、127.0.0.1だとコンテナの中からしか届かないからです。

EXPOSEは、書けばブラウザから8080番で見えると勘違いしていました。実際にやるのは「このイメージは8080番で待ち受けます」とイメージに書き残すことだけです。ホストから届くようにしているのはcompose.ymlのportsのほうで、公式がどう書いているかは後で引きます。

ENVのAPP_ENVはcompose.ymlのenvironmentにも同じキーがあり、ONLY_IN_DOCKERFILEはこちらにしかありません。わざと片方にだけ置いています。

(補足)RUNを&&で1行に繋いでいる理由

RUNひとつが1レイヤです。updateとinstallを分けると古い一覧のままinstallが走り、rm -rfを分けると消す前のレイヤが残ります。

compose.ymlに書いたことは実行のたびに効く

compose.ymlのほうは、題材に出てくるのがこの8つです。

キー 何をする 対応するDockerfile側
services コンテナを並べる。名前がそのままホスト名になる なし
build context:のディレクトリにあるDockerfileをビルドする なし
image ビルドせず、既製のイメージを取ってくる なし
working_dir コンテナの作業ディレクトリ WORKDIR
volumes ホストやDockerの領域をコンテナに重ねる なし
ports ホストからコンテナのポートに届くようにする EXPOSE
environment 環境変数を渡す ENV
depends_on / healthcheck 起動順と、条件つきの待ち合わせ なし

index.phpがpgsql:host=dbで繋がるのは、composeが同じネットワークにコンテナを並べてサービス名で名前解決するからです。

    ports:
      - "8080:8080"

左の8080がホスト側、右がコンテナ側です。EXPOSE 8080と数字は同じですが、ホストから届くようにしているのはこちらでした。

DockerfileにはAPP_ENV=localとあるのに、最初のcurlが返したのはenvironmentに書いたdockerのほうです。

volumesは書き方で寿命が変わります。

    volumes:
      - .:/work
    volumes:
      - pgdata:/var/lib/postgresql/data
書き方 実体 down -vのあと
.:/work(bind mount) ホストのディレクトリ 消えない
pgdata(named volume) Dockerが管理する領域 消える

DockerfileにCOPYがないので、public/index.phpがコンテナに入るのは.:/workのおかげでした。ホスト側で保存すれば、そのままリロードで反映されます。

depends_on: [db]だけだと、dbが起動した時点でphpが動き出します。Postgresは接続を受け付けるまでに数秒かかるので、その間にリクエストを投げるとConnection refusedで落ちました。condition: service_healthyを足すと、db側のhealthcheckpg_isreadyを2秒おきに30回まで)が通るまで待ちます。

healthcheckをphp側に書いても効きません。conditionが見るのは待たれる側で、db側に無ければこの条件は使えません。

上から読み終えてみると、Dockerfileで見たものと同じ名前がいくつも出てきました。

両方に書けるものはcompose側が勝つ

出てきた同名のうち、値まで同じなのが/workでした。DockerfileWORKDIRcompose.ymlworking_dirです。同じなので素通りしていましたが、2箇所に書けるならどちらが効くかは決まっているはずです。compose側に違う値を入れます。

Dockerfile compose.yml どっちが効くか
WORKDIR working_dir: compose
ENV environment: compose(同じキーだけ上書き、残りは残る)
CMD command: compose
ENTRYPOINT entrypoint: compose(このときCMDは消える)
EXPOSE ports: 役割が違う。EXPOSEは公開しない

Compose file referenceにはworking_dircommandもイメージ側の値を "overrides" するとあります。この2つは仕様です。ENVだけは事情が違います。

WORKDIRENVはcompose側の値になった

working_dir: /from-composeAPP_ENV: dockerを与えた側だけ変わりました。

--- 上書きなし ---
APP_ENV=local WD=/work
--- 上書きあり ---
APP_ENV=docker WD=/from-compose

イメージ側は書き換わっていません。ここからは題材のphpに戻って、イメージと、そこから起動したコンテナに同じ変数を聞いた結果です。

$ docker image inspect dockerfile-compose-lab-php --format '{{range .Config.Env}}{{println .}}{{end}}' | grep APP_ENV
APP_ENV=local

$ docker inspect $(docker compose ps -q php) --format '{{range .Config.Env}}{{println .}}{{end}}' | grep APP_ENV
APP_ENV=docker

composeがやっているのはイメージの書き換えではなく、コンテナを起動する瞬間に別の値を渡すことでした。DockerfileのAPP_ENV=localはイメージに焼かれたまま残っています。

environment:ENVを全部は消さなかった

DockerfileにはONLY_IN_DOCKERFILE=yesもあります。APP_ENVだけ上書きしてenvを叩きます(順不同)。

$ docker compose -f compose.override-demo.yml run --rm override env | grep -E "APP_ENV|ONLY_IN_DOCKERFILE"
ONLY_IN_DOCKERFILE=yes
APP_ENV=docker

丸ごと差し替わると思っていましたが、同じキーだけ上書きされ、残りは残りました。ただしCompose file referenceのenvironmentの項に、イメージ側のENVとの関係は書かれていません。仕様と言い切れないので、手元で確かめた挙動として書いておきます。

docker compose configENVは出てこない

$ docker compose -f compose.override-demo.yml config override
name: verify
services:
  override:
    build:
      context: /Users/yamoto/dev/personal/lab/dockerfile-compose-lab
      dockerfile: Dockerfile
    command:
      - php
      - -r
      - echo 'APP_ENV=', getenv('APP_ENV'), ' WD=', getcwd(), PHP_EOL;
    environment:
      APP_ENV: docker
    networks:
      default: null
    working_dir: /from-compose
networks:
  default:
    name: verify_default

ONLY_IN_DOCKERFILEが出てきません。ここに出ない変数を設定なしと判断したことがありますが、実際はコンテナの中にありました。configが見ているのはcompose.ymlだけで、イメージの中身は追っていないのだと理解しています。最終的な値はdocker inspectかコンテナ内のenvでしか見えません。

EXPOSEはポートを公開しない

題材とは別の検証用ファイルです。command:は同じで、差はports:だけ。

$ docker compose -f compose.expose-demo.yml ps --format 'table {{.Service}}	{{.State}}	{{.Ports}}'
SERVICE       STATE     PORTS
only-expose   running   8080/tcp
with-ports    running   0.0.0.0:18080->8080/tcp, [::]:18080->8080/tcp
--- inspectで見る ---
only-expose: ExposedPorts={"8080/tcp":{}} PortBindings={}
with-ports: ExposedPorts={"8080/tcp":{}} PortBindings={"8080/tcp":[{"HostIp":"","HostPort":"18080"}]}

ExposedPortsは両方同じで、違うのはPortBindingsです。公開しているのはports:の側です。Dockerfile referenceにもこうあります。

The EXPOSE instruction doesn't actually publish the port.

ではports:のないサービスには届かないのか。同じネットワークから叩きました(この検証用ファイルにenvironment:は無いのでlocalのままです)。

$ docker compose -f compose.expose-demo.yml exec -T with-ports php -r "echo file_get_contents('http://only-expose:8080/');"
APP_ENV=local

ホストに出ないだけで、compose networkの中では名前で引けます。題材のdbports:が無くてもphpから繋がります。

(補足)entrypointを書くとCMDが消える

ENTRYPOINTがある別イメージです。

FROM alpine:3.20
ENTRYPOINT ["echo", "EP:"]
CMD ["cmd-from-dockerfile"]
--- そのまま ---
EP: cmd-from-dockerfile
--- commandだけ上書き ---
EP: cmd-from-compose
--- entrypointだけ上書き ---
EP-from-compose:

command:だけならENTRYPOINTは残り、entrypoint:だとCMDごと消えました。公式にも明記があります。

If entrypoint is non-null, Compose ignores any default command from the image, for example the CMD instruction in the Dockerfile.

https://docs.docker.com/reference/compose-file/services/#entrypoint

おわりに

コピーして動かしていたファイルを、行ごとに説明できるようになりました。両方に書けるものはcompose側が勝つ、という線が引けました。同じようにコピーして動かしていた方の参考になれば嬉しいです。誤っている箇所あれば指摘ください。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?