0
1

Node.jsとBunのDockerイメージを使った開発環境について備忘録

Last updated at Posted at 2024-07-27

後発のJSランタイム兼パッケージマネージャことBunについての勉強メモ
BunについてホストPCへのグローバルインストールをせずにDockerを用いた環境構築を行います
対比する目的でNodeを使った環境も併せて構築します

環境情報

項目 内容
PC本体のOS Windows 10 Pro
PC本体のメモリ 32GB
WSL2 Ubuntu 20.04
WSL2のメモリ 16GB
エディタ VSCode 1.91.1
Docker 20.10.14

NodeとBunのコンテナを作成

まず、NodeとBunについてまっさらなコンテナ環境を作成します

Node.jsのDockerイメージについて

上記リンクのTagsページより 20.16.0-slim というイメージを使用しました

.docker/node/Dockerfile
FROM node:20.16.0-slim

ENV TZ Asia/Tokyo

WORKDIR /app

# uid=1000
USER node
  • Dockerfile内でUSER命令を使用し、ホストPC側でdockerコマンドを実行するユーザーのUIDと揃えています(いわゆるWSL2の一般ユーザー)
  • uid=1000のユーザーはnodeイメージにおいては node という名前になっています

BunのDockerイメージについて

上記リンクのTagsページより 1.1.20-debian というイメージを使用しました

.docker/bun/Dockerfile
FROM oven/bun:1.1.20-debian

ENV TZ Asia/Tokyo

WORKDIR /app

# uid=1000
USER bun
  • こちらもNodeのコンテナと同様の理由でUSER命令を用いて実行ユーザーを書き換えます
  • uid=1000のユーザーはbunイメージにおいては bun という名前になっています

Dockerコンテナ起動

compose.ymlは以下のような内容となっています

compose.yml
services:
  node:
    build:
      context: ./
      dockerfile: ./.docker/node/Dockerfile
    volumes:
      - ./node_app:/app:cached
    tty: true

  bun:
    build:
      context: ./
      dockerfile: ./.docker/bun/Dockerfile
    volumes:
      - ./bun_app:/app:cached
    tty: true

以下のコマンドでDockerコンテナを起動

$ docker compose up -d
[+] Building 6.6s (11/11) FINISHED                                                                                                             
 => [node-and-bun-with-docker_bun internal] load build definition from Dockerfile                                                         0.0s
 => => transferring dockerfile: 31B                                                                                                       0.0s
 => [node-and-bun-with-docker_node internal] load build definition from Dockerfile                                                        0.0s
 => => transferring dockerfile: 31B                                                                                                       0.0s
 => [node-and-bun-with-docker_bun internal] load .dockerignore                                                                            0.0s
 => => transferring context: 2B                                                                                                           0.0s
 => [node-and-bun-with-docker_node internal] load .dockerignore                                                                           0.0s
 => => transferring context: 2B                                                                                                           0.0s
 => [node-and-bun-with-docker_bun internal] load metadata for docker.io/oven/bun:1.1.20-debian                                            6.3s
 => [node-and-bun-with-docker_node internal] load metadata for docker.io/library/node:20.16.0-slim                                        6.4s
 => [node-and-bun-with-docker_bun 1/2] FROM docker.io/oven/bun:1.1.20-debian@sha256:7f8d283941dc113f09ddcb6727160980199302277a8abfc57168  0.0s
 => CACHED [node-and-bun-with-docker_bun 2/2] WORKDIR /app                                                                                0.0s
 => [node-and-bun-with-docker_node] exporting to image                                                                                    0.1s
 => => exporting layers                                                                                                                   0.0s
 => => writing image sha256:327eb7ec2de746a47e2f4494044f0ba5c79f2ad6d4a2ddeee09405abb8bd886d                                              0.0s
 => => naming to docker.io/library/node-and-bun-with-docker_bun                                                                           0.0s
 => => writing image sha256:3e5277598d4240c6aa08e7874b442d3dfe28c4c909cc3510528f9127a6251e19                                              0.0s
 => => naming to docker.io/library/node-and-bun-with-docker_node                                                                          0.0s
 => [node-and-bun-with-docker_node 1/2] FROM docker.io/library/node:20.16.0-slim@sha256:a22f79e64de59efd3533828aecc9817bfdc1cd37dde598aa  0.0s
 => CACHED [node-and-bun-with-docker_node 2/2] WORKDIR /app                                                                               0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
