DockerでBunを使ってサーバーを立ててみた

## はじめに

この記事では、JavaScriptランタイムであるBunをDockerコンテナ内で使う方法を紹介します。
その際、**Bunでサーバーを立ち上げ、ホストPCからアクセスする**というのをやってみました。

:::note warn
筆者はDocker初心者なので、必ずしも記事の内容が正しいわけではありません。
他のサイトや記事も参考にすることをお勧めします。
:::

:::note warn
この記事は[Zenn](https://zenn.dev/nanasi_1/articles/6375c0fbaa3b8d)とのマルチポストです。ご了承ください。
:::

Bunのホームページはこちら。

https://bun.sh


### 環境

* Docker 25.0.3
* VSCode 1.93.0
* Mac


## やり方

やり方はいくつかあると思いますが、この記事では以下の方法を使います。

* Docker Composeを使用
* 公式から配布されているイメージを使用

また、`exec`でコンテナに入れるようにします。

ざっくりとした手順はこちらです。

1. `Dockerfile`にベースイメージを書く
1. `Dockerfile`にコンテナが終了しないためのコマンドを書く
1. `compose.yaml`に設定を書く
1. コマンドでコンテナを立ち上げる


## Dockerfileを作成する

まずはプロジェクトルートに移動して、`Dockerfile`を作成します。


### ベースイメージを設定する

Bunの公式Dockerイメージはこちら。
今回はこのイメージを使ってコンテナを作成します。

https://hub.docker.com/r/oven/bun

`Dockerfile`に以下を追記します。

```dockerfile
# ベースイメージ
FROM oven/bun:latest

# プロジェクトルートを変更する（任意）
WORKDIR /project
```


### コンテナが終了しないようにする

このイメージだけだと、私の環境ではコンテナが終了してしまいました。
今回はコンテナに入って`bun`コマンドを実行したいので、この状態を修正する必要があります。

`Dockerfile`を以下のように編集してください。

```dockerfile
# ベースイメージ
FROM oven/bun:latest

# プロジェクトルートを変更する（任意）
WORKDIR /project

# コンテナが終了しないようにする
CMD ["bash"]
```

ちなみに`CMD ["bash"]`と`compose.yaml`の`tty: true`を組み合わせることによって、コンテナが停止するのを防ぐことができます。


## `compose.yaml`に設定を書く

Docker Composeでコンテナを立ち上げるため、`compose.yaml`を作成します。
そして内容を以下のように編集します。

```yml
version: '3'
services:
  app:
    build: .
    tty: true
    volumes: 
      - .:/project # パスはWORKDIRの記述と揃えるのがおすすめ
    ports:
      - 3000:3000
```

ここでは`app`という名前のサービスを作成しています。

* `build: .`: カレントディレクトリにある`Dockerfile`を使用
* `tty: true`: コンテナを終了させないように指定
* `volumes`: コンテナとホストでファイルを共有するのに使用
* `ports`: ホストからコンテナ内で立てたサーバーにアクセスするため、ポートを共有している

なお最後の`ports`は、Bunをサーバーの起動以外に使う場合は不要です。
また、サーバーを起動するポートをJavaScript側で変更する場合、こちらの設定を変更する必要があります。


## コンテナを起動する

これで準備は済んだので、いよいよコンテナを起動してみます。
コンテナを起動して入るには、以下のコマンドを入力します。

```bash
docker compose up -d
docker compose exec app bash
```

実行すると、プロンプトが変わりDockerコンテナに入ることができるはずです。


### bunコマンドが使えることを確認する

このコンテナはBunのイメージを使っているため、`bun`コマンドが使えるはずです。
試しに以下のコマンドを実行してみてください。

```bash
bun --help
```

ヘルプが出力されたら成功です。


### サーバーを起動してみる

では、JavaScriptで書いたサーバーをBunで動かしてみます。

まずはサーバーになるJavaScriptファイルを作成します。
`index.js`を作成し、以下のように編集してください。

```js
// サーバーを起動する
Bun.serve({
  fetch(req) {
    // リクエストが来たらログを出力
    console.log('request!')

    // Bunというレスポンスを返す
    return new Response("Bun");
  },
});
```

`Bun.serve`関数については、[ドキュメント](https://bun.sh/docs/api/http#bun-serve)が参考になると思います。

:::note info
コンテナとホストでファイルを共有（マウント）しているので、ホストで`index.js`を作成すると自動的にコンテナにも`index.js`が作成されます。
:::


次に、このファイルをBunで実行してみます。
以下のコマンドを入力してください。

```bash
bun index.js
```

サーバーが起動できたら、ホストから http://localhost:3000 にアクセスしてみてください。
`Bun`というレスポンスが返ってきたら成功です。


## コンテナをサーバー起動用にする

ところで、こちらで作成したDockerコンテナですが、当然ながらBunのサーバーを終了してもコンテナは終了しません。
開発時はこれで大丈夫ですが、完成した時にこれだとサーバーの軌道に少し手間がかかります。

ということで、次は**コマンド一つでコンテナが起動してサーバーが立ち上がり**、そのコマンドを**終了するとコンテナもサーバーも停止**ようにします。

なお、作成したJavaScriptコードががサーバーのように常時稼働するものではない場合、JavaScriptコードはコンテナ内でBunによって実行され、実行が終わるとコンテナも終了します。


### やり方

大まかには以下のような手順を踏みます。

* `Dockerfile`の常時起動用の行を削除する
* `Dockerfile`にサーバーを起動するコマンドを書く
* `compose.yaml`の`tty`を削除する
* Volumeを使っていたところを`COPY`に置き換える（任意）
* `start-server.sh`を作成する（任意）

なお、Gitを使っている場合、この作業を始める前にブランチを分けておくことをお勧めします。
多分そうすれば本番環境用のブランチと開発用のブランチができると思います。多分。


### `Dockerfile`を書き換える

まずは`Dockerfile`を編集していきます。
ここでは常時起動用の行を削除し、代わりにサーバーを起動するためのコマンドを追記します。

```dockerfile
# ベースイメージ
FROM oven/bun:latest

# プロジェクトルートを変更する（任意）
WORKDIR /project

# サーバーを起動する
CMD ["bun", "index.js"]
```

:::note warn
`index.js`はエントリーポイントとなるJavaScriptファイルのパスです。
将来的に`index.js`を`src`に移動したり、TypeScriptに変えて`index.ts`になった場合、この`Dockerfile`も変更する必要があることに注意してください。
:::

また、本番でファイルをマウントするならVolumeより`COPY`を使ってコピーするのがおすすめだとChatGPTが言っていたので、こちらも書き換えてみます。

```dockerfile
# ベースイメージ
FROM oven/bun:latest

# プロジェクトルートを変更する（任意）
WORKDIR /project

# ファイルをコピーする
COPY ./ ./

# サーバーを起動する
CMD ["bun", "index.js"]
```

ちなみにですが、おそらく変更しなくても動きます。


### `compose.yaml`を書き換える

次は`compose.yaml`の設定を変更します。
ここでは`tty`と`volume`の設定を削除します。

```yml
version: '3'
services:
  app:
    build: .
    ports:
      - 3000:3000
```

なお、上で`Dockerfile`に`COPY`を追加していない場合、`volume`の削除は必要ありません。

* * *

ここまででサーバーを直接起動することができるようになったはずなので、一度試してみます。
コンテナを立ち上げるため、以下のコマンドを入力してください。

```bash
docker compose up --build
```

実行するとコンテナが起動し、その中でサーバーが立ち上がるはずです。
ホストから http://localhost:3000 にアクセスし、`Bun`というレスポンスが返ってきたら成功です。

なお、`--build`というオプションは、使用しているイメージをビルドし直すものになります。
イメージを作り直さないと、`Dockerfile`に変更を加える前のイメージが使われてしまいます。
それを防ぐために今回はこのオプションをつけています。


### サーバーを起動する実行ファイルを作る

サーバーを起動したいときに`docker`コマンドをいちいち打ってもいいですが、場合によっては少しわかりにくいかもしれません。
どうせなら一つのファイルにまとめてしまおうと思います。

ということで、`start-server.sh`を作成し、内容を以下のようにします。

```bash
docker compose up
docker compose down
```

下の行は、コンテナが停止（`docker compose up`が終了）した際に自動でコンテナを破棄するためのコマンドです。
ここにオプションを付与することで、イメージなどを自動で削除することもできるみたいです。

ファイルが作れたら、試しに実行してみます。
以下のコマンドで実行してみてください。

```bash
bash start-server.sh
```

もしくは、実行権限を付与して`./start-server.sh`としても実行できます。