後発の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
というイメージを使用しました
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
というイメージを使用しました
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は以下のような内容となっています
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)のレイヤー情報です。
最後のCMD命令によってbun(/usr/local/bin/bun)が実行されており、ログの情報と状況は合致します。
というわけでCMD命令が原因落ちていることが分かったのでコンテナでの開発を行うために、何かしらコンテナが落ちないようにするCMD命令の上書きを行います。
今回は以下のようにしました。
FROM oven/bun:1.1.20-debian
ENV TZ Asia/Tokyo
WORKDIR /app
# uid=1000
USER bun
+
+CMD ["sh"]
Dockerfileを修正後、改めてコンテナ環境を起動しなおし今度はbunイメージを使用したコンテナ環境も落ちていないことを確認できました
$ 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)のレイヤー情報です。
こちらも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コンテナは落ちなかったということみたいです。
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というサブコマンドから対話型のセッションを開始できるとありました。
今回の記事の中ではbunコンテナを落ちないようにする施策として CMD ["sh"]
の設定を行いましたが、nodeコンテナと同じ要領で行くのであれば以下のような形もアリかも?
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を動かしてみます
なお、作業開始の前にnodeとbunの両コンテナについてブラウザからの動作確認で必要となるポートフォワーディングの設定を実施しています。
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)を使用したインストールを行いました
$ 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を使用したインストールが可能なので以下のような作業となりました。
$ 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くらい(ものすごく速い)
- 最初のパッケージマネージャの選択で
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
- 正常に起動できることが確認できました
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 でアクセスできるということではない)
- こちらも正常に起動できることが確認できました
- bunコンテナの方は3001:3000のようなポートフォワーディングを行っているので、app.vueに適当な修正を加えhmr updateが機能することも確認しました
備考
今回の検証について直接関係ないことや、いまいちよくわかってないことについて書いています。
HMRの件は特に勉強不足感がある(要viteの勉強)。
bunでのパッケージ管理についてyarn.lockを利用
bun.lockb はバイナリで中身のテキストを確認する方法はあるのですが(bun bun.lockb
)、確認したいときに都度コマンドを打つのはやや面倒な気がしました。
この件についてBunは bun.lockb と合わせて yarn.lock を生成することが出来るのでその設定を行いました。
※この設定によって bun.lockb が生成されなくなるわけではないのでlockファイルを2重に管理するようなことが無いように .gitignore の設定も行います
[install.lockfile]
# whether to save a non-Bun lockfile alongside bun.lockb
# only "yarn" is supported
print = "yarn"
- 新規で bunfig.toml というファイルを作成
~~~省略~~~
# 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 でのポートフォワーディングの設定について以下のような記述を行っていました。
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サーバーを起動したときの様子
- app.vueについてウェルカムのコンポーネントを削って保存し、hmr updateという出力が流れることを確認
- ブラウザで http://localhost:3001 にアクセスしたときの様子
- ws(WebSocket)の接続が行えていることと、エディタ側で更新したapp.vueについて何やら変更を通知している風の受信イベントを確認
Docker×NuxtのHMR用に24678のポートフォワーディングを設定するというのはググると結構見つかる情報なのですが、最新の状況はNuxt公式のissueを調べてもわかりませんでした。
(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に関する通信が制御できることを確認しました。
export default defineNuxtConfig({
hooks: {
"vite:extendConfig": (config) => {
if (typeof config.server!.hmr === "object") {
config.server!.hmr = false;
}
},
},
});
ちなみに↑の設定では問題の切り分けとしてhmr = falseという設定を行っていますが、これによって完全にHMRに関する通信が無くなるかというとそういうわけでもないみたい?
また、このときのHMRのための通信について24678ポートの使用を試みようとしているようでした。
(7/27 23:43追記)
15:07追記の件について24678:24678,24679:24678のようなポートフォワーディングでのHMRを実現したい場合、具体的にどのような修正が必要かの調査を行いました
なお、ホスト側とコンテナ側のポートが異なるbunコンテナの方で検証を行いました
※実際にはブラウザ等で動作確認を行うための3000番ポートでHMRが動作するようになっているので2024年現在の最新のNuxtではこのような設定は不要の(のはず)。
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にポートフォワーディングの設定を追加
// 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という出力ではない)の出力が出ること
- ブラウザで閲覧中の画面に変更が反映されること
などを確認できました。