Docker Compose Watchのすすめ

やあ!id:cockscombです。日々の生活に役立つちょっとした知識を紹介していきます。最近は、Apple WatchやPixel Watchみたいな、ナントカWatchのリリースが多いですね。でも今日紹介するのは、WatchはWatchでも、Docker Compose Watchです。


Docker Composeは、複数のコンテナを扱った開発に用いる道具で、コンテナを活用した開発では当たり前に使われている。そのDocker Composeに、ファイルの変更を監視してコンテナの再構成を行わせるのが、Docker Compose Watchだ。Docker Compose 2.22以降で利用できる。最新のDocker Desktopにも付属している。

ホットリロードとコンテナ開発

Docker Compose Watchがどういうものかを説明する前に、Next.jsのホットリロードについて考える。Next.jsでは next dev コマンドが自動的にホットリロードを行う。ファイルの変更を検知し、内蔵されたWebpack(あるいはTurbopack)がファイルをビルドし、Hot Module Replacementを駆動させる。

これをDockerのコンテナ内で実行するためには、ローカルのコードをコンテナ内にマウントさせるのが普通だ。コンテナにマウントさせることで、ローカルのエディタで編集した内容を即座にコンテナへ反映させられる。ところが、これだと不便なケースがある。

node_modules/ ディレクトリのように、コンテナの内外で共有すると不都合なものがある。node_modules/ には環境に依存したファイルが作られるかもしれない。こういうとき、マウントでは .dockerignore ファイルは参照されないから、compose.yaml を丁寧に書いたりするが、メンテナンス性が低下する。

コンテナの外で package.json が書き換えられた場合、コンテナ内で npm install を実行するか、あるいはコンテナイメージの再ビルドを行う必要があるが、もっぱら手作業になる。

こういう場面でDocker Compose Watchを使う。

Docker Compose Watchを使う

ここではNext.jsのアプリケーションをDockerコンテナで開発しているものとする。次のような compose.yamlで、next dev を実行している。ここではワーキングディレクトリ全体をコンテナ内にマウントしているとする。

services:
  server:
    build:
      context: .
      target: builder
    command: ["npm", "run", "dev"]
    volumes:
      - .:/app
    environment:
      NODE_ENV: development
    ports:
      - 3000:3000

Docker Compose Watchを使うには、volumes でマウントするのをやめて、develop.watch セクションを追加する。ここに今回はふたつのエントリを追加している。ワーキングディレクトリ全体(path: .)に変化があればそれを同期(sync)させる設定と、package.json に変化があればコンテナイメージを再ビルド(rebuild)させる設定だ。

services:
  server:
    build:
      context: .
      target: builder
    command: ["npm", "run", "dev"]
    develop:
      watch:
        - action: sync
          path: .
          target: /app/
        - action: rebuild
          path: ./package.json
    environment:
      NODE_ENV: development
    ports:
      - 3000:3000

このまま docker compose up の代わりに docker compose watch すると、Docker Compose Watchが動作する。

もしファイルに変化があれば、次のようなログが出力され、同期される。

Syncing server after changes were detected:
  - /path/to/your/project/src/app/page.tsx

package.json が変化した時は、コンテナイメージが再ビルドされて作り直される。

Rebuilding server after changes were detected:
  - /path/to/your/project/package.json
[+] Building 1.6s (14/14) FINISHED                         docker:desktop-linux
 => [server internal] load build definition from Dockerfile                0.0s
 => => transferring dockerfile: 1.06kB                                     0.0s
 => [server internal] load .dockerignore                                   0.0s
 => => transferring context: 672B                                          0.0s
 => [server] resolve image config for docker.io/docker/dockerfile:1        0.8s
 => CACHED [server] docker-image://docker.io/docker/dockerfile:1@sha256:a  0.0s
 => [server internal] load metadata for docker.io/library/node:20-slim     0.6s
 => [server base 1/1] FROM docker.io/library/node:20-slim@sha256:8d26608b  0.0s
 => [server internal] load build context                                   0.0s
 => => transferring context: 781B                                          0.0s
 => CACHED [server deps 1/2] WORKDIR /app                                  0.0s
 => CACHED [server deps 2/2] RUN --mount=type=bind,source=package.json,ta  0.0s
 => CACHED [server builder 1/4] WORKDIR /app                               0.0s
 => CACHED [server builder 2/4] RUN --mount=type=bind,source=package.json  0.0s
 => CACHED [server builder 3/4] COPY . .                                   0.0s
 => CACHED [server builder 4/4] RUN npm run build                          0.0s
 => [server] exporting to image                                            0.0s
 => => exporting layers                                                    0.0s
 => => writing image sha256:23d98e60db18c434926bb471e9844030bd135ee5bf5b4  0.0s
 => => naming to docker.io/library/watch-server                            0.0s
[+] Running 1/0
 ✔ Container watch-server-1  Running  

node_modules/ ディレクトリなどはここではケアしていないが、.dockerignore ファイルで無視するようにしていると、その設定が反映される。

ここまでのことが次のドキュメントに同じように書かれている。

Docker Compose Watchが使える場面とそうでない場面

Docker Compose Watchでは、これまでマウントでなんとかしていたようなことが概ねカバーされている。実際に使ってみているが、目立った不具合もなく(アルファバージョンでは少し不具合があったが今は解消されている)便利だ。マウントするのに較べて設定は簡単だし、驚くような挙動がなく、コントロールしやすく感じる。

ただしなんらかの理由で、コンテナの中で書き換えたファイルをローカルに取り出すような、逆方向の処理はカバーされない。そういったユースケースがある場合は、今まで通りマウントしたり、docker compose cp で丁寧に取ってくる必要がある。


ということで、Docker Compose Watchを紹介しました。みんなも試してみてね!