【Windows環境】DockerでNode+Express+postgres+heroku CLIの爆速コンテナを作る~ロングストーリー
Docker、ややこしいねえ。書いてある通りにやってもうまくイカン事多いネ。WindowsでやろうとするとWSLが絡むから尚ややこイ。自分なりに試行錯誤してあくまで自己流ですんで自己責任で参考にしてください。
Windows+WSL環境でデフォルトでコンテナ作成するとパフォーマンスがめっちゃ悪い
まず、普通にWindows上のどこかに開発環境(プロジェクトレポジトリ)を置いてそれをDockerコンテナ化するとパフォーマンスがめっちゃ悪い。さらにビルドされたDockerイメージのサイズが巨大
( VSCodeから「新しい開発コンテナの作成」メニューでコンテナ作るとこうなるヨ。それとも裏技あるのだろうか…?)
簡単に言うとWindows上のファイルシステムはWSL Ubuntuでは /mnt/Ubuntu/c/*
としてマウントされており、そのファイルシステムがWSL Ubuntu自身のファイルシステムとは違うから。
Windows上のどこか、例えばC:¥Users¥[ユーザ名]¥app_project
に開発プロジェクトを置くと、それはWSLから見た場合、/mnt/Ubuntu/c/[Users/ユーザ名]/app_project
になる。つまりマウント先になるわけだ。ところがDockerコンテナはデフォルトでWSLの\var\lib\docker
あたりに置かれる。つまりUbuntu自分自身のファイルシステム上に置かれる。
ということは、Windows上で開発しながらC:¥Users¥[ユーザ名]¥app_project
配下のコードを作成・編集するってことは、WSL Ubuntuから見ると/mnt/Ubuntu/c/[Users/ユーザ名]/app_project
配下と\var\lib\docker
配下のファイル群をシンクロさせ続けることになる。
/mnt/Ubuntu/c/[Users/ユーザ名]/app_project
はマウントしたWindowsファイルシステムだからNTFSだ。一方で\var\lib\docker
はUbuntu上のファイルシステムだからEXT4だ。NTFSとEXT4という異なるファイルシステム同士でシンクロし続けようと頑張るのでめっちゃパフォーマンスが悪いというわけです。
解決方法
開発レポジトリをWindows上のどこか(例:C:¥Users¥[ユーザ名]¥app_project
)ではなく、WSLのUbuntu上に置いてしまおう。
例えば、/home/[ユーザ名]/Projects
などである。
このWSL Ubuntuのフルパスは、Windowsからは\\wsl.localhost\Ubuntu\home\[ユーザ名]\Projects
として見ることができる。エクスプローラーからでもアクセスできる。
次のように。
\\wsl.localhost\Ubuntu\home\atom\Projects
結論から言うと、開発プロジェクトをホストもゲストもWSL Ubuntuの中だけで完結してしまおう、ということになる。
Ubuntuの中の開発レポジトリ/home/[ユーザ名]/Projects
と、同じくUbuntuの中のDockerコンテナが配置される\var\lib\docker
とのシンクロになるので、これでパフォーマンスは爆速になる。
何だったらWindowsホストマシンを汚したくないだけ、という目的ならDocker関係なく「開発はWSL Ubuntuで」という理由だけでWSLの/home/[ユーザ名]配下にプロジェクトルートを置いても良いかもしれない。VSCodeでWSL Ubuntuのフォルダにアクセスできるし。
※ 参考:Windows + WSL2 + docker + laravel を 10 倍速くする方法
Macの場合こういう悩みがあるのかまったく不明です。ご面倒でなければコメント下さい・・・
じゃあDockerコンテナを作ろう
Dockerファイル
FROM node:20-alpine3.18
# 必要なパッケージのインストールとアップデート
RUN apk update \
&& apk add --no-cache \
git \
RUN rm -rf /var/cache/apk/*
docker-compose.ymlファイル(イメージ名やコンテナ名をexpress-testとしてあるのはあくまで例です)。
#docker-compose.yml
version: "3"
services:
node:
build:
context: .
dockerfile: Dockerfile
image: express-test
container_name: express-test
volumes:
- ./app:/usr/src/express-test
ports:
- "3000:3000"
stdin_open: true
environment:
- WATCHPACK_POLLING=true
user: 1000:1000
Dockerファイル。FROMセクションはnode:20-alpine3.18のように軽量Linuxを示すalpineを付けた方が良い(3.18というのはただのバージョン番号)。bullseye, slimでもいいけど。よく単にnode:latestとかnode:18とかしてる事例を見るけどイメージサイズがギガレベルになる。alpineやbullseye付ければ150MB程度のイメージサイズになる(alpineがRedHat系でbullseyeがDebian系、だったと思う)
docker-compose.ymlのvolumes:セクションを見てほしいが、デフォルトのコンテナ配置場所\var\lib\docker
だと永続化できないので、/usr/src/
配下をvolumes:で永続化&コンテナ配置場所(ワーキングディレクトリ)に指定している。
stdin_open, environmentの- WATCHPACK_POLLING, user: 1000:1000の設定の解説はChatGPTに聞いて。
コンテナの作成方法
【前提条件】
あくまでWSL+UbuntuのインストールおよびDockerデスクトップのインストールが終わっていること。
参考:WSL2+ubuntu20.04: GUI化して使う方法
参考:Windows上でDocker環境を作成する方法とその構成
では始めます。
-
\\wsl.localhost\Ubuntu\home\atom\Projects
配下にプロジェクトルートフォルダを作成する(例:\\wsl.localhost\Ubuntu\home\atom\Projects\express-test
) - その中にDockerファイル、docker-compose.yml、.dockerignoreファイル、appフォルダ(中身カラ)を置く。
- .dockerignoreファイルは以下の一行書くだけ
node_modules
- WSL Ubuntuでプロジェクトルートに移動して下記のコマンド。
docker-compose up -d
- イメージがビルドされコンテナが立ち上がる。
-
docker ps
コマンドでコンテナが立ち上がってるのが確認できるはず。✔ Container LINEbot-express Running
じゃあnodeコンテナにExpress等を入れてみよう。
ここからはVSCodeを使う。
まずVSCodeでコンテナにアタッチ
VSCodeで「実行中のコンテナにアタッチ」すればdocker container exec -it express-test bash
コマンドでコンテナ内に入るのと同じことで、VSCodeのターミナルがコンテナ内に入ってる状態になる。そのままindex.jsファイルを作るとかpackage.jsonを編集するとか同時にできるので作業しやすい。
-
VSCodeを立ち上げる。
-
VSCodeの左下の青い><アイコンをクリック。
-
「実行中のコンテナにアタッチ」を選択。
-
作成したコンテナの名前が出てくるのでそれを選択する。
-
もしここで「ワークスペースがない」みたいなこと言われたら、docker-compose.ymlのvolumes:セクションで指定したワークスペース(例:/usr/src/express-test)を入力する。
-
30秒くらいかけてVSCodeでコンテナが開く。
-
ご覧の通り、コンテナを開いた状態。ターミナルのパスをご覧あれ。
-
ここではプロジェクトルートがLINEbot-expressという名前だが、Volume:セクションで指定したコンテナワークスペース
/usr/src/LINEbot-express
が開けているということ。
興味があればWSL Ubuntuでls /usr/src
コマンドを打ってみてほしい。中には何もない。Volume:セクションで指定したパスはあくまでコンテナの中のワークスペースだということだ。OS(Ubuntu)から覗いてみてもコンテナの実体は見れない。
コンテナの中でnpm init, expressとnodemonのインストール
- VSCodeのターミナルで
npm init -y
コマンド - package.jsonが出来るので開く。
- "scripts"セクションで次のコード
"start": "node index.js",
を追加しておく。"scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" },
-
npm install express
でExpressをインストール。 - 同じ階層で良いからindex.jsを作成し、以下のコードを入力。
const express = require('express') const app = express() const port = 3000 app.get('/', (req, res) => { res.send('Hello World!') }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
- ターミナルで
npm start
コマンドを打つ。 - ブラウザを開きhttp://localhost:3000/をロードする。「Hello World!」と画面に表示されるはず。
- このままだとindex.jsに変更を加えるたびにいちいちnode index.jsを再起動しなければならない。そこで
npm install nodemon
でnodemonをインストールしpackage.jsonの"start": "node index.js",
を"start": "nodenon index.js",
に変更。"scripts": { "start": "nodemon index.js", "test": "echo \"Error: no test specified\" && exit 1" },
-
そしたらターミナルで
npm start
する。 -
今度はnodemonが立ち上がりnodemonがjsファイルやjsonファイルを監視してるよーというログが黄色文字でlog出力され最後に緑文字でnodemonが
node index.js
を実行とlogってるのが確認できるはず。 -
index.jsの「Hello World!」の箇所を「こんにちは!」に変えてみる。
-
nodemonがすぐに再起動するのが分かる(一瞬で終わる)。
-
ブラウザをリロードすると「こんにちは!」という表示に変わっているはず。
これでnode.js+Express環境が整った。
じゃあ次にPostgresコンテナを作成しよう。
あまりやる事ない。
ほとんどdocker-compose.ymlを編集するだけ。以下は変更済みdocker-compose.ymlファイル。
version: "3"
services:
node:
build:
context: .
dockerfile: Dockerfile
image: linebot-reserve-image
container_name: LINEbot-express
volumes:
- ./app:/usr/src/LINEbot-express
ports:
- "3000:3000"
stdin_open: true
environment:
- WATCHPACK_POLLING=true #
-
user: 1000:1000
########## ここから下のコードを追加 ###########
depends_on:
- postgres
postgres:
image: postgres:16-alpine3.18
container_name: LINEbot-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: app_db
ports:
- "5432:5432"
################### ここまで ###################
- Node.jsのコンテナは
depends_on:
を使用してPostgreSQLのコンテナが起動してから実行されるようにしている。 - docker-compose.ymlを上記のように編集したら
docker-compose up -d
コマンドでもう一度コンテナを上げなおす。この過程でpostgresイメージも作成される。Postgres用Dockerファイルを用意しても良いが、しなくてもよい(postgres:16-alpine3.18をDockerHubから直落としすることになるだけ)。 - データの永続化のためにボリュームを設定しなくてもいいのかと思ったが、PostgreSQLのDockerイメージでは、デフォルトでデータディレクトリがボリュームにマウントされており、データの永続化が行われている。したがって、特別なボリューム設定を行わなくてもデータはコンテナの外部に永続的に保存されるらしい(ChatGPT調べ)。
- さて、
docker ps
でnodeコンテナとpostgresコンテナが立ち上がってることを確認できたら、WSLで次のコマンドを打ちpostgresコンテナに潜入する。 docker container exec -it LINEbot-postgres psql -U postgres
- プロンプトが
postgres=#
という表示に変わる。postgres=#
- ここでpsqlメタコマンドを打つことができる。
- \l でデータベース一覧を表示
- \c [データベース名]でデータベースに接続
- テーブルを作成したい場合、
-
select current_schema;
でデフォルトスキーマを選択 - そしてテーブル作成してみる。
create table users (id integer, name varchar(10));
- \dt でテーブル一覧表示
-
insert into users VALUES (1, 'Atom');
でデータをテーブルに入力。 -
select * from users;
でテーブルのデータを確認できる。 - \q でpostgresコンテナから抜ける。
これでnode.js+Express+postgres環境が整った。
NodeコンテナからPostgresコンテナに接続テストしてみよう
簡単です。
-
ルートディレクトリにtest.jsなど適当にファイルを作成。
-
test.jsに次のようにコーディング。
const express = require('express'); const { Pool } = require('pg'); const app = express(); const port = 3000; // PostgreSQL接続情報 const pool = new Pool({ user: 'postgres', // データベースユーザー host: 'LINEbot-postgres', // Postgresコンテナのホスト名 database: 'app_db', // データベース名 password: 'password', // データベースパスワード port: 5432, // データベースのポート }); // ユーザーテーブルからデータを取得 app.get('/', async (req, res) => { try { const result = await pool.query('SELECT * FROM users'); res.json(result.rows); } catch (error) { console.error('Error executing query', error); res.status(500).json({ error: 'Internal Server Error' }); } }); app.listen(port, () => { console.log(`Server is running on port ${port}`); });
- package.jsonの
"start": "nodemon index.js",
を"start": "nodemon test.js",
に変更。 - VSCodeのターミナルで
npm start
- ブラウザでhttp://localhost:3000を開く。usersテーブルのデータがJSON形式で表示されているはず。
コンテナをチームで共有
こうして構築したコンテナをチームで共有したかったらdockerhubにプッシュしておけば良い。Expressやpgのバージョン指定があったらコンテナ構築段階で指定してインストールすればよろしい(例:npm install pg@7.0)
参考:コンテナイメージをDockerHubにpushして共有する
以上
後述
alpineは軽量ゆえいろいろとよく使う系ツールが入っていないことが分かった。curlもない。bashもない。glibcもsudoもなんもない。そのためDockerファイルを以下のように追記修正した。
FROM node:20-alpine3.18
# 必要なパッケージのインストールとアップデート
RUN apk update \
&& apk add --no-cache \
git \
curl \ # ← 追記
bash \ # ← 追記
sudo \ # ← 追記
postgresql-client \ # ← 追記
# 追加のパッケージをここに列挙する
# 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
Dockerファイルを変更したら現存するコンテナ・イメージを削除してdocker-compose up -d
コマンドを再度打つべし。
heroku CLIは手動でインストールするしかなかった。
heroku CLIのインストール方法:
- WSL Utuntuにてコンテナに入る(root権限で。-u 0 がそれを意味する)。
コマンド:docker exec -it -u 0 LINEbot-express /bin/sh
- heroku CLIをインストールする。
コマンド:curl https://cli-assets.heroku.com/install.sh | sh
また開発途中でいろいろ必要になってくるかもしれない。
「# 追加のパッケージをここに列挙する」
とあるように必要なものが出てきたらここに列挙し、コンテナとイメージを削除した後、docker-compose up -d
コマンドを打つべし。
例えばaws CLIインストールの場合:
apk add aws-cli
なので
FROM node:20-alpine3.18
# 必要なパッケージのインストールとアップデート
RUN apk update \
&& apk add --no-cache \
git \
curl \
bash \
sudo \
aws-cli \ # ← 追記
# 追加のパッケージをここに列挙する
# 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
例えばfirebase CLIインストールの場合:
npm install -g firebase-tools
なので、
FROM node:20-alpine3.18
# 必要なパッケージのインストールとアップデート
RUN apk update \
&& apk add --no-cache \
git \
curl \
bash \
sudo \
aws-cli \
postgresql-client \
# 追加のパッケージをここに列挙する
RUN npm install -g firebase-tools # ← 追記
# 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
※ しかしnpm install firebase-toolsはコンテナ内で実行できるだろう。たぶん。
heroku CLIも自動インストールさせる
毎回Dockerを立ち上げるたびにheroku CLIを手動でインストールさせられるのはつらいので以下のようにDockerファイルを書き換えた。
FROM node:20-alpine3.18
# 必要なパッケージのインストールとアップデート
FROM node:20-alpine3.18
# 必要なパッケージのインストールとアップデート
RUN apk update \
&& apk add --no-cache \
git \
curl \
sudo \
bash \
postgresql-client \
# 追加のパッケージをここに列挙する
# 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
# heroku CLIをインストール
RUN adduser -D heroku \
&& echo "heroku ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers \
&& su - heroku -c 'curl https://cli-assets.heroku.com/install.sh | sh' \
&& addgroup heroku root
ただ単純に
RUN curl https://cli-assets.heroku.com/install.sh | sh
を追加するだけだとエラー、エラー、エラー・・・。
Claudeに助けてもらったヨ(ChatGPTより賢い!)
以上