[+] Running 3/3
 ⠿ Network node-and-bun-with-docker_default   Created                                                                                     0.0s
 ⠿ Container node-and-bun-with-docker-bun-1   Started                                                                                     0.7s
 ⠿ Container node-and-bun-with-docker-node-1  Started                                                                                     0.9s

コンテナを起動するとnodeのコンテナは起動していますが、bunのコンテナが即落ち(Exited (0))していました。

$ docker ps
CONTAINER ID   IMAGE                           COMMAND                  CREATED          STATUS          PORTS     NAMES
96e3ffc02f36   node-and-bun-with-docker_node   "docker-entrypoint.s…"   42 seconds ago   Up 41 seconds             node-and-bun-with-docker-node-1

$ docker ps -a
CONTAINER ID   IMAGE                                                                COMMAND                  CREATED              STATUS                          PORTS     NAMES
96e3ffc02f36   node-and-bun-with-docker_node                                        "docker-entrypoint.s…"   About a minute ago   Up About a minute                         node-and-bun-with-docker-node-1
157b4b2a4381   node-and-bun-with-docker_bun                                         "/usr/local/bin/dock…"   About a minute ago   Exited (0) About a minute ago             node-and-bun-with-docker-bun-1

bunコンテナのログを出してみると、bunコマンドを実行したときの出力が確認できました。

$ docker compose logs bun
node-and-bun-with-docker-bun-1  | Bun is a fast JavaScript runtime, package manager, bundler, and test runner. (1.1.20+ae1948925)
node-and-bun-with-docker-bun-1  | 
node-and-bun-with-docker-bun-1  | Usage: bun <command> [...flags] [...args]
node-and-bun-with-docker-bun-1  | 
node-and-bun-with-docker-bun-1  | Commands:
node-and-bun-with-docker-bun-1  |   run       ./my-script.ts       Execute a file with Bun
node-and-bun-with-docker-bun-1  |             lint                 Run a package.json script
node-and-bun-with-docker-bun-1  |   test                           Run unit tests with Bun
node-and-bun-with-docker-bun-1  |   x         vite                 Execute a package binary (CLI), installing if needed (bunx)
node-and-bun-with-docker-bun-1  |   repl                           Start a REPL session with Bun
node-and-bun-with-docker-bun-1  |   exec                           Run a shell script directly with Bun
node-and-bun-with-docker-bun-1  | 
node-and-bun-with-docker-bun-1  |   install                        Install dependencies for a package.json (bun i)
node-and-bun-with-docker-bun-1  |   add       lyra                 Add a dependency to package.json (bun a)
node-and-bun-with-docker-bun-1  |   remove    moment               Remove a dependency from package.json (bun rm)
node-and-bun-with-docker-bun-1  |   update    @remix-run/dev       Update outdated dependencies
node-and-bun-with-docker-bun-1  |   link      [<package>]          Register or link a local npm package
node-and-bun-with-docker-bun-1  |   unlink                         Unregister a local npm package
node-and-bun-with-docker-bun-1  |   patch <pkg>                     Prepare a package for patching
node-and-bun-with-docker-bun-1  |   pm <subcommand>                Additional package management utilities
node-and-bun-with-docker-bun-1  | 
node-and-bun-with-docker-bun-1  |   build     ./a.ts ./b.jsx       Bundle TypeScript & JavaScript into a single file
node-and-bun-with-docker-bun-1  | 
node-and-bun-with-docker-bun-1  |   init                           Start an empty Bun project from a blank template
node-and-bun-with-docker-bun-1  |   create    next-app             Create a new project from a template (bun c)
node-and-bun-with-docker-bun-1  |   upgrade                        Upgrade to latest version of Bun.
node-and-bun-with-docker-bun-1  |   <command> --help               Print help text for command.
node-and-bun-with-docker-bun-1  | 
node-and-bun-with-docker-bun-1  | Learn more about Bun:            https://bun.sh/docs
node-and-bun-with-docker-bun-1  | Join our Discord community:      https://bun.sh/discord

