【Docker】コンテナでNode.jsを実行してみた

前の記事に引き続き、今回はコンテナでNode.jsのアプリケーションを動かすことに挑戦してみました。

https://qiita.com/suzuki0430/items/b2527cfba7602f9fe3bc

![スクリーンショット 2021-03-06 13.44.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c2407b87-859c-85a7-36d5-2ab9843bac09.png)

#実装
実装は以下の手順で行いました。

1. Node.jsアプリの作成
2. Dockerfileの作成
3. Dockerfileのビルド
4. コンテナの起動
5. ブラウザとアプリの接続


##Node.jsアプリの作成
`localhost:8080`を開いたときに`Hi there`がブラウザに表示されることを目指します。

`package.json`と`index.js`の中身は以下のようにします。

```package.json
{
  "dependencies": {
    "express": "*"
  },
  "scripts": {
    "start": "node index.js"
  }
}
```

```index.js
const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.send('Hi there');
});

app.listen(8080, () => {
  console.log('Listening on port 8080');
});
```
##Dockerfileの作成
Dockerイメージをビルドしたとき、ファイルシステムにアプリに必要なプログラム(dependencies)をインストールし、メタ情報にアプリの起動コマンド(`npm start`)を導入することを目指します。

<a href="https://qiita.com/suzuki0430/items/b2527cfba7602f9fe3bc">前の記事</a>でredisをコンテナで立ち上げたときのように、試しにalpineのベースイメージを使ってDockerfileを作成してみます。

```dockerfile
FROM alpine

RUN npm install

CMD ["npm", "start"]
```

**これをビルドすると、`RUN npm install`の部分でこけます。**
なぜかというと、alpineイメージは軽量であるがゆえに、`node`も`npm`もプリインストールされていないからです。

そのため、ベースイメージには`node`や`npm`がプリインストールされているnodeイメージなどを選択する必要があります。
ただ、nodeイメージにはnpmやnode以外にもテキストエディタツールなどの余計なプログラムもプリインストールされているので、軽量なalpineに`npm`と`node`がプリインストールされた`node:alpine`や`node:slim`などの最適化されたベースイメージを使うのがベストプラクティスです。

ベースイメージを`node:alpine`に変えて再度ビルドしてみます。

```dockerfile
FROM node:alpine

RUN npm install

CMD ["npm", "start"]
```

**このビルドもこけます。**
なぜなら、`node:alpine`のファイルシステムに`package.json`が含まれておらず、インストールするプログラムが不明だからです。
もしたまたまハードドライブに`package.json`が含まれていたとしても、コンテナに割り当てられたハードドライブのセグメントにファイルがあるわけではないので、`npm install`は実行できません。

ここでDockerfile内に、`RUN`の前にファイルコピーするための記述`COPY`を加えます。
`COPY ./ ./`とすることで、ローカルのカレントディレクトリのファイル(package.json, index.js)をコンテナ内のファイルシステムにコピーすることができます。


```dockerfile
FROM node:alpine

WORKDIR /usr/app

COPY ./ ./
RUN npm install

CMD ["npm", "start"]
```
*`WORKDIR /usr/app`という記述がありますが、これはファイルのコピー先(ワーキングディレクトリ)を指定しています。詳細は後述します。

**これでようやくビルドが成功します。**

```
(base) [9:42:15] → docker build -t suzuki0430/simpleweb:latest .                                                                               ~/Programs/docker/simpleweb
Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM node:alpine
 ---> b3dce3e0529f
Step 2/5 : WORKDIR /usr/app
 ---> Using cache
 ---> d995d6f0fd23
Step 3/5 : COPY ./ ./
 ---> 8b999fe7dc6e
Step 4/5 : RUN npm install
 ---> Running in 52b539c857e3

added 50 packages, and audited 50 packages in 3s

found 0 vulnerabilities
npm notice
npm notice New minor version of npm available! 7.0.8 -> 7.6.1
npm notice Changelog: <https://github.com/npm/cli/releases/tag/v7.6.1>
npm notice Run `npm install -g npm@7.6.1` to update!
npm notice
Removing intermediate container 52b539c857e3
 ---> 5f7e4d45df44
Step 5/5 : CMD ["npm", "start"]
 ---> Running in e8085a051d01
Removing intermediate container e8085a051d01
 ---> e76e6a91dc2b
Successfully built e76e6a91dc2b
Successfully tagged suzuki0430/simpleweb:latest
```
コンテナの作成・起動を行います。

