やあ!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を紹介しました。みんなも試してみてね!