以下の画像は使用しているDockerイメージ(bun:1.1.20-debian)のレイヤー情報です。

image.png

最後のCMD命令によってbun(/usr/local/bin/bun)が実行されており、ログの情報と状況は合致します。

というわけでCMD命令が原因落ちていることが分かったのでコンテナでの開発を行うために、何かしらコンテナが落ちないようにするCMD命令の上書きを行います。
今回は以下のようにしました。

.docker/bun/Dockerfile
FROM oven/bun:1.1.20-debian

ENV TZ Asia/Tokyo

WORKDIR /app

# uid=1000
USER bun
+
+CMD ["sh"]

Dockerfileを修正後、改めてコンテナ環境を起動しなおし今度はbunイメージを使用したコンテナ環境も落ちていないことを確認できました:innocent:

$ docker ps
CONTAINER ID   IMAGE                           COMMAND                  CREATED          STATUS          PORTS     NAMES
65df9d42f35b   node-and-bun-with-docker_node   "docker-entrypoint.s…"   25 seconds ago   Up 23 seconds             node-and-bun-with-docker-node-1
5ca416c8f1e3   node-and-bun-with-docker_bun    "/usr/local/bin/dock…"   25 seconds ago   Up 23 seconds             node-and-bun-with-docker-bun-1

nodeコンテナが落ちない訳

読まなくてもいいやつ

念のためなぜnodeコンテナの方は落ちていないかも調べました。
以下の画像は使用しているDockerイメージ(node:20.16.0-slim)のレイヤー情報です。

image.png

こちらもbunと同じ具合に最後のCMD命令では単純にnodeコマンドのみを実行しています。

コンテナのログは以下のようになっていました

$ docker compose logs node
node-and-bun-with-docker-node-1  | Welcome to Node.js v20.16.0.
node-and-bun-with-docker-node-1  | Type ".help" for more information.

nodeコンテナは起動できているのでコンテナ内で実際にnodeコマンドを実行していたところ対話が可能な状態となりました。これによってnodeコンテナは落ちなかったということみたいです。

image.png

Node.js公式のドキュメントとしては以下のような情報がありました。

https://nodejs.org/en/learn/command-line/how-to-use-the-nodejs-repl#how-to-use-the-nodejs-repl

If we run the node command without any script to execute or without any arguments, we start a REPL session:

↓

実行するスクリプトや引数を指定せずにノード コマンドを実行すると、REPL セッションが開始されます。

まとめ

Node.jsは引数やスクリプトを指定せずにnodeコマンドを実行するとREPLモードになり、つまりは対話が可能な状態となるため待ちの状態となり結果としてコンテナが落ちなかった

bunのREPLモードは?

読まなくてもいいやつ

nodeの方は何も引数やスクリプトを指定せずに node とだけ打つと対話可能(REPL)なセッションが開始されましたが、Bunの方はどうかと調べたところbunコマンドを実行したときの出力としてreplというサブコマンドから対話型のセッションを開始できるとありました。

image.png

今回の記事の中ではbunコンテナを落ちないようにする施策として CMD ["sh"] の設定を行いましたが、nodeコンテナと同じ要領で行くのであれば以下のような形もアリかも?

.docker/bun/Dockerfile
FROM oven/bun:1.1.20-debian

ENV TZ Asia/Tokyo

WORKDIR /app

# uid=1000
USER bun
+
+CMD ["bun", "repl"]

なお、実際に bun repl を実行してみると以下のような実験段階的なメッセージが出力されました。

