Dockerで開発環境を作成するメモ2

Dockerの開発環境作成メモの2つ目。Dockerfileについてのメモ。

Dockerfile のベストプラクティス
http://docs.docker.jp/engine/articles/dockerfile_best-practice.html

前回ではalpineイメージを作成して、OSのコンテナ展開したり、コンテナ内に入ったりする例をさらっと記載しました。今回は開発環境としてNode.jsを題材としてもう少し自分なりの実践的な使い方をまとめておきます。

Node.jsのようなミドルウェアをコンテナ化する理由としては、自分としてはバージョンの問題だと思ってます。

今動作しているアプリはその当時の最新バージョンだったりしますが、時が経つごとに古くなってきます。新しいバージョンと入れ替えたくなりますが、切り替えることにより動作しなくなるアプリも出てくるので、結局古いまま使用していることがモヤモヤしてしまいます。

ミドルウェアをコンテナ化して、ローカルで管理しているソースやデータを永続データとしてコンテナにマウントして実行できれば、アプリケーション毎にバージョン切り替えが簡単になるはずです。もし新しいバージョンのコンテナだと動作しない場合、対策とるまではそのアプリだけ古いバージョンのまま利用することも簡単になるはずです。

このあたりをまずはゴールとしてどうしたらいいのか検討&メモっていきます。

そもそもミドルウェアのイメージってどうなっているのか?

現時点でのNode.jsの最新安定化バージョンは12.13.1です。Docker Hubを見ていると、PC版とArm版などがあります。ただ同じバージョンでもstretchbusterやらいろいろあります。これはDebianのバージョン名で、stretchがDebian9(2020年6月まで)、busterがDebian10(2024年?)のことです。

例えば、node : 12.13.1-stretchnode : 12.13.1-busterの詳細を見ると以下になります。

# 12.13.1-stretch
ADD file:152359c10cf61d80091bfd19e7e1968a538bebebfa048dca0386e35e1e999730 in / 
:
COPY file:238737301d47304174e4d24f4def935b29b3069c03c72ae8de97d94624382fce in /usr/local/bin/ 
# 12.13.1-buster
ADD file:9b7d9295bf7e8307ba4e81ce20770256b964da70dea966568b3515ad026d0b27 in / 
:
COPY file:238737301d47304174e4d24f4def935b29b3069c03c72ae8de97d94624382fce in /usr/local/bin/ 

/usr/local/binにコピーしている箇所は同じで、おそらくNode.js関連のモジュール群だと思います。ADDしている箇所のIDが異なりここがDebianの最小構成のモジュール群であり、バージョンが違うので、IDが異なっているんだと思います。

確認するために今度は、debianのイメージの詳細を見てみます。2行しかないです。

# stretch
ADD file:152359c10cf61d80091bfd19e7e1968a538bebebfa048dca0386e35e1e999730 in / 
CMD ["bash"]
# buster
ADD file:9b7d9295bf7e8307ba4e81ce20770256b964da70dea966568b3515ad026d0b27 in / 
CMD ["bash"]

ほかにもslim版を見てみると、やはりIDは一致します。つまり、Node.jsのイメージの中身はDebianの最小限のモジュールとNode.jsのモジュールで構成されていることがわかります。これらのイメージからコンテナに展開すると、コンテナ内にはDebianとNode.jsだけの最小環境が展開されることがわかります。

ちなみにslim版と通常版のイメージの違いは、内部でapt-get updateをやるかやらないかの違いだと思います。

Node.js用開発環境のイメージを作成する

> cd node-dev
> ls
.dockerignore  Dockerfile test.js

.dockerignoreはDockerfileからイメージをビルドするときに余計なフォルダやファイルが混入しないようにするための定義ファイルです。.gitignoreと同じようなものです。

#.dockerignore
node_modules
npm-debug.log

test.jsはまずは実行すると、標準出力に1行出力するだけのソースとします。

console.log("hello node-dev");

Dockerfileはまずは以下とします。node:12.13.1-alpineのイメージから作成して、ソースtest.js/srcフォルダを作成してコピーするだけです。

FROM node:12.13.1-alpine
WORKDIR /src
COPY ./test.js ./
> docker build -t node-dev:1.0 .
:
Sending build context to Docker daemon  8.704kB
Step 1/3 : FROM node:12.13.1-alpine
 ---> 3fb8a14691d9
