---
title: "Dockerを使ってNode開発環境を作る"
description: "Dockerのイメージとコンテナを活用して、サーバーを実行する分離したNode開発環境を作る"
authors:
  - name: "Dan Arias"
    url: "https://auth0.com/blog/authors/dan-arias/"
date: "Feb 26, 2019"
category: "Developers,Deep Dive,Docker"
tags: ["docker", "node", "compose", "docker-compose", "automation", "environment", "containers", "images"]
url: "https://auth0.com/blog/jp-use-docker-to-create-a-node-development-environment/"
---

# Dockerを使ってNode開発環境を作る


このチュートリアルでは、Nodeアプリケーションをローカルで作成・実行する代わりに、Docker Nodeの公式イメージがベースとするDebian Linuxオペレーティングシステムのメリットを活用する方法について紹介します。ここでは、ポータブルなNode開発環境を作成し、開発者が「自分のマシンで実行しているはずなのに？」といぶかしがる問題を解決します。これは、いずれかのプラットフォームでDockerイメージを実行すると、コンテナが先読みして作成されることで発生するものです。

このチュートリアルでは、次の2つの空間を使います。

- **ローカルのオペレーティングシステム**：TerminalやPowerShellなどのCLIアプリケーションを使って、Dockerのローカルインストールを使用し、イメージをビルドしてコンテナとして実行します。

- **コンテナのオペレーティングシステム**：Dockerコマンドを使って、実行中のコンテナのベースのオペレーティングシステムにアクセスします。このコンテキストの中でコンテナのシェルを使ってコマンドを実行し、Nodeアプリケーションの作成と実行を行います。

コンテナのオペレーティングシステムは、ローカルのオペレーティングシステムとは別に実行します。コンテナ内で作成したファイルは、ローカルではアクセスできません。コンテナ内で実行しているサーバーは、ローカルのウェブブラウザでのリクエストをリッスンできません。**これは、ローカル開発には理想的ではありません**。この制約を解決するには、次のことを行って2つのシステム間を橋渡しします。

- **コンテナのファイルシステムにローカルのフォルダをマウントする**：このマウントポイントをコンテナの作業ディレクトリとして使うと、コンテナ内で作成したすべてのファイルをローカルで永続化することができます。また、ローカルで行われたプロジェクトファイルへの変更をコンテナに伝えることもできます。

- **ホストにコンテナネットワークの操作を許可する**：ローカルポートをコンテナポートにマッピングすると、ローカルポートへのHTTPリクエストはすべてDockerによってコンテナポートにリダイレクトされます。

このDockerベースのNode環境戦略を実際に使ってみるには、基本的なNode Expressウェブサーバーを作成します。さあ、始めてみましょう。

<!-- TOC -->

<div id="toc-container">

<span>目次</span>

<ul id="toc">
<li><a class="toc-link" href="#removing-the-burden-of-installing-node" target="_self">Nodeをインストールする負担を取り除く</a></li>
<li><a class="toc-link" href="#creating-the-project-foundation" target="_self">プロジェクトの基礎を作成する</a></li>
<li><a class="toc-link" href="#accessing-the-container-operating-system" target="_self">コンテナのオペレーティングシステムにアクセスする</a></li>
<li><a class="toc-link" href="#installing-dependencies-and-running-the-server" target="_self">依存関係をインストールしてサーバーを実行する</a></li>
<li><a class="toc-link" href="#modifying-and-extending-the-project" target="_self">プロジェクトを変更・拡張する</a></li>
<li><a class="toc-link" href="#recap-and-uncovering-little-lies" target="_self">まとめ... 小さなウソをばらすと</a></li>
</ul>
</div>

<!-- /TOC -->

## <a class="toc-target" id="removing-the-burden-of-installing-node"></a>Nodeをインストールする負担を取り除く

シンプルな"Hello World!"というNodeアプリケーションを実行するには、一般的なチュートリアルでは次のことをやるようにと書かれています。

- Nodeをダウンロードしてインストールする
- Yarnをダウンロードしてインストールする
- Nodeの別のバージョンを使うには、Nodeをアンインストールしてから、[`nvm`](https://github.com/creationix/nvm)をインストールする
- NPMパッケージをグローバルにインストールする

->![Ain't nobody got time for that!](https://images.ctfassets.net/23aumh6u8s0i/4xVUHMWNLagUIrrnQDuE46/24abba17c53d63744bfb84e235796432/Aint-Nobody-Got-Time)<-

どのオペレーティングシステムにも、上記のインストールが一筋縄ではいかないそれぞれの癖があります。しかし、NodeエコシステムへのアクセスはDockerイメージを使うことで標準化することができます。**このチュートリアルでインストールが必要なのはDockerだけです**。Dockerのインストールが必要な場合は、[このDockerインストールドキュメント](https://docs.docker.com/install/#supported-platforms)から使用しているオペレーティングシステムを選択し、次の手順に従ってください。

NPMと同様に、Dockerは数多くのDockerイメージが登録されている[Docker Hub](https://hub.docker.com/)にアクセスできます。このDocker Hubから、Nodeのさまざまなバージョンをイメージとして実行することができます。これらのイメージは、ローカルプロセスとして他と重複・競合せずに実行できます。Node 8 with NPMやNode 11 with Yarnに依存するクロスプラットフォームプロジェクトを同時に作成することもできます。

## <a class="toc-target" id="creating-the-project-foundation"></a>プロジェクトの基礎を作成する

まず、システムのどこかに`node-docker`フォルダを作成します。これがプロジェクトディレクトリになります。

Node Expressサーバーを実行することを目標に、`node-docker`プロジェクトディレクトリの下に`server.js`ファイルを作成し、次のように入力して保存します。

```javascript
// server.js
const express = require("express");
const app = express();

const PORT = process.env.PORT || 8080;

app.get("/", (req, res) => {
res.send(`
    <h1>Docker + Node</h1>
    <span>A match made in the cloud</span>
`);
});

app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}...`);
});
```

Nodeプロジェクトには`package.json`ファイルと`node_modules`フォルダが必要です。_Nodeがシステムにインストールされていない場合は_、Dockerを使ってこれらのファイルを構造化ワークフローに従って作成します。

## <a class="toc-target" id="accessing-the-container-operating-system"></a>コンテナのオペレーティングシステムにアクセスする

コンテナOSには、次のメソッドでアクセスします。

- <a href="#single-command" target="_self">1つの長い`docker run`コマンドを使用する</a>。
- <a href="#dockerfile" target="_self">`Dockerfile`と短い`docker run`コマンドを組み合わせる</a>。
- <a href="#docker-compose" target="_self">`Dockerfile`をDocker Composeと組み合わせて使用する</a>。

### <a id="single-command"></a> 1つの`docker run`コマンドを使用する

次のコマンドを実行します。

```
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000 \
-u node node:latest /bin/bash
```

コンテナシェルへのアクセスがどのように行われるかを理解するために、この`docker run`コマンドを分析してみましょう。

```bash
docker run --rm -it
```

`docker run`は、新しいコンテナインスタンスを作成します。コンテナが存在するようになると、[`--rm`フラグ](https://docs.docker.com/engine/reference/run/#clean-up---rm)がコンテナを自動的に停止・削除します。`-i`と`-t`を組み合わせたフラグは、[シェルなどのインタラクティブプロセス](https://docs.docker.com/engine/reference/run/#foreground)を実行します。`-i`フラグは、STDIN（Standard Input）を開いたままの状態にし、その間に`-t`フラグがプロセスがテキストターミナルになりすまして信号をわたします。

> `--rm`は、ことわざの「out of sight, out of mind」のようなもので、見なければ忘れてしまいます。

> `-it`チームがいないと、画面には何も表示されません。

->![Hello, IT, have you tried turning it off and on again?](https://images.ctfassets.net/23aumh6u8s0i/3nakiU8KELbKSpyeyDXaPQ/b226e7664c58d45971fd708ee36c8117/giphy)<-

```
docker run --rm -it --name node-docker
```

[`--name`フラグ](https://docs.docker.com/engine/reference/run/#name---name)は、ログやテーブルで見つけやすいように、コンテナにわかりやすい名前を付けます。たとえば、[`docker ps`](https://docs.docker.com/engine/reference/commandline/ps/)を実行したときなどです。

```
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app
```

[`-v`フラグ](https://docs.docker.com/engine/reference/run/#volume-shared-filesystems)は、このマッピングを引数として使い、ローカルフォルダをコンテナフォルダにマウントします。

`<HOST FOLDER RELATIVE PATH>:<CONTAINER FOLDER ABSOLUTE PATH>`

環境変数は、MacやLinux上でコマンド`$PWD`を、Windows上でコマンド`$CD`を実行したときに、現在の作業ディレクトリをプリントすることができます。[`-w`フラグ](https://docs.docker.com/engine/reference/run/#workdir)は、マウントポイントをコンテナの作業ディレクトリとして設定します。

```
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000
```

[`-e`フラグ](https://docs.docker.com/engine/reference/run/#env-environment-variables)は、環境変数`PORT`に値`3000`を設定します。[`-p`フラグ](https://docs.docker.com/engine/reference/run/#expose-incoming-ports)は、ローカルポート`8080`をコンテナポート`3000`にマッピングし、`server.js`の中で消費される環境変数`PORT`を照合します。

```javascript
const PORT = process.env.PORT || 8080;
```

```
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000 \
-u node node:latest /bin/bash
```

セキュリティの保護とファイル権限の問題を回避するため、[`-u`フラグ](https://docs.docker.com/engine/reference/run/#user)によって、Nodeイメージ内にコンテナプロセスを実行するユーザーとしてルート以外のユーザー`node`を設定します。フラグを設定すると、実行するイメージが`node:latest`に指定されます最後の引数は、実行中にコンテナ内で実行するコマンドです。`/bin/bash`によって、コンテナシェルが呼び出されます。

> イメージがローカルにない場合は、Dockerがバックグラウンドで[`docker pull`](https://docs.docker.com/engine/reference/commandline/pull/)を発行し、[Docker Hub](https://hub.docker.com/)からダウンロードします。

コマンドが実行されると、次のコンテナシェルのプロンプトが表示されます。

```bash
node@<CONTAINER ID>:/home/app$
```

次のメソッドに移る前に、`exit`と入力して<ENTER>を押し、コンテナのターミナルを終了します<ENTER>。

### <a id="dockerfile"></a> `Dockerfile`を使用する

前のセクションの`docker run`コマンドは、イメージのビルドタイムとコンテナのランタイムフラグおよび要素から構成されます。 

```
docker run --rm -it --name node-docker \
-v $PWD:/home/app -w /home/app \
-e "PORT=3000" -p 8080:3000 \
-u node node:latest /bin/bash
```

イメージのビルドタイムに関連するものはすべて、次のように`Dockerfile`を使ってカスタムイメージとして定義できます。

* `FROM`は、コンテナのベースイメージ`node:latest`を指定します。
* `WORKDIR`は、`-w`を定義します。
* `USER`は、`-u`を定義します。
* `ENV`は、`-e`を定義します。
* `ENTRYPOINT`は、コンテナが実行されると`/bin/bash`の実行を指定します。

これにもとづき、`node-docker`プロジェクトディレクトリの下に`Dockerfile`という名前のファイルを作成し、次のように入力して保存します。

```dockerfile
FROM node:latest

WORKDIR /home/app
USER node
ENV PORT 3000

EXPOSE 3000

ENTRYPOINT /bin/bash
```

`EXPOSE 3000`は、ランタイムに表示するポートを記述します。ただし、コンテナの名前、ポートマッピング、ボリュームマウンティングを定義するコンテナのランタイムフラグは、`docker run`で指定する必要があります。

`Dockerfile`内で定義するコンテナイメージは、実行する前に[`docker build`](https://docs.docker.com/engine/reference/commandline/build/)を使ってビルドする必要があります。ローカルターミナルで、次のように実行します。

```
docker build -t node-docker .
```

`docker build`は、[`-t`フラグ](https://docs.docker.com/engine/reference/commandline/build/#tag-an-image--t)を使ってイメージに`node-docker`というわかりやすい名前をつけます。これは、コンテナの名前とは別のものです。イメージが作成されたことを確認するには、`docker images`を実行します。

イメージが作成されたら、次の短いコマンドを実行してサーバーを実行します。

```bash
docker run --rm -it --name node-docker \
-v $PWD:/home/app -p 8080:3000 \
node-docker
```

コンテナシェルのプロンプトは、次の形式で表示されます。

`node@<CONTAINER ID>:/home/app$`

もう一度、次のメソッドに移る前に、`exit`と入力して<ENTER>を押し、コンテナのターミナルを終了します<ENTER>。

### <a id="docker-compose"></a> Docker Composeを使用する

> Linuxの場合は、[Docker Composeは個別にインストール](https://docs.docker.com/compose/install/)します。

前のセクションの`Dockerfile`と短い`docker run`コマンドにもとづいて、Docker ComposeのYAMLファイルを作成し、Node開発環境をサービスとして定義します。

**`Dockerfile`**:

```dockerfile
FROM node:latest

WORKDIR /home/app
USER node
ENV PORT 3000

EXPOSE 3000

ENTRYPOINT /bin/bash
```

**Command**

```bash
docker run --rm -it --name node-docker \
-v $PWD:/home/app -p 8080:3000 \
node-docker
```

抽象化する`docker run`コマンドの要素は、コンテナ名、ボリュームマウンティング、ポートマッピングのみです。

`node-docker`プロジェクトディレクトリの下に`docker-compose.yml`という名前のファイルを作成し、次のように入力して保存します。

```yaml
version:"3"
services:
nod_dev_env:
build: .
container_name: node-docker
ports:
- 8080:3000
volumes:
- ./:/home/app
```

* `nod_dev_env`は、サービスにわかりやすい名前を付けます。
* `build`は、`Dockerfile`へのパスを指定します。
* `container_name`は、コンテナにわかりやすい名前を付けます。
* `ports`は、ホストからコンテナへのポートマッピングを構成します。
* `volumes`は、ローカルフォルダからコンテナフォルダへのマウンティングポイントを定義します。

このサービスを開始して実行するには、次のコマンドを実行します。

```
docker-compose up
```

`up`は、自らのイメージとコンテナを、前に使った`docker run`コマンドと`docker build`コマンドで作成されたものから個別にビルドします。これを検証するには、次を実行します。

```bash
docker image
# Notice the image named <project-folder>_nod_dev_env
docker ps -a
# Notice the container named <project-folder>_nod_dev_env_<number>
```
 
`up`は、イメージとコンテナを作成しますが、コンテナシェルのプロンプトは表示されません。これは、`docker-compose.yml`で定義したフルサービスを`up`が開始するためです。ただしインタラクティブな出力は表示せず、代わりに静的なサービスログのみを表示します。インタラクティブな出力を得るには、代わりに`docker-compose run`を使って、`nod_dev_env`を個別に実行します。 

まず、`up`で作成したイメージやコンテナを消去するために、ローカルターミナルで次のコマンドを実行します。
       
```bash
docker-compose down
```

さらに、次のコマンドでサービスを実行します。

```bash
docker-compose run --rm --service-ports nod_dev_env
```

`run`コマンドは、`docker run -it`と同じように動作しますが、コンテナポートをホストにマッピングすることも表示することもありません。Docker Composeファイルの中で構成したポートマッピングを使用するには、`--service-ports`フラグを使います。コンテナシェルのプロンプトが、次の形式でもう一度表示されます。

`node@<CONTAINER ID>:/home/app$`

何らかの理由でDocker Composeファイルに指定したポートが使用中の場合は、`--publish`, (`-p`)フラグを使って別のポートマッピングを手動で指定できます。たとえば、次のコマンドはホストポート`4000`をコンテナポート`3000`にマッピングします。

```bash
docker-compose run --rm -p 4000:3000 nod_dev_env
```

## <a class="toc-target" id="installing-dependencies-and-running-the-server"></a>依存関係をインストールしてサーバーを実行する

> アクティブなコンテナシェルがない場合は、上記のいずれかのメソッドを使ってアクセスします。
 
コンテナシェルで、Nodeプロジェクトを初期化し、次のコマンドを発行して依存関係をインストールします（`npm`を使うこともできます）。

```bash
yarn init -y
yarn add express
yarn add -D nodemon
```

`package.json`と`node_modules`がローカルの`node-docker`プロジェクトディレクトリの下にできていることを確認します。

[`nodemon`](https://github.com/remy/nodemon)は、ソースコードに変更を加えるたびにサーバーを自動的に再起動し、開発ワークフローの効率化を図ります。`nodemon`を構成するには、`package.json`を次のように更新します。

```json
{
// Other properties...
"scripts": {
"start": "nodemon server.js"
}
}
```

コンテナシェル内で、`yarn start`を実行してNodeサーバーを実行します。

サーバーをテストするには、ローカルブラウザを使って[`http://localhost:8080/`](http://localhost:8080/)にアクセスします。Dockerは、ホストポート`8080`からコンテナポート`3000`にリクエストを自動的にリダイレクトします。

ローカルファイルのコンテンツとサーバーの接続をテストするには、`server.js`をローカルで開き、レスポンスを次のように更新して変更を保存します。

```javascript
// server.js

// package and constant definitions...

app.get("/", (req, res) => {
res.send(`
    <h1>Hello From Node Running Inside Docker</h1>
`);
});

// server listening...
```

ブラウザのウィンドウを閉じ、新しいレスポンスを調べます。

## <a class="toc-target" id="modifying-and-extending-the-project"></a>プロジェクトを変更・拡張する


Nodeがローカルシステムにインストールされていない場合は、ローカルターミナルを使ってプロジェクトの構造とファイルのコンテンツを修正できますが、`yarn add`などのNode関連のコマンドは発行できません。コンテナなしでサーバーを実行すると、内部のコンテナポート`3000`へのサーバーリクエストも行えません。

コンテナ内のサーバーを操作したり、Nodeプロジェクトに変更を加えたい場合は、[`docker exec`](https://docs.docker.com/engine/reference/commandline/exec/)を使って、実行中のコンテナとそのIDにコマンドを実行する必要があります。`docker run`コマンドは、分離した新しいコンテナを作成するため、使用しません。

実行中のコンテナのIDは、簡単に取得できます。

* すでにコンテナシェルが開いている場合は、コンテナIDがシェルプロンプトに表示されています。

`node@<CONTAINER ID>:/home/app$`

* コンテナIDは、プログラミング的に取得することもできます。[`docker ps`](https://docs.docker.com/engine/reference/commandline/ps/)を使い、一致するコンテナの`CONTAINER ID`を返すように、名前でフィルタします。

```bash
docker ps -qf "name=node-docker"
```

> `-f`フラグは、`name=node-docker`の条件にもとづいてコンテナをフィルタします。`-q` (`--quiet`)は、出力の中から一致するコンテナのIDだけを表示するように制限します。実質上、docker exec`コマンドの中に`node-docker`の`CONTAINER ID`が組み込まれます。

コンテナIDを取得したら、`docker exec`を使って次のことができます。

* 実行中のコンテナシェルの新しいインスタンスを開きます。

```bash
docker exec -it $(docker ps -qf "name=node-docker") /bin/bash
```

* 内部ポート`3000`を使ってサーバーリクエストを出します。

```bash
docker exec -it $(docker ps -qf "name=node-docker") curl localhost:3000
```

* 依存関係をインストールまたは削除します。

```bash
docker exec -it $(docker ps -qf "name=node-docker") yarn add body-parser
```

> もう一つのアクティブなコンテナシェルができたら、代わりにそこで`curl`、`yarn add`を簡単に実行できます。

## <a class="toc-target" id="recap-and-uncovering-little-lies"></a>まとめ... 小さなウソをばらすと

ここでは、さまざまなレベルの複雑さで分離したNode開発環境を作成する方法を紹介しました。1つの`docker run`コマンドを使用する方法、`Dockerfile`を使ってカスタムイメージをビルドして実行する方法、Docker Composeを使ってコンテナをDockerサービスとして実行する方法などです。

それぞれのレベルはより多くのファイル構成が必要ですが、コンテナを実行するコマンドはより短くなります。これは、構成をファイルにカプセル化することで環境がポータブルなものになり、維持しやすくなるので、価値あるトレードオフです。さらに、実行中のコンテナを操作してプロジェクトを拡張する方法も紹介しました。

IDEの場合は、構文アシスタントが使うにはNodeをローカルにインストールしなければなりません。もしくは、`vim`などのCLIエディタをコンテナ内で使うこともできます。

->![「構文アシスタントが使うにはNodeをローカルにインストールしなければならないなんて！」と、ハリー・ポッターが怒っています。](https://images.ctfassets.net/23aumh6u8s0i/fiXwLKWUXoYnHnCvlqgWL/735189d25b0f454bad861c84c67f50f2/giphy)<-

それでも、分離した開発環境の恩恵は受けられます。プロジェクトのセットアップ、インストール、コンテナ内で実行するランタイムの手順に制限を課すと、チーム全員が同じバージョンのLinuxでコマンドを実行するようになるので、これらの手順を標準化できます。さらに、Nodeツールで作成したキャッシュや隠れファイルはすべて、コンテナの中に閉じ込めておけるので、ローカルシステムを汚すことはありません。しかも、`yarn`が何とタダで手に入ります。

[JetBrains](https://www.jetbrains.com/)は、デバッグアプリケーションを実行しているときに、DockerイメージをNodeやPythonのリモートインタープリタとして使用する機能の提供を始めました。将来的には、これらのツールをシステムにダウンロードしてインストールする必要がまったくなくなるかもしれません。開発者の環境を標準化し、ポータブルなものとするために、業界が何をもたらしてくれるか、これからも注目していきましょう。

<include src="asides/AboutAuth0" />