$ bun repl
Welcome to Bun v1.1.20
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
> 
> .help
Commands & keybinds:
    .help         Show this help message.
    .info         Print extra REPL information.
    .multiline    Toggle multi-line mode. (Alt+M)
    .save         Save all successful commands evaluated in this REPL session to a file.
    .load         Load a file into the REPL session. (Experimental)
    .clear        Clear the screen. (Ctrl+L)
    .exit         Exit the REPL. (Ctrl+C / Ctrl+D)

    Alt+N         Insert a raw newline at cursor position, works even in single-line mode.
      * In multi-line mode, Enter can only append newline to the end of the input.
    Alt+M         Toggle multi-line mode.
    Shift+Tab     Insert an indent instead of triggering autocompletions (autocomplete not yet implemented).
> 
> .exit
  • セッションの閉じ方なんかはnodeのREPLと同じ風になっていました

両コンテナ環境でそれぞれNuxt3導入

コンテナが正常に稼働する状況は作れたので、それぞれのJSランタイム環境下でNuxt3を動かしてみます

image.png

なお、作業開始の前にnodeとbunの両コンテナについてブラウザからの動作確認で必要となるポートフォワーディングの設定を実施しています。

compose.yml
services:
  node:
    build:
      context: ./
      dockerfile: ./.docker/node/Dockerfile
    volumes:
      - ./node_app:/app:cached
+    ports:
+      - "3000:3000"
    tty: true

  bun:
    build:
      context: ./
      dockerfile: ./.docker/bun/Dockerfile
    volumes:
      - ./bun_app:/app:cached
+    ports:
+      - "3001:3000"
    tty: true
  • ホスト側のポートは重複できないのでbunコンテナの方はデフォルトのポート番号+1としました

nodeコンテナでの作業

Node.js標準のパッケージマネージャ(npm)を使用したインストールを行いました

nodeコンテナ内での操作
$ npx nuxi@latest init .

✔ Which package manager would you like to use?
npm
◐ Installing dependencies...                                                                                                      11:24:26 AM
npm warn deprecated are-we-there-yet@2.0.0: This package is no longer supported.
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated npmlog@5.0.1: This package is no longer supported.
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated gauge@3.0.2: This package is no longer supported.
npm warn deprecated glob@8.1.0: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported

> postinstall
> nuxt prepare

✔ Types generated in .nuxt                                                                                                       11:25:42 AM

added 643 packages, and audited 645 packages in 1m

128 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
✔ Installation completed.                                                                                                        11:25:42 AM

✔ Initialize git repository?
No
                                                                                                                                  11:25:48 AM
✨ Nuxt project has been created with the v3 template. Next steps:
 › Start development server with npm run dev                                                                                      11:25:48 AM
  • 依存するパッケージのインストールで結構時間が掛かりましたが特にエラー無く完了しました

bunコンテナでの作業

bunはパッケージマネージャも兼ねており、Nuxt3もBunを使用したインストールが可能なので以下のような作業となりました。

bunコンテナ内での操作
$ bunx nuxi@latest init .

✔ Which package manager would you like to use?
bun
◐ Installing dependencies...                                                                                                      11:41:45 AM
bun install v1.1.20 (ae194892)

 ERROR  Error: Command failed: bun install                                                                                        11:41:45 AM
The first argument must be a Readable, a ReadableStream, or an async iterable.

b
$ nuxt prepare
✔ Types generated in .nuxt                                                                                                       11:41:49 AM

+ nuxt@3.12.4
+ vue@3.4.34

611 packages installed [8.51s]
  • エラーが出てしまいましたがインストール処理自体は継続している様子(念のため再度bun installを実行しました)
  • インストールに掛かる時間はnodeのときの1/10くらい(ものすごく速い)

image.png

  • 最初のパッケージマネージャの選択でbunを選択することで npm install の代わりに bun install が行われました
  • package-lock.json の代わりに bun.lockb が生成されました

nodeコンテナで動作確認

ホスト側から以下のようなコマンドでdevサーバーを起動

