RustをDockerで動かしてホットリロードしてみる

# 対象読者
Dockerコンテナ上でRustのコードをデプロイしたいけど、コードを変更するたびにDockerコンテナを立ち上げ直さなければいけない、や一回プロセス終了させて
```bash
docker compose exec hoge cargo run 
```
とかやるの面倒だ、と思っている方が対象です！

ある程度、Dockerのコマンドや概念、RustやCargoについて知っている前提で進めていきます。

# 要約
- Docker Compose Watch を利用してコードの変更を認識する
- cargo-watchをコンテナでインストールする
- compose.ymlにこれを追記する
```yml:compose.yml
services:
  back:
    develop:
      watch:
        - action: sync
          path: ./backend
          target: /work/backend
          ignore:
            - ./backend/target
    command: /bin/sh -c "cargo watch -x run"
```

# Rustでホットリロード
皆さん！Rustでサーバーサイド処理書いていますか？
サーバーサイドでなくても、常に動かしておくプログラムっていっぱい書くと思います。
そのようなプログラムを書いているとき、プログラムを書き替えたら再ビルドを自動でしてくれると便利ですよね。

**そこでホットリロードの登場です！**

ホットリロードは、プログラムを書き替えると再ビルドを自動で行ってくれる機能ですが、それをRustで行う方法として **cargo-watch** があります。
やり方としては、
```bash
cargo install cargo-watch --locked
```
と、cargo-watchをインストールして、プロジェクトのディレクトリで
```bash
cargo watch -x run
```
とするだけでホットリロードが実現できます。

しかし、Dockerコンテナ上ではそううまくはいきません。

# 起こったこと
まず、前提としてこのようなディレクトリ構成で作業しています。
```plaintext
/project
├─/backend
│　├─Cargo.toml
│　├─Cargo.lock
│　├─.gitignore
│　├─Dockerfile
│　├─/target
│　└─/src
│　　　└─main.rs
├─/frontend
├─/database
│　├─init.sql
│　└─Dockerfile
├─.env
└─compose.yml
```

また、compose.ymlやDockerfileにはこのようなことを記述しています。

```yaml:compose.yml
services:
  back:
    build:
      context: ./backend
    tty: true
    hostname: "${APP_HOST}"
    ports:
      - "${APP_PORT}:${APP_PORT}"
    env_file:
      - .env
    volumes:
      - "./cargo-cache:/usr/local/cargo/registry"
      - "./rust-target-cache:/work/backend/target"
      - "./backend:/work/backend"
    command: /bin/sh -c "cargo watch -x run"

volumes:
  cargo-cache:
  rust-target-cache:
```

```dockerfile:./backend/Dockerfile
FROM rust:latest

WORKDIR /work/backend

# 必要なツールをインストール
RUN apt-get update && apt-get install -y \
	inotify-tools

# cargo-watchをインストール
RUN cargo install cargo-watch

```

このcompose.ymlとDockerfileを書いて
```bash
docker compose up -d
```

としたところ、

**ホットリロードが行われていませんでした😭**

いったいなぜ？と思い、私たちはアマゾンのジャングルの中へ・・・

**なんとマウントされたファイルは認識しないとのこと！？**[1]

となると、どうしたらいいんだ・・・と思い、調査を続けると、どうやら **Docker Compose Watch** なるものがあるじゃないですか！

# Docker Compose Watch
Docker Compose Watchは、compose.ymlファイルに記述したディレクトリ以下で変更があった場合に、コンテナの再スタートや再ビルド、同期等を行ってくれる機能です。

この様にcompose.ymlに記述すると、Docker側でホットリロードを行ってくれます。（[3]からの引用）
```yaml: compose.yml
services:
  web:
    build: .
    command: npm start
    develop:
      watch:
        - action: sync
          path: ./web
          target: /src/web
          ignore:
            - node_modules/
        - action: rebuild
          path: package.json
```
この設定の場合は、./web以下のファイルに変更があれば/src/webと./webを同期し、package.jsonに変更があればコンテナをビルドし直す設定になっています。

これを利用して早速Rustでホットリロードしていきましょう！

# Dockerコンテナ上でのRustのホットリロード
では、まず./backend/Dockerfileを変更していきましょう。
このやり方では、マウントではなくCOPYして同期させる方法でWindows上の変更をDockerコンテナに適用させます。
そのため、COPYを追記し、backendディレクトリ以下のファイルをコンテナにコピーさせています。
```Dockerfile
FROM rust:latest

WORKDIR /work/backend

# 必要なツールをインストール
RUN apt-get update && apt-get install -y \
	inotify-tools

# cargo-watchをインストール
RUN cargo install cargo-watch

COPY . .
```
次に、変更したファイルをDocker上のファイルに適用させるため、compose.ymlを変更しましょう！
backend以下に変更があれば、コンテナ上のファイルにも適用させてほしいので、./backend以下に変更があれば、/work/backendに対してsyncを行うよう設定します。
また、targetディレクトリ以下が変更されても無視をする設定を行っています。
```yml: compose.yml
services:
  back:
    build:
      context: ./backend
    tty: true
    hostname: "${APP_HOST}"
    ports:
      - "${APP_PORT}:${APP_PORT}"
    env_file:
      - .env
    volumes:
      - "./cargo-cache:/usr/local/cargo/registry"
      - "./rust-target-cache:/work/backend/target"
    depends_on:
      - db
    develop:
      watch:
        - action: sync
          path: ./backend
          target: /work/backend
          ignore:
            - ./backend/target
    command: /bin/sh -c "cargo watch -x run"

volumes:
  cargo-cache:
```
これで、動かしてみます。
動かす方法は、
```bash
docker compose up --watch
```
で、composeで立ち上げたものをwatchしています。
さて、まずは動作確認・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474462/4c93d616-9bf7-4058-488a-f82bb604cd44.png)

ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!（DBコンテナが動いているのは気にしないでね）

では、ホットリロードが機能するのか・・・
```rust
println!("Hello, World!");
```
を追記してみてみたいと思います！

動くか・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3474462/980fe144-edcf-8c6c-6d55-16d75375e2ab.png)

ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!

ついにやってやりました！
これでわずらわしさを少し減らして開発ができる・・・

めちゃくちゃ調べるのに時間がかかったので是非参考にしていただければと思います。
また、至らぬ点や気になる点があればぜひコメントお願いします！

# 参考文献
[1] "WSL+DockerでHot reload が動作しない", https://banatech.net/blog/view/54 (2024/12/27)
[2] "Docker Compose Watchを触ってみる", https://zenn.dev/maybe_dog/articles/bfeeee3b6650a1 (2024/12/27)
[3] "Use Compose Watch", https://docs.docker.com/compose/how-tos/file-watch/ (2024/12/27)