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 コンテナ名
で停止させておく。