```
(base) [9:43:11] → docker run suzuki0430/simpleweb                                                                                             ~/Programs/docker/simpleweb

> start
> node index.js

Listening on port 8080
```
コンテナの起動も正常にできたみたいなのでこれで成功！といきたいところなのですが、**ブラウザからアプリが開けません...えっ？**
![スクリーンショット 2021-03-06 10.26.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ff2ebd2f-bc00-828c-590d-47407420e49b.png)

###ポートマッピング
アプリが開けなかったのは、コンテナのポート8080とローカルホストのポート8080が接続されていなかったためです。
ブラウザから`localhost:8080`にアクセスする際、localhost(ローカルPC)のポートに接続しようとします。
ただ、今回はコンテナの中でNode.jsのアプリケーションを実行しているため、コンテナのポートと接続する必要があります。
![スクリーンショット 2021-03-06 14.30.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c2f69ea8-155d-a469-1ec9-22294ac3fe7f.png)

ローカルPCとコンテナのポートを紐付けることをポートマッピングと呼びます。
ポートマッピングを実行するためには`docker run -p 8080:8080 [image id]`コマンドを実行します。
コンテナの作成・起動とポートマッピングを同時に実行してくれます。

**これで開けるようになりました！**
![スクリーンショット 2021-03-06 10.43.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/cc18e5d3-c910-5038-60f7-6fae7ad722a2.png)

###キャッシュの利用
アプリは開けるようになったのですが、気になる部分が1つあります。
それは今のDockerfileの内容だと、ファイルを更新してイメージをビルドする際に`npm install`が必ず実行されてしまうことです。
最終的にビルドは完了するので問題がないといったらないのですが、ビルドの待ち時間はできるだけ避けたいものです...

**そこでキャッシュの利用を考えます。**

Dockerfileを`RUN npm install`の前に`package.json`のみをコピーするように書き換えます。

```dockerfile
FROM node:alpine

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install
COPY ./ ./

CMD ["npm", "start"]
```
すると、`package.json`の中身が同じであれば、`RUN npm install`までの手順をキャッシュによってスキップすることができるようになります。

```
Step 3/6 : COPY ./package.json ./
 ---> Using cache
 ---> 1e7f34ded131
Step 4/6 : RUN npm install
 ---> Using cache
 ---> 8f0ae8a2f7f6
```

###ワーキングディレクトリの指定
Dockerfile内に`WORKDIR /usr/app`という記述がありますが、ここでは`RUN`, `CMD`, `ENTRYPOINT`, `COPY`, `ADD`, `docker run`, `exec`で実行するコンテナプロセスのワーキングディレクトリを指定しています。

`WORKDIR /`と指定することももちろんできるのですが、ルートディレクトリにはいろんなファイルやフォルダが含まれているので、ファイル名が同じだったりすると干渉が起こってしまいます。そのため、コンテナプロセス用のディレクトリを指定してあげる必要があります。

コンテナ起動時にシェルを開くと、確かにワーキングディレクトリが`/usr/app`となっています。

```
(base) [11:06:07] → docker run -it suzuki0430/simpleweb sh                                                                                     ~/Programs/docker/simpleweb
/usr/app #
```

#おわりに
Node.jsをコンテナで動かすことができるようになりました。次はDocker-composeについて学習します。

#参考資料

https://www.udemy.com/course/docker-and-kubernetes-the-complete-guide/

https://www.creationline.com/lab/29422