$ docker compose exec node npm run dev

> dev
> nuxt dev

Nuxt 3.12.4 with Nitro 2.9.7                                                                                                      11:55:22 AM
                                                                                                                                  11:55:22 AM
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

  ➜ DevTools: press Shift + Alt + D in the browser (v1.3.9)                                                                       11:55:24 AM

✔ Vite client built in 46ms                                                                                                      11:55:25 AM
✔ Vite server built in 424ms                                                                                                     11:55:25 AM
✔ Nuxt Nitro server built in 1004 ms                                                                                       nitro 11:55:27 AM
ℹ Vite client warmed up in 1ms                                                                                                   11:55:27 AM
ℹ Vite server warmed up in 818ms                                                                                                 11:55:28 AM

image.png

  • 正常に起動できることが確認できました

bunコンテナで動作確認

ホスト側から以下のようなコマンドでdevサーバーを起動

$ docker compose exec bun bun run dev
$ nuxt dev
Nuxt 3.12.4 with Nitro 2.9.7                                                                                                      11:57:38 AM
                                                                                                                                  11:57:39 AM
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

  ➜ DevTools: press Shift + Alt + D in the browser (v1.3.9)                                                                       11:57:40 AM

✔ Vite client built in 50ms                                                                                                      11:57:41 AM
✔ Vite server built in 1410ms                                                                                                    11:57:43 AM
✔ Nuxt Nitro server built in 783 ms                                                                                        nitro 11:57:44 AM
ℹ Vite server warmed up in 1364ms                                                                                                11:57:45 AM
ℹ Vite client warmed up in 2386ms                                                                                                11:57:46 AM
  • コンソール出力にある ➜ Local: http://localhost:3000/ はコンテナ内の話な点に注意(ホストPCのブラウザから http://localhost:3000 でアクセスできるということではない)

image.png

  • こちらも正常に起動できることが確認できました
  • bunコンテナの方は3001:3000のようなポートフォワーディングを行っているので、app.vueに適当な修正を加えhmr updateが機能することも確認しました

備考

今回の検証について直接関係ないことや、いまいちよくわかってないことについて書いています。
HMRの件は特に勉強不足感がある(要viteの勉強)。:disappointed_relieved:

bunでのパッケージ管理についてyarn.lockを利用

bun.lockb はバイナリで中身のテキストを確認する方法はあるのですが(bun bun.lockb)、確認したいときに都度コマンドを打つのはやや面倒な気がしました。

この件についてBunは bun.lockb と合わせて yarn.lock を生成することが出来るのでその設定を行いました。
※この設定によって bun.lockb が生成されなくなるわけではないのでlockファイルを2重に管理するようなことが無いように .gitignore の設定も行います

./bun_app/bunfig.toml
[install.lockfile]
# whether to save a non-Bun lockfile alongside bun.lockb
# only "yarn" is supported
print = "yarn"
  • 新規で bunfig.toml というファイルを作成
./bun_app/.gitignore
~~~省略~~~

# Local env files
.env
.env.*
!.env.example

+# bun lockfile
+bun.lockb
  • .gitignore で bun.lockb をGit管理の対象から外すよう設定
$ rm ./bun_app/bun.lockb
$ git add -A
$ docker compose exec bun bun install
bun install v1.1.20 (ae194892)

$ nuxt prepare
✔ Types generated in .nuxt                                                                                                                                                                                                           1:42:52 PM

Checked 637 installs across 704 packages (no changes) [2.88s]
  • 既存の bun.lockb について削除し削除した状態をGitにステージ
  • 再度bun installを実行し bun.lockb と yarn.lock が生成されること&Gitの差異に上がらないことを確認

HMRについてよくわかってない問題

これまでNuxt3の開発環境を作るときにHMR(Hot module replacement)で24678番ポートを使用すると認識していたのですが、どうもこちらは認識が誤っているみたい?
実際にこの記事を書いていた際、compose.yml でのポートフォワーディングの設定について以下のような記述を行っていました。

compose.yml
services:
  node:
    build:
      context: ./
      dockerfile: ./.docker/node/Dockerfile
    volumes:
      - ./node_app:/app:cached
    ports:
      - "3000:3000"
+      - "24678:24678"
    tty: true

  bun:
    build:
      context: ./
      dockerfile: ./.docker/bun/Dockerfile
    volumes:
      - ./bun_app:/app:cached
    ports:
      - "3001:3000"
+      - "24679:24678"
    tty: true

以下は上記の compose.yml の記述をやめた状態でbunコンテナ側のdevサーバーを起動したときの様子

image.png

  • app.vueについてウェルカムのコンポーネントを削って保存し、hmr updateという出力が流れることを確認

image.png

  • ブラウザで http://localhost:3001 にアクセスしたときの様子
  • ws(WebSocket)の接続が行えていることと、エディタ側で更新したapp.vueについて何やら変更を通知している風の受信イベントを確認

Docker×NuxtのHMR用に24678のポートフォワーディングを設定するというのはググると結構見つかる情報なのですが、最新の状況はNuxt公式のissueを調べてもわかりませんでした。:sob:

(7/27 15:07追記)
以下のNuxt本体のissueでViteのHMRに関する設定が nuxt.config.ts で効かないという現象についてのやり取りが読めました
https://github.com/nuxt/nuxt/issues/27558

vite.server.hmr の代わりに vite:extendConfig というフックでHMRに関する設定を行うことで、ブラウザなどで閲覧したときのHMRに関する通信が制御できることを確認しました。

nuxt.config.ts
export default defineNuxtConfig({
  hooks: {
    "vite:extendConfig": (config) => {
      if (typeof config.server!.hmr === "object") {
        config.server!.hmr = false;
      }
    },
  },
});

image.png
ちなみに↑の設定では問題の切り分けとしてhmr = falseという設定を行っていますが、これによって完全にHMRに関する通信が無くなるかというとそういうわけでもないみたい?
また、このときのHMRのための通信について24678ポートの使用を試みようとしているようでした。

(7/27 23:43追記)
15:07追記の件について24678:24678,24679:24678のようなポートフォワーディングでのHMRを実現したい場合、具体的にどのような修正が必要かの調査を行いました
なお、ホスト側とコンテナ側のポートが異なるbunコンテナの方で検証を行いました
※実際にはブラウザ等で動作確認を行うための3000番ポートでHMRが動作するようになっているので2024年現在の最新のNuxtではこのような設定は不要の(のはず)。

compose.yml
services:
  node:
    build:
      context: ./
      dockerfile: ./.docker/node/Dockerfile
    volumes:
      - ./node_app:/app:cached
    ports:
      - "3000:3000"
    tty: true

  bun:
    build:
      context: ./
      dockerfile: ./.docker/bun/Dockerfile
    volumes:
      - ./bun_app:/app:cached
    ports:
      - "3001:3000"
+      - "24679:24678"
    tty: true
  • まず、compose.ymlにポートフォワーディングの設定を追加
./bun_app/nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: "2024-04-03",
  devtools: { enabled: true },
+  hooks: {
+    "vite:extendConfig": (config) => {
+      config.server!.hmr = {
+        clientPort: 24679,
+        port: 24678,
+      };
+    },
+  },
});
  • 次にbun_appの方のnuxt.config.tsを修正
  • "vite:extendConfig"のフックにてViteConfigのhmrに関する設定を上書き
    • clientPort としてブラウザからアクセスするポート(ポートフォワーディングにおけるホスト側)を設定
    • port としてコンテナ内のHMRを使用するポート(ポートフォワーディングにおけるコンテナ側)を設定

この修正を設定した状態でbunコンテナのdevサーバーを起動し

  • WebSocket通信が確立できること
  • エディタで修正したファイルを更新時にpage reload(hmr updateという出力ではない)の出力が出ること
  • ブラウザで閲覧中の画面に変更が反映されること

などを確認できました。

image.png

image.png

0
1
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
1