Step 2/3 : WORKDIR /src
 ---> Running in d3bc9d74160f
Removing intermediate container d3bc9d74160f
 ---> be1f3e9150cd
Step 3/3 : COPY ./test.js ./
 ---> ca7e16c4ee23
Successfully built ca7e16c4ee23
Successfully tagged node-dev:1.0

イメージを確認

> docker images
REPOSITORY          TAG       IMAGE ID            CREATED             SIZE
node-dev            1.0       ca7e16c4ee23        23 seconds ago      80.2MB

コンテナ展開を実行すると、最後にコンテナ内部でnodeが実行されることがわかる。

> docker run -it --name dev1 node-dev:1.0
Welcome to Node.js v12.13.1.
Type ".help" for more information.
>

いったん終了してコンテナを確認すると、コンテナが作成されて停止していることがわかる。docker start dev1で起動することはもちろんできる。

> docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                     PORTS               NAMES
83b9242bd9e0        node-dev:1.0        "docker-entrypoint.s…"   About a minute ago   Exited (0) 3 seconds ago                       dev1

docker runを実行するときに、後ろにnodeのコマンドを指定すると、node {ソースファイル}を実行するコンテナが作成される。(コンテナはプログラムを実行し終わると停止する)

> docker run -it --name dev2 node-dev:1.0 test.js
hello node-dev
> docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
71c65b658be1        node-dev:1.0        "docker-entrypoint.s…"   11 seconds ago      Exited (0) 10 seconds ago                       dev2

docker startでこのコンテナを再度開始すると、node test.jsが再度実行されることになる。(バックグラウンドで)

> docker start dev2
> docker logs dev2
hello node-dev   <-- docker run
hello node-dev   <-- docker start

最後にnodeを実行するのではなく、シェルを開始したい場合は、使用するシェルを指定してCMDを上書きしてしまえばいい。また、コンテナを使い捨てしたい場合は、--rmを付けるとプロセス終了時に停止したコンテナが自動削除される。

> docker run -it --rm node-dev:1.0 /bin/ash
/src # node --version
v12.13.1
/src # exit

> docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
(停止したコンテナすら存在しない)

継続して使い続けるのであれば、--rmを付与せずにコンテナ作成する。-dも付与してバックグラウンドで実行しても良い。

> docker run -it -d --name dev4 node-dev:1.0 /bin/ash
c8271bae35b9b3a9c10741103ff3d930e9e4c7c7d4ec2cf397ce292d1f718ce4
> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
c8271bae35b9        node-dev:1.0        "docker-entrypoint.s…"   5 seconds ago       Up 5 seconds                            dev4

コンテナ内に入って、nodeプログラムを実行する。コンテナから外に出て、プロセスを確認すると、コンテナが起動したままになる。(バックグラウンド実行しているので)

> docker exec -it dev4
/src # node test.js
hello node-dev
/src # exit
> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
c8271bae35b9        node-dev:1.0        "docker-entrypoint.s…"   2 minutes ago       Up 2 minutes                            dev4

コンテナを停止する場合はdocker stop、開始してまたプログラムを実行したければdocker startする

> docker stop dev4
> docker start dev4
> docker exec -it dev4 /bin/ash

次にDockerfileを以下のように、最後にCMDで、このプログラムを実行するところまで記述するとどうなるのか?

FROM node:12.13.1-alpine
WORKDIR /src
COPY ./test.js ./
CMD [ "node", "test.js" ]
> docker build -t node-dev:1.1 .
 docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
node-dev            1.1                 fdfbeb24b0f7        5 seconds ago       80.2MB
node-dev            1.0                 ca7e16c4ee23        11 minutes ago      80.2MB

test.jsが実行されるコンテナが作成される。実行されてると停止しているのがわかる。

> docker run -it --name dev5 node-dev:1.1
hello node-dev
> docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
4e8cb429b4dd        node-dev:1.1        "docker-entrypoint.s…"   7 seconds ago       Exited (0) 6 seconds ago                       dev5

開始するとまたプログラムは実行されるがすぐに停止するので、中に入れないことは留意しておく。

> docker start dev5
dev5
> docker logs dev5
hello node-dev
hello node-dev

プログラムを開始するだけのコンテナであれば、DockerfileのCMDにプログラムを開始する指定をすればいいが、開発環境を提供するコンテナであれば最後はシェルを起動しておくのが良さそう。