Docker への入門の一環として、自分で Dockerfile を作成し、それを使って Node.js アプリを Docker Container で動かしてみる。
Hello World
Dockerfile を使うことで、既存の Docker Image を編集して新しい Docker Image を作ることができる。
具体的には、Dockerfileという名前のファイルにコマンドを記述していくことで、その内容に基づいた Docker Image を作成できるようになる。
例えば以下の Dockerfile では、FROMとCMDというコマンドを使っている。これらのコマンドの意味は後述する。
FROM node:16 CMD [ "echo", "Hello World" ]
カレントディレクトリに上記のDockerfileがある状態で% docker build -t sample .を実行すると、sampleという名前の Docker Image が作成される。名前はもちろんsample以外でも構わない。
作られたsampleから Docker Container を作成し実行すると、Hello Worldが表示される。ちなみに--rmオプションを付けておくと、Docker Container の停止時にその Docker Container を自動的に削除してくれる。
% docker run --rm sample Hello World
このように、
Dockerfileというファイルにコマンドを記述するDockerfileに基づいて Docker Image を作成する- 作られた Docker Image から Docker Container を作成、実行する
というのが基本的な流れになる。
Dockerfile コマンドを知らないと意図した Docker Image は作れないので、まずはコマンドについて簡単に見ていく。
FROM
ゼロから Docker Image を作ることはまずなく、既存の Docker Image をベースして、そこにコマンドによるカスタマイズを重ねてオリジナルの Docker Image を作ることになる。
そのベースとなる Docker Image を指定するのが、FROMコマンド。
先程はFROM node:16と記述したが、このようにすると公式が配布している Node.js v16の Docker Image がベースになる。
CMD
これは、Docker Container の起動時に実行するコマンドを指定するためのコマンド。
そのため、以下のようにすると、Docker Container の起動時に Node.js のバージョンが表示される。
FROM node:16 CMD [ "node", "--version" ]
実際に試してみる。
まずは Docker Image の作成。
% docker build -t sample .
sampleという Docker Image は先程も作成したので既に存在するが、新しい内容でsampleという Docker Image が改めて作られるので、問題ない。
sampleから Docker Container を作成、実行すると、Node.js のバージョンが表示される。
% docker run --rm sample v16.14.2
Docker Image の後片付け
上述したように、既に存在する Docker Image と同名の Docker Image を作成しようとした場合、特にエラーになることはなく、問題なく作成される。
ではこれまで存在した Docker Image はどうなるかというと、<none>という名前に変化する。
% docker imagesで調べてみると、<none>という名前の Docker Image が作られているはず。
この記事では基本的にsampleという名前で Docker Image を作っていく。
そうすると、<none>という名前の Docker Image がどんどん増えていく。
使うことのない Docker Image が増えても容量を圧迫するだけなので、適宜削除していく必要がある。
ひとつひとつ削除してもいいのだが、以下のコマンドを実行することで<none>という名前の Docker Image を一括で削除できる。
% docker rmi $(docker images -f "dangling=true" -q)
COPY
先程紹介したCMDコマンドは、Dockerfile にひとつしか書くことができない。
では、Docker Container の起動時に複数のコマンドを実行させたい場合は、どうすればいいのか。
シェルスクリプトを使えばよい。シェルスクリプトに実行させたいコマンドを書いておき、CMDコマンドでそのシェルスクリプトを実行することで、複数のコマンドを実行できる。
CMD ["bash", "shell.sh"]
だがそのためには、任意のシェルスクリプト(上記の例だとshell.sh)を、Docker Image に含めないといけない。
そういった時に使うのが、COPYコマンドである。
COPYコマンドを使うと、ホストにあるファイルを Docker Image にコピーすることができる。
まず、以下の内容のshell.shというファイルをカレントディレクトリに用意する。
#!/bin/bash echo 1 echo 2 echo 3
次に、Dockerfile の内容を以下のようにして、% docker build -t sample .でビルドする。
FROM node:16 COPY shell.sh ./ CMD [ "ls" ]
ビルドされた Docker Image から Docker Container を起動するとlsコマンドが実行されるが、既存のディレクトリに加えてshell.shも存在していることが分かる。
% docker run --rm sample bin boot dev etc home lib lib64 media mnt opt proc root run sbin shell.sh srv sys tmp usr var
Dockerfile を以下のように書き換えてビルド、実行すると、shell.shの中身が実行される。
FROM node:16 COPY shell.sh ./ CMD ["bash", "shell.sh"]
% docker run --rm sample 1 2 3
COPYコマンドは、ひとつめの引数でホスト側のパスを指定し、ふたつめの引数で Docker Image のパスを指定する。
ホスト側のパスの基準となるのは、% docker build -t sample .の.の部分。ここで基準となるパスを指定する。
例えば、カレントディレクトリにあるmaterialというディレクトリにa.txt、b.txt、c.txtがある場合、以下のどちらの組み合わせでも、3 つのファイルを Docker Image にコピーできる。
COPY ./material ./と記述し、% docker build -t sample .でビルドする- 基準となるパスが
.であり、COPYコマンドの引数が./materialなので、./material内にあるファイルがコピーされる
- 基準となるパスが
COPY ./ ./と記述し、% docker build -t sample -f Dockerfile ./materialでビルドする- 基準となるパスが
./materialであり、COPYコマンドの引数が./なので、./material内にあるファイルがコピーされる - デフォルトだとビルド時に指定されたパス(このケースだと
./material)にある Dockerfile を探してしまうため、-fオプションで明示的に Dockerfile を指定している
- 基準となるパスが
では、Docker Image のパスの基準は、どのように決まるのか。それを指定するのが、WORKDIRコマンドである。
WORKDIR
WORKDIRコマンドで指定したディレクトリが、Dockerfile でコマンドを実行する際の基準になる。
COPYにしろCMDにしろ、WORKDIRで指定したディレクトリが基準になる。
指定したディレクトリが既存の Docker Image に存在しなかった場合、自動的に作成される。
例えば以下の内容でビルド、実行を行うと、/foo/barと表示される。/foo/barでpwdコマンドを実行したためである。
FROM node:16 WORKDIR /foo/bar CMD [ "pwd" ]
WORKDIRコマンドを使わなかった場合は、これまで見てきたように/が基準のディレクトリとなる。
RUN
Docker Image のビルド時に実行するコマンドを指定するためのコマンド。
CMDは Docker Container の起動時に実行するコマンドを指定するためのものなので、そこが異なる。
また、CMDと違って複数書くことができる。
RUNで実行するコマンドの基準となるディレクトリも、WORKDIRで指定したディレクトリになる。
Node.js でウェブアプリを作る
最後に Dockerfile の実践として、Node.js アプリが起動する Docker Container を作ることにする。
アプリはどんなものでも構わないのだが、TypeScript で書かれた、Hello Worldを返すだけのウェブアプリにする。
まず、Docker は意識せずにアプリを書いていく。
必要なライブラリをインストール。
% yarn init -y % yarn add -D typescript ts-node-dev @types/node@16
tsconfig.jsonを用意。
{ "compilerOptions": { "sourceMap": true, "outDir": "dist", "strict": true, "lib": ["esnext", "DOM"], "esModuleInterop": true } }
index.tsを用意。
import http from "http"; http .createServer(function (_, res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello World\n"); }) .listen(3000);
package.jsonのscriptsに"start": "ts-node-dev index.ts"を書き加える。
この状態で% yarn run startを実行するとウェブサーバが起動するはずなので、curl で動作確認してみる。
% curl http://localhost:3000 Hello World
問題なく動いているので、ここから、Docker で動かしていく。
Dockerfile
まずは Dockerfile を書く。
FROM node:16 WORKDIR /scripts COPY package.json ./ COPY yarn.lock ./ COPY tsconfig.json ./ RUN npm i -G yarn RUN yarn install --frozen-lockfile COPY index.ts ./ CMD ["yarn", "run", "start"]
必要なファイルをコピーしたあと、ライブラリのインストールを行う。
その後index.tsをコピーし、Docker Container の起動時にyarn run startが実行されるようにする。
Docker Image のビルドと Docker Container の起動
ビルドは今まで通り% docker build -t sample .でよい。
Docker Container の起動は、以下のようにする。
% docker run --rm -d -p 8080:3000 sample
-dで、Docker Container がバックグラウンドで実行されるようにする。
そして-p 8080:3000で、ホスト側の8080ポートと Docker Container 側の3000ポートをマッピングする。
これで、ホストの8080ポートを通して、Docker Container の3000ポートにアクセスできる。
% curl http://localhost:8080 Hello World
最後に、このままだとずっと Docker Container が起動したままなので、% docker psで Docker Container で名前を調べ、% docker stop コンテナ名で停止させておく。