環境
- Windows10 Home 20H2
- WSL2インストール済み
- Docker Desktop for Windows
- VSCode
参考にさせて頂いた記事、サイト
- Docker、ボリューム(Volume)について真面目に調べた - Qiita
- Dockerを使ってNode開発環境を作る
- DockerfileのCMD、docker-compose.yamlのcommandに、複数コマンドを複数行で書く - miuuuu’s blog
当記事のきっかけ
前の記事で、自分なりにdocker使えるような気がし始めてきたところですが、疑問が生じました。
前の記事(※)では、空のnode.jsコンテナがあり、そのコンテナ内でnpm initでpackage.jsonを初期化、順次必要なパッケージをインストールしていき、package.jsonに依存関係を記録していきました。
あらかじめ依存関係を記録したpackage.jsonや初期のコードがあり、それをコンテナに展開させるにはどうするのかを、自分なりに試そうと思いました。
※自分がDockerについて調べて、やっと使えるようになった気がした手順 - Qiita
やりたいこと
- 初期のコード、依存関係が登録されたpackage.jsonが先にある。
- コンテナ生成時に必要なパッケージ、初期コードがコンテナに展開される。
- コンテナを削除しても、ソースは残る。(永続化)
実践
###ファイル構造(ホスト)
(root)
|
|--src/ (←このフォルダ以下をコンテナにマウントする。)
| |--dist/(※distフォルダ以下は、typescriptをトランスパイルした際に作成される。初めは存在しない。)
| | |--server.js
| |
| |--nodo_modules(※npm install実行時にpackage.jsonをもとに展開される。初めは存在しない。)
| |
| |--src/
| | |-server.ts
| |
| |--nodemon.json
| |--package.json
| |--tsconfig.json
| |--webpack.config.json
|
|--docker.compose.yml
dcoker-compose.yml
version: '3'
services:
web-server:
image: node:14
container_name: docker-node-c1
volumes:
- ./src:/app
ports:
- 3000:3000
working_dir: /app
command: >
bash -c "npm install && npm run build && npm run dev"
その他ファイルを準備する
import * as Express from 'express';
const app = Express();
app.get('/', (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
res.send('Hello World.');
});
const portNo = process.env.PORT || 3000;
app.listen(portNo, () => {
console.log(`app running on port ${portNo}.`);
});
export default app;
{
"watch": [
"dist"
],
"ext": "ts, js, json",
"exec": "node ./dist/server.js"
}
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "nodemon -L",
"start": "node ./dist/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.12",
"nodemon": "^2.0.7",
"ts-loader": "^9.2.3",
"typescript": "^4.3.2",
"webpack": "^5.39.0",
"webpack-cli": "^4.7.2",
"webpack-node-externals": "^3.0.0"
}
}
{
"compilerOptions": {
"sourceMap": true,
"noImplicitAny": true,
"module": "es2015",
"target": "es2017",
"jsx": "react",
"lib": ["es2018", "dom"],
"moduleResolution": "node",
"removeComments": true,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictFunctionTypes": false
}
}
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
mode: 'development',
target: 'node',
externals: [ nodeExternals() ],
devtool: 'eval-source-map',
module: {
rules: [
{
loader: 'ts-loader',
test: /\.ts$/,
exclude: [/node_modules/],
options: { configFile: 'tsconfig.json'}
}
]
},
resolve: {
extensions: ['.ts', '.js', '.json']
},
entry: './src/server.ts',
output: {
filename: 'server.js',
path: path.resolve(__dirname, 'dist')
},
node: {
__dirname: false
}
};
コンテナを生成する
ターミナルで以下のコマンドを実行します。
docker compose up
VSCodeのエクスプローラ部を確認すると、srcフォルダにnode-modulesフォルダが生成され、配下にnpm installによりウンロードされたファイルが表示されます。
初期コードのTypeScriptファイルserver.tsがトランスパイルされ、distフォルダが生成され、server.jsファイルが出来上がります。
生成されたコンテナ内でnode.jsサーバが起動します。
ブラウザにて、localhost:3000にアクセスすると、"Hello World."が表示されます。
実験
コードの修正が反映するかの実験
server.tsを修正します。
//...省略...
app.get('/', (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
//res.send('Hello World.');
res.send('hogehoge.');
});
//...省略...
VSCodeで別ターミナルを開き、コンテ内に入りトランスパイルします。
# コンテナ内に入る
docker exec -it docker-node-c1 /bin/bash
# 以下のような表示になる。
root@c34ee48d4afb:/app#
# トランスパイルを実行する。
# nodemonが変更を検知し、node.jsサーバが再起動します。
root@c34ee48d4afb:/app# npm run build
ブラウザをリロードし、表示が"hogehoge."になることを確認します。
コンテナ再起動後も修正が保持されているかの実験
docker compose upを実行したターミナルでctrl+cを押下し、コンテナを停止します。
ブラウザでlocalhost:3000にアクセスすると、
"このサイトにアクセスできません"
等の表示になります。
再度、docker compose upを実行し、コンテナを起動します。
ブラウザでlocalhost:3000にアクセスすると、"hogehoge."と表示され、コンテナにコードの修正が保持されていることが確認できます。
以上です。
奮闘記
ああでもない、こうでもないと、試した記録です。
Dockerfileを使用する
ホストに完成package.json、ソースコードを持ち、それらをコンテ内にコピーしたイメージを考えました。
フォルダ構成
(root)
|
|--src/ (←このフォルダ以下をデータコンテナにマウントする。)
| |--dist/(※distフォルダ以下は、typescriptをトランスパイルした際に作成される。初めは存在しない。)
| | |--server.js
| |
| |--nodo_modules(※npm install実行時にpackage.jsonをもとに展開される。初めは存在しない。)
| |
| |--src/
| | |-server.ts
| |
| |--nodemon.json
| |--package.json
| |--tsconfig.json
| |--webpack.config.json
|
|--Dockerfile
Dockerfile
FROM node:14
# コンテナ内の作業フォルダはapp
WORKDIR /app
# ./srcフォルダをコンテナの/appにコピーする。
# 依存関係が記録済みのpackage.json、その他ソースコードをコピーされる。
COPY ./src ./
# コンテナ内でコピーされたpackage.jsonを元に、パッケージがインストールされる。
RUN npm install
# typescriptをトランスパイルする。
RUN npm run build
# nodemonでnode.jsサーバを起動する。
CMD npm run dev
このDockerfileをもとにイメージ"docker-node-image1"を作成します。
Dockerfileがあるディレクトリで以下のコマンドを実行します。
docker build . -t docker-node-image1
イメージ"docker-node-image1"をもとにコンテナ"docker-node-c1"を作成します。
docker run --name docker-node-c1 -p 3000:3000 docker-node-image1
# コンテナの生成、パッケージのインストール、タイプスクリプトのビルド、nodemonの起動まで行われます。
# 当方のターミナルでは以下のような表示になります。
> app@1.0.0 dev /app
> nodemon -L
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): dist/**/*
[nodemon] watching extensions: ts,js,json
[nodemon] starting `node ./dist/server.js`
app running on port 3000.
ブラウザでlocalhost:3000にアクセスし、コンテナが起動していることを確認します。
続いてコンテナ内に入り、ファイルが展開されているかを確認します。
別のターミナルを開き、以下のコマンドを実行します。
docker exec -it docker-node-c1 /bin/bash
# 以下のような表示になります。
root@932173c904c6:/app#
# dirコマンドでディレクトリを確認します。
root@932173c904c6:/app#dir
# 結果
dist nodemon.json package.json tsconfig.json
node_modules package-lock.json src webpack.config.js
コンテナ内にホストからコピーしたファイルが展開されていることが確認できました。
確認ができたので、このコンテナは削除します。
docker rm docker-node-c1
ここで私は、「よし、あとはコンテナ内のappフォルダにホストのフォルダをマウントすれば成功だ!」と思い、以下のコマンドを実行しました。
# -vオプションの"C:\hoge\fuga"の部分はプロジェクトフォルダのルートです。
# ※-vオプション部分のホスト側のパス部分、フルパスでないと期待した挙動になりませんでした(/srcというような相対パスでは不可でした)。
docker run -p 3000:3000 -v C:\hoge\fuga\src:/app --name docker-node-c1 docker-node-image1
成功を確信していましたが結果はエラーでした。
# 結果
> app@1.0.0 dev /app
> nodemon -L
sh: 1: nodemon: not found
npm ERR! code ELIFECYCLE
npm ERR! syscall spawn
npm ERR! file sh
npm ERR! errno ENOENT
npm ERR! app@1.0.0 dev: `nodemon -L`
npm ERR! spawn ENOENT
npm ERR!
npm ERR! Failed at the app@1.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?
npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2021-07-01T15_54_06_909Z-debug.log
エラーメッセージの
Local package.json exists, but node_modules missing, did you mean to install?
をGoogle翻訳にかけると、
ローカルのpackage.jsonは存在しますが、node_modulesがありません。インストールするつもりでしたか?
とのことです。Dockerfileで「RUN npm install」を指定してたのになぜ?と思いました。
おそらくコンテナ生成時、以下のようなことが起こったのではないかと想像しています。
- イメージdocker-node-image1からコンテナを生成すると、/appディレクトリ内にはnode_modues、distフォルダが存在する。
- docker runコマンドの-v オプションにより、ホストの./srcフォルダの内容がコンテ内の/appに上書きされる。この時、ホスト側の./srcフォルダにはnode_modules、distフォルダはないので、コンテナ内の/appディレクトリのnode_moduels、distも消える。
エラーは発生しましたが、dokcer ps -aコマンドを確認すると、コンテナは生成されていました。
後のために削除します。
# コンテナ一覧確認
docker ps -a
# 結果 エラーは発生したが、コンテナ"docker-node-c1"は生成されている。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5429c0746eb9 docker-node-image1 "docker-entrypoint.s…" 21 minutes ago Exited (1) 21 minutes ago docker-node-c1
# 削除する。
docker rm docker-node-c1
次は、コンテナ生成後、bashを起動し、そこでnpm install等を試しました。
docker run -it -p 3000:3000 -v C:\hoge\fuga\src:/app --name docker-node-c1 docker-node-image1 /bin/bash
# コンテナ生成後、コンテナ内に入り、以下のような表示になります。
root@2cde3c6232ad:/app#
# ここでnpmコマンドを実行します。
root@2cde3c6232ad:/app# npm install
root@2cde3c6232ad:/app# npm run build
root@2cde3c6232ad:/app# npm run dev
ホスト側の./srcフォルダにnode_modules、distが現れ、node.jsサーバも立ち上がり、localhost:3000にアクセスすることができました。
コンテナ停止、再起動、を確認。
# コンテナ停止
docker stop docker-node-c1
# コンテナ再起動
docker start docker-node-c1
# コンテナ内に入り、node.jsサーバを起動
docker exec -it docker-node-c1 /bin/bahs
# 以下はコンテナ内
# nodemonでnode.jsサーバを起動。
root@932173c904c6:/app#npm run dev
期待した挙動になりました。
実は、この部分記事の下書きを書きながら気づきました。これで、当初やりたかったことができてしまいました。
以上を振り返ると、
- Dockerイメージにpackage.json、初期コードを持たせている。
- Docerfileの"RUN npm install"でコンテナ生成時にnpm installを行っている。
- しかし、docker run -vオプションで、ホストのsrcを上書きしている。
ということは、この例では、Dockerfileでソースのコピー等不要でした。
もっと言うと、Dockerfile用いてイメージの作成は不要でした。
Dockerhubに上がっているNode.jsのイメージを用いてコンテナを生成し、ホストのsrcフォルダをマウントすれば、やりたいことができました。
# 先の例(再掲)
# doker run -it -p 3000:3000 -v C:\hoge\fuga\src:/app --name docker-node-c1 docker-node-image1 /bin/bash
# イメージでdocker-node-image1ではなくDockerhub上のイメージ"node:14"を用いる。
doker run -it -p 3000:3000 -v C:\hoge\fuga\src:/app --name docker-node-c1 node:14 /bin/bash
# コンテナ生成後、コンテナ内のbashが起動し、以下のような表示になります。
root@638e25fd74f5:/#
# appディレクトリに移動します。
root@638e25fd74f5:/# cd app
# パッケージをインストールします。
root@638e25fd74f5:/app# npm install
# 以下トランスパイルや、npm start等の実行…省略
# コンテナの削除
docker rm docker-node-c1
当例ではDockerfile用いる必要ありませんでしたが、個人的には実際にDockerfile記述し、挙動を確認、体験してみるという意味では、大変意義がありました。
感想
何度も、コンテナの生成、破棄をコマンドを実行し、行いました。
自分は、ドキュメントを読んで理屈を理解するよりも、実際に実行してみて、感覚を掴むほうが理解が進みました。いや、完全に理解はしていないと思いますが、dockerいいなと思いました。