超基礎からの 速習 Docker (2)

![Whale, Docker](https://cdn-images-1.medium.com/max/800/1*8DG81gjTPZ2cmTfZmeD9nw.jpeg)

本稿は、Christffer Noring さん ([@chris_noring](https://twitter.com/chris_noring)) の [Learn Docker, from the beginning, part II](https://softchris.github.io/pages/docker-two.html) を翻訳し、分かりやすいように少しだけ追記、サンプルコードの実行上の補足等を行ったものです。シリーズ翻訳の意図については [超基礎からの 速習 Docker (1)](https://qiita.com/Gucchiy/items/16045193baabf3dfa176) の冒頭に掲載しています。併せてご参照くださいませ。

#超基礎からの 速習 Docker シリーズ一覧
- **[超基礎からの 速習 Docker (1)](https://qiita.com/Gucchiy/items/16045193baabf3dfa176)** 
    - なぜ Docker なのか、コンテナーやイメージ、Dockerfile の基本コンセプトの解説、もちろん、それらを管理するのに必要なコマンド群もカバーしています。
- **超基礎からの 速習 Docker (2)** 
    - いまここ。
- **[超基礎からの 速習 Docker (3)](https://qiita.com/Gucchiy/items/679b95715592bb4bb4de)**
    - データベースをコンテナ化し、レガシーなやり方及び新しいやり方である Network を用いて他のコンテナから連絡可能にします。
- **[超基礎からの 速習 Docker (4)](https://qiita.com/Gucchiy/items/8a4421dd087a99b44c78)**
    - Docker Compose を用いた複数サービスの管理方法を解説します（その１）
- **[超基礎からの 速習 Docker (5)](https://qiita.com/Gucchiy/items/8d078dde6d890fdd8144)**
    - Docker Compose を用いた複数サービスの管理方法を解説します（その２）

速習 Docker パート2へようこそ。できればパート1を読んで Docker コア コンセプトと基本コマンドについての基本的な理解が出来ているか、他で知っているかしてくれていると良いですね。

本稿では、以下のトピックをカバーしています。

- **復習、そして課題の紹介** パート1で学んだことを復習しましょう。そして、Volume を使わないことがいかに骨が折れるかお伝えしましょう。
- **永続的データ** Volume は、僕らが作るファイル群や僕らが中身をいじるデータベース（e.g Sqlite）を永続化するのに利用できます。
- **WORKDIR を Volume に変える** Volume は、更新あるごとにコンテナーをセットアップしたり、削除したりすることなしにアプリ作業を続ける良い方法も提供してくれます。

>**訳注**
多くの Docker 文章で "Persist data" を "永続的データ" と翻訳していますが、字面では少し分かりにくいですね。この後の Volume の実地で明らかになりますが、要はコンテナーが削除されてもそのまま残っているデータのことで、それを "Persist"、"永続的" と言ってます。 

#リソース

Docker を使う事、コンテナー化は、一枚岩をマイクロサービスに分解していくことです。このシリーズのいたるところで僕らは Docker やそのコマンド体系をマスターするために学ぶことになります。そうすれば、きっと君は自作のコンテナーをプロダクション環境で使いたくなるでしょう。その環境は大抵クラウド上にあります。十分な Docker 経験を積んだと思ったなら、次のリンクで Docker をクラウドでどのように活用できるか、ご確認してみると良いと思います。

- [Azure 無料アカウントのサインアップ](https://azure.microsoft.com/ja-jp/free/?wt.mc_id=personal-blog-chnoring) プライベート レジストリのようなクラウドのコンテナーを使うには、無料 Azure アカウントが必要でしょう。
- [クラウドのコンテナー](https://docs.microsoft.com/ja-jp/azure/containers/?wt.mc_id=personal-blog-chnoring) クラウドのコンテナーについて他に知っておくべきことについて網羅する概要ページです。
- [自作コンテナーをクラウドにデプロイ](https://docs.microsoft.com/ja-jp/azure/container-instances/container-instances-tutorial-prepare-app?wt.mc_id=personal-blog-chnoring) 今の Docker スキルをレバレッジしてクラウド上でサービスを動かすことがいかに簡単かを示すチュートリアル。
- [コンテナー レジストリの作成](https://docs.microsoft.com/ja-jp/azure/container-instances/container-instances-tutorial-prepare-acr?wt.mc_id=personal-blog-chnoring) 自作 Docker イメージを Docker Hub に入れられますが、クラウドのコンテナー レジストリも可能です。自作イメージをどこかにストアして、一瞬でレジストリから実際のサービスをできるようにすることは凄くないですか？

#復習、そして Volume を使わないことの問題

OK、このシリーズの最初のパートで作った express ライブラリーを使った Node.js アプリケーションをそのまま使っていきましょう。

このセクションでは次のことを行います：

- **コンテナーの実行** コンテナーをスタートすることで、最初のパートで学んだ Docker 基本コマンドをいくつか繰り返します。
- **アプリの更新** 僕らのソースコードを更新し、コンテナーをスタート、ストップ。このやり方がいかに骨が折れるか実感してもらいます。

##コンテナーの実行

アプリケーションが大きくなるにつれて、ルートを追加したくなったり、特定のルートでレンダリングするものを変えたりしたくなります。ここまでのソースコードを見てみましょう：

``` js:app.js
const express = require('express')

const app = express()

const port = process.env.PORT

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(port, () => console.log(`Example app listening on port ${port}!`))
```
基本コマンドを覚えているか見てみましょう。Let's Type:
`docker ps`

```sh:
% docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
```

OK、何もないようですね。前回使ったかどうかに関係なく、最後に僕らは docker stop または docker kill を使ってクリーンアップしましたね。ですので、コンテナーをビルドしなければなりません。僕らはどんなイメージを持っているか見てみましょう。
`docker images`

```sh:
% docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
chrisnoring/node    latest              177e657fae6e        6 days ago          943MB
```
OK、僕らのイメージがありますね。コンテナーを作って実行しましょう。
`docker run -d -p 8000:3000 chrisnoring/node`
これは、コンテナーを作り、ポート 8000 、detached モードで実行します。-d フラグを指定してくれてありがとう！
>**訳注**
あれっ、パート1では daemon モードって言ってなかったっけ・・？ -d は --detached の略式なので、detached モードの方であってそうだけど。

```
% docker run -d -p 8000:3000 chrisnoring/node
a093abebf85dd157f35413a5398b0bbc23a5013e90acbca0a109791db0840ec3
```
コンテナー ID を取得しました。良いですね。http://localhost:8000 にアクセスしてアプリを見つけられるか見てみましょう。
![Hello World](https://cdn-images-1.medium.com/max/528/1*e3m0bmN42AEsQcYIurlPGQ.png)
OK、良いですね。いよいよ次のステップ、ソースコードを更新する準備が整いました。

##アプリの更新
デフォルトのルートを、Hello Chris とレンダリングするように変えましょう。次のように加えます。

```js:
app.get('/', (req, res) => res.send('Hello Chris!'))
```
OK、変更を修正したら、ブラウザに戻ってみよう。すると、「Hello World」のままだって気が付きますよ。まだ僕らの変更はコンテナーに反映されてないようです。こうなったら、コンテナーを落として、削除、イメージをリビルド、そしてコンテナーを再び実行する必要があります。すべてのコマンドを実行しなければならない都合上、ビルドしてコンテナーを実行するとき名前で行えるようにすると良いですね。次のようにコンテナーを実行する代わりに：
`docker run -d -p 8000:3000 chrisnoring/node`
こうタイプしましょう：
`docker run -d -p 8000:3000 --name my-container chrisnoring/node`
この意味するところは、コンテナーが my-container と言う名前を得ることで、コンテナー ID の代わりにこの名前でコンテナーを参照できるようにしたのです。セットアップや削除にコンテナーIDと同じように使えるので、ちょっと良い感じですね。（訳注：以下いずれも Node.js プロジェクトのフォルダで実行ください。）

```
docker stop my-container // this will stop the container, it can still be started if we want to

docker rm my-container // this will remove the container completely

docker build -t chrisnoring/node . // creates an image

docker run -d -p 8000:3000 --name my-container chrisnoring/node
```

こんな感じでコマンドを繋ぐとことも出来ます：

```
docker stop my-container && docker rm my-container && docker build -t chrisnoring/node . && docker run -d -p 8000:3000 --name my-container chrisnoring/node
```
>**訳注**
&& によるコマンドのチェーンは PowerShell v7 以降のサポートになります。Windows で PowerShell またはコマンド プロンプトをご利用の皆様はご注意ください。なお、&& はここしか出てこないけどね。

これを最初に見た時の印象は「わお」です。コマンドだらけです。特に開発フェーズにある時にはもっといい方法があるはずです。

![WOW](https://cdn-images-1.medium.com/max/572/1*LQxLEO4X3SkeqF1y4dMUZA.gif)

そうです、もっと良い方法があります。それでは Volume について見て行きましょう。

#Volume を使う

Volume や データ Volume は、ファイルを書いて永続化できる場所を作る方法です。どうしてそうしたいのでしょう？開発時、アプリケーションの状態はそのままにしたいですよね。最初から始める必要なんてない。一般に、log ファイルや JSON ファイル、データベース (SQLite) までも Volume に保存したいでしょう。
Volume を作ることはとても簡単です。たくさんの方法がありますが、主に二つの方法があります：

- コンテナーを作成する前に、
- だらだらする e.g コンテナーを作っている間

##Volumeの作成と管理
Volumeを作るには以下のようにタイプします：
`docker volume create [name of volume]`
作成された Volume は以下のようにタイプすることで確認できます：
`docker volume ls`

```
% docker volume ls
DRIVER              VOLUME NAME
```

これで、僕らが持っている他の Volume をリストできます。Volume は使っている間にたくさんになってしまいがちですので、数を減らすやり方を知っていると良いでしょう。次のようにタイプします：
`docker volume prune`
これで今使っていない Volume を削除できます。続けて良いかどうか確認がはいります。

```
% docker volume prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] 
```
もし、単体の Volume を削除したい場合は、次のようにタイプします：
`docker volume rm [name of volume]`
君が知りたいだろうもう一つのコマンドは、作った Volume をより詳細に見ることのできる inspect コマンドです。恐らく、永続化ファイルに関する話題としては、もっとも重要です。
`docker inspect [name of volume]`
これについてのコメントは、Docker が配置するこれらファイルについてほとんど気にしないかも知れませんが、デバッグ目的で時々知りたくなるはずです。このセクションの後で示すように、アプリケーションの開発時に永続化ファイルをコントロールすることは、僕らのアドバンテージとして機能します。

```
% docker inspect logs
[
    {
        "CreatedAt": "2020-05-16T10:18:25Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/logs/_data",
        "Name": "logs",
        "Options": null,
        "Scope": "local"
    }
]
```
Moutpoint フィールドは、Docker が永続化ファイルにしようとしている場所を教えてくれます。

##アプリケーションに Volume をマウントする

OK、それでは、アプリケーションで使いたい Volume の要点に触れました。僕らのコンテナーにあるファイルを変更したり作成したりしたいので、コンテナーを終了して再起動しても、変更はそのまま残ります。
これを行うために、ほとんど同じことをするための二つの異なるコマンド・構文を使えます。それは：

- -v、--volume、構文は -v [name of volume]:[directory in the container] となります。例えば、-v my-volume:/app 
- --mount、構文は --mount source=[name of volume],target=[diretory in container]、例えば、--mount source=my-volume,target=/app

コンテナの実行と組み合わせると、次のようになります：
`docker run -d -p 8000:3000 --name my-container --volume myvolume:/logs chrisnoring/node`

試してみましょう。まず初めにコンテナを実行します：

```
% docker run -d -p 8000:3000 --name my-container --volume logs:/logs chrisnoring/node
db2e45b88f88f67633c20e55907770b84d342c5e2377df531bf990522bb0bcc8
```
>**訳注** 
もし、すでにコンテナーが存在している状態の場合はエラーになりますので、`docker ps -a` などとして、実行しているコンテナーを確認し、`docker stop my-container`  `docker rm my-container` して、コンテナーを削除してください。 

それでは、コンテナーの内部に Volume がマウントされているかどうか、inspect コマンドを実行してみましょう。

```json:
        ・・・
        },
        "Mounts": [
            {
                "Type": "volume",
                "Name": "logs",
                "Source": "/var/lib/docker/volumes/logs/_data",
                "Destination": "/logs",
                "Driver": "local",
                "Mode": "z",
                "RW": true,
                "Propagation": ""
            }
        ],
        "Config": {
        ・・・
```

>**訳注** 
inspect コマンドの実行ですが、ここでは、`docker inspect my-container` のことを言っているようです。このようにすることで、my-container コンテナーの状態全体が JSON で返ってきますので、そのうちの "Mounts" の内容を確認します。

OK、僕らの Volume がありますね。良いでしょう。次のステップは、コンテナーの中の Volume を配置することです。コンテナーに入っていきましょう：
`docker exec -it my-container bash`
そして、/logs ディレクトリを確認します：

```
% docker exec -it my-container bash
root@db2e45b88f88:/app# cd ..
root@db2e45b88f88:/# ls
app  bin  boot  dev  etc  home  lib  lib64  logs  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@db2e45b88f88:/# cd logs/
root@db2e45b88f88:/logs# echo logging... > logs.txt
root@db2e45b88f88:/logs# ls
logs.txt
root@db2e45b88f88:/logs# 
```

OK、今、もしコンテナーを終了したなら、Volume に作ったすべては永続化されて、Volume にないものはすべて失われるはず。うん、いいアイディア。良いでしょう、Volume の原理を理解しましょう。

>**訳注**
一旦、`docker stop my-container`  `docker rm my-container` して、コンテナーを削除しても、`docker volume ls` すると Volume が残っていることが分かります。その上で、また `docker run (略) --volume logs:/logs (略)`、`docker exec -it my-container bash` して中に入れば、/logs フォルダに logs.txt が残っていることが確認できるでしょう。

##Volume をサブディレクトリとしてマウントする

ここまで、Volume を作って、Docker に永続化するファイルの場所を設定しました。それらのファイルを永続化すると設定したとき、何が起きているのでしょう？

もし、ハード ドライブのディレクトリをポイントすると、そのディレクトリが見えたり、ファイルを配置することができるだけでなく、すでにあるファイルをコンテナーのマウント ポイントに持ってくることもできます。それではデモしていきましょう。私が言っているのは：

- **ディレクトリの作成** /logs ディレクトリを作りましょう
- **ファイルを作成する** logs.txt ファイルを作り、そこにテキストを書きましょう
- **コンテナーを実行しましょう** ローカル ディレクトリ + /logs をマウント ポイントとして作成しましょう

最初の二つのコマンドで、こんな風なファイル構造になるでしょう。（訳注：Node.js プロジェクトのフォルダ内です）

```
app.js
Dockerfile
/logs
 logs.txt // contains 'logging host...'
package.json
package-lock.json
```

さて、次は run コマンドを実行して、コンテナーを立ち上げましょう：

```
% docker run -d -p 8000:3000 --name my-container --volume $(pwd)/logs:/logs chrisnoring/node
186c05ced5e02c4a84996f4b55717e08633c0f8f0b228674ef9f0cb3ecb15f35
```

>**訳注** 
例によって、すでにコンテナーが存在している状態の場合はエラーになりますので、`docker ps -a` などとして、実行しているコンテナーを確認し、`docker stop my-container`  `docker rm my-container` して、コンテナーを削除してください。 

>なお、Windows では $(pwd)/logs がエラーとなります。代わりに、PowerShell から以下のようにしてください。
`docker run -d -p 8000:3000 --name my-container -v "${pwd}/logs:/logs" chrisnoring/node`

--volume コマンドが少し異なりますね。最初のパラメータ $(pwd)/logs は、カレント ディレクトリとそのサブ ディレクトリ logs を意味しています。二つ目のパラメータ /logs は、僕らのホスト コンピューターの logs ディレクトリーをコンテナーの同じ名前のディレクトリにマウントすることを意味しています。
コンテナーにダイブして、コンテナーが僕らのホスト コンピュータにある logs ディレクトリからのファイルを実際に持ってきていることを確認しましょう。

```
% docker exec -it my-container bash
root@186c05ced5e0:/app# ls
Dockerfile  app.js  node_modules  package-lock.json  package.json
root@186c05ced5e0:/app# cd ..
root@186c05ced5e0:/# ls
app  bin  boot  dev  etc  home  lib  lib64  logs  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@186c05ced5e0:/# cd logs/
root@186c05ced5e0:/logs# ls
logs.txt
root@186c05ced5e0:/logs# cat logs.txt
logging host...
root@186c05ced5e0:/logs#
```

上記通りのコマンド セットで `docker exec -it my-container bash` コンテナーに入り、logs ディレクトリーに進み、ついに、`cat logs.txt` で logs.txt を読み出しました。結果は logging host... これは一つの例で、僕らがホスト コンピューターで持っているファイルと内容は正確に同じです。

これは ホスト コンピューターの Volume とコンテナーの間を接続している Volume です。ホスト コンピューターの log.txt を編集して、コンテナーに何が起きるか確認してみましょう。

```
root@186c05ced5e0:/logs# cat logs.txt
logging host 2...
```

わぉ、コンテナーを終了したり、再起動したりせずに、コンテナーの中を変更しました。

##アプリケーションを Volume として扱う

アプリを Volume として扱うように作るために、コンテナーをこんな風に終了させます。
`docker kill my-container && docker rm my-container`
なぜこんなことをする必要があるのでしょう？それは、ソースコードだけでなく Dockerfile も変更しようとしていて、以下に示そうとしているように Volume を使わない限り、コンテナーは変更を反映できないからです。

今回は、Volume の名前を `--volume $(pwd):/app` とパラメータで変えてコンテナーを再実行する必要があります。（訳注：Windows + PowerShell の場合は `--volume "${pwd}:/app"` となります）
>**注：**
もし、PWDがスペースを含むディレクトリで構成されている場合は、`"$(PWD)":/app` を代わりにパラメータとして指定してください。つまり、$(PWD) をダブルクォーテーション「"」で囲む必要があります。指摘してくれた Vitaly に感謝します:grinning:

完全なコマンドは以下のようになります（訳注：Node.js プロジェクトのフォルダで行います）：

```
% docker run -d -p 8000:3000 --name my-container --volume $(pwd):/app chrisnoring/node
1b4019ec7c4379f7943a5a1e5a9f3e397cbf81a85060d64e9684e8760867802b
```

これによって、僕らのアプリ ディレクトリを Volume にして、コンテナー内の何か変更を反映するようにします。

では、Node.js Express アプリケーションにルートを追加しましょう：

```js:
app.get("/docker", (req, res) => {

  res.send("hello from docker");

});
```

OK、Express ライブラリを扱うことで知っていることから、ブラウザーから http://localhost:800/docker は到達できるかな？

![Cannot GET /docker](https://cdn-images-1.medium.com/max/674/1*XXzp0H1eTDXIuo2QJ1EYjQ.png)

悲しい顔だね:frowning: 動いていません。何が良くなかったのでしょう？まぁ、こういうことです。Node.js Express アプリケーションのソースコードを変更した場合、アプリをリスタートする必要があります。戻って、ファイル変更に対して即座に Node.js Express ウェブ サーバーを再起動する方法を考えなければなりません。それにはいくつかのやり方があります。例えば：

- **インストール** ウェブ サーバーをリスタートする nodemon や forever のようなライブラリをインストール
- **実行** PKILL コマンドを実行して、node.js プロセスを kill し、node app.js を実行する

nodemon のようなライブラリをインストールするだけなら、面倒ということもないので、さっそくそうしましょう（訳注：プロジェクトのフォルダで実行してください）：

```
npm install --save-dev nodemon
```

これは、他のライブラリの依存関係を pckage.json に持ったということですが、アプリを動かす方法を変える必要があるということでもあります。アプリは `nodemon app.js` と言うコマンドを使ってスタートさせなければなりません。これは nodemon が、変更あり次第、全体をリスタートするようケアすることを意味します。それでは、start script を package.json に追加しましょう。結局、それは Node.js 的な方法です：

![start:nodemon app.js](https://cdn-images-1.medium.com/max/750/1*o9LH57gAsnkcA2N6osgAEw.png)

Node.js が初めての人のためにも、上でおこなったことを書こう。start script を package.json に追加するっていうのは、"scripts" セクションに行き、次のように entry start を追加します（package.json の抜粋）：

```json:package.json
"scripts": {
  "start": "nodemon app.js"
}
```

デフォルトでは、"scripts" にあるコマンドは、`npm run [name of command]` と入力して実行します。しかし、 start や test といった知られたコマンドは、キーワード run で省略でき、`npm run start` は `npm start` と書けます。他のコマンド "log" を追加してみましょう（package.json の抜粋）：

```json:package.json
"scripts": {
  "start": "nodemon app.js",
  "log": "echo \"Logging something to screen\""
}
```

新しいコマンド "log" を実行するのに、`npm run log` と書けます。

OK、一つ残っていますが、それは、Dockerfile を僕らのアプリをどう start するか変更することです。最後のラインを：

```dockerfile:Dockerfile
ENTRYPOINT ["node", "app.js"]
```

こうします：

```dockerfile:Dockerfile
ENTRYPOINT ["npm", "start"]
```

Dockerfile を変更したので、イメージをリビルドします。このようにします：
`docker build -t chrisnoring/node .`
>**訳注**
しつこいようですが、`docker stop my-container`  `docker rm my-container` をお忘れなく。コンテナーが残っている時に、新たにイメージを作ると、古いイメージが実行中のコンテナー用に無名のイメージとして残ります。必要に応じて、`docker images` で確認し、`docker rmi <image id>` などとして削除した方がよいでしょう。

OK、次のステップはコンテナーの起動です：
`docker run -d -p 8000:3000 --name my-container --volume $(PWD):/app chrisnoring/node`
注目に値することは、カレントとなるプロジェクト ディレクトリ全体を公開し、それをコンテナ内の /app にマッピングする方法です。

/docker に対するルートはすでに追加したので、次のように新しいルートを追加してみましょう。

```javascript:
app.get('/nodemon', (req, res) => res.send('hello from nodemon'))
```

僕らが app.js の変更を保存したとき、nodemon がその変更を反映してくれますように！

![hello from nodemon](https://cdn-images-1.medium.com/max/688/1*65hjrpy7SNfF5_ENUZTXbA.png)

アァァァァンド、僕らは勝った！/nodemon がルートとして動いている。僕は君を知らないけど、これを最初に機能させたのは僕ってことだ。（訳注：ちょっと意味不明・・）

![We have a winner](https://cdn-images-1.medium.com/max/220/1*PyeUlERT6b8VmSF1V0iv5Q.gif)

#サマリー
これで記事の最後まで到達しました。とてもクールで使いやすい機能である Volume について学びました。更に重要なことは、全ての開発環境を Volume にして、ソースコードをコンテナーのリスタートなしに働かせ続けられるようにしたことです。

僕らのシリーズのパート3では、リンクされたコンテナーやデータベースがどのように動くかについてカバーします。乞うご期待。

[Twitter](https://twitter.com/chris_noring) をフォローして、トピックへのあなたの問い合わせやご質問、提案を頂けるとハッピーです。

***
**[超基礎からの 速習 Docker (3) に進む](https://qiita.com/Gucchiy/items/679b95715592bb4bb4de)**