30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Docker の volume と network の初歩

Docker の volume は、コンテナが使うデータを永続化するための仕組みで、これを使うことでコンテナのライフサイクルとは別にデータを管理することができる。
また、network という機能を使うことで、コンテナ間で通信ができるようになる。
この記事では、volume と network の基本的な使い方を見ていく。

コンテナを削除すればそのなかにあるデータも削除されてしまう

基本的に Docker のコンテナは、一度作ったものを長く大切に使い続けるのではなく、作成と破棄を繰り返して使うことが多い。
そしてコンテナを破棄すれば、コンテナのなかにあるデータも当然失われてしまう。
MySQL のコンテナで試してみる。

まずコンテナを作り起動させる。

% docker run --name test_db -dit -e MYSQL_ROOT_PASSWORD=password mysql:8

以下のコマンドを入力するとパスワードを求められるので、先程MYSQL_ROOT_PASSWORDとして設定したpasswordを入力する。

% docker exec -it test_db mysql -p

そうすると MySQL に接続できるので、sample_dbというデータベースを作ってみる。

mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.01 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample_db          |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

mysql> exit
Bye

その後、test_dbコンテナを一度停止させてから再度起動してアクセスしても、sample_dbは存在する。

% docker stop test_db
% docker start test_db
% docker exec -it test_db mysql -p
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample_db          |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

だがコンテナを削除し新しく作り直してからその中身を確認すると、sample_dbは存在しない。

% docker stop test_db
% docker rm test_db
% docker run --name test_db -dit -e MYSQL_ROOT_PASSWORD=password mysql:8
% docker exec -it test_db mysql -p
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

今見ているコンテナは、名前こそtest_dbではあるが最初に作成したコンテナとは別のコンテナなのだから、sample_dbが存在しないのは当然といえる。

このように、コンテナを削除してしまうと、コンテナに対して行った操作やそれによって生まれたデータは、全て失われてしまう。

volume を使ってデータを永続化する

Docker が提供する volume という機能を使うことで、データを永続化できる。
volume を使うと、コンテナではなくホストマシンにデータを保存するようになる。そしてそれでいて、コンテナのなかにデータが存在するかのように扱うことができるので、コンテナから簡単にデータを利用することができる。

まず、先ほど作成したtest_dbはもう使わないので、停止し削除する。

% docker stop test_db
% docker rm test_db

volume を使うためにはまず、docker volume create volumeの名前で volume を作る必要がある。
今回はmysql_volumeという volume を作ることにする。

% docker volume create mysql_volume

そしてコンテナを作る際に、-v ボリューム名:コンテナの記憶領域のパスというオプションをつければいい。

% docker run --name first_db -dit -e MYSQL_ROOT_PASSWORD=password -v mysql_volume:/var/lib/mysql mysql:8

上記の例では、-v mysql_volume:/var/lib/mysqlというオプションをつけて、first_dbという名前のコンテナを作っている。

「コンテナの記憶領域のパス」として/var/lib/mysqlを指定しているが、こうすることで、volume に保存されているデータを、コンテナの/var/lib/mysqlに存在するかのように扱うことができる。シンボリックリンクのようなもので、/var/lib/mysqlには実体は存在せず、あくまでも volume にデータが保存される。そして volume はコンテナではなくホストマシンに存在するため、このコンテナが破棄されたところで、データは残り続ける。
MySQL コンテナの場合、データは/var/lib/mysqlに保存される。そのため SQL でデータに対して何らかの操作を行うと、/var/lib/mysqlに対して操作を行うことになり、それはつまり volume に対して操作を行うことになる。

実際に操作を行って確認してみる。

まずfirst_dbに接続してsample_dbというデータベースを作る。

% docker exec -it first_db mysql -p
mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.01 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample_db          |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

次にfirst_dbを削除する。

% docker stop first_db
% docker rm first_db

そしてsecond_dbという新しいコンテナを作る。この際、先程と同様にmysql_volumeをコンテナと紐付けるようにする。

% docker run --name second_db -dit -e MYSQL_ROOT_PASSWORD=password -v mysql_volume:/var/lib/mysql mysql:8

そうすると、既にsecond_dbのなかにsample_dbが存在している。

% docker exec -it second_db mysql -p
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample_db          |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

first_dbに対して行った操作(sample_dbの作成)はmysql_volumeという volume に保存され、そのデータはfirst_dbとは別に管理されている。そのため、first_dbが削除されても残り続ける。
そしてsecond_dbを作成する際にmysql_volumeと紐付けたので、既にsecond_dbにはsample_dbが存在しているのである。

network によるコンテナ間の通信

network という機能を使うと、コンテナ間で通信ができるようになる。
例として Nginx のコンテナと Node.js のコンテナを用意し、両者間で通信できるようにしてみる。

Nginx コンテナの準備

まずnginxディレクトリとnodeディレクトリを作り、そこに必要なファイルを入れていくことにする。

% mkdir nginx
% mkdir node

そして以下のコマンドを実行し、nginx/userディレクトリにデータを用意する。このデータを Nginx コンテナにコピーして使うことになる。

% mkdir nginx/user
% echo "{\"id\": 1, \"name\": \"Alice\"}" > nginx/user/1.json
% echo "{\"id\": 2, \"name\": \"Bob\"}" > nginx/user/2.json
% echo "{\"id\": 3, \"name\": \"Carol\"}" > nginx/user/3.json

最後に、以下の内容のnginx/Dockerfileを作成。

FROM nginx:latest
WORKDIR /usr/share/nginx/html
COPY ./user ./user

Dockerfile の書き方については特に説明しないので、下記を参照。

numb86-tech.hatenablog.com

そして以下のコマンドを実行し、sample_nginx_imageというイメージを作る。

% docker build -t sample_nginx_image ./nginx

そしてそのイメージから、sample_nginxというコンテナを作る。

% docker run --name sample_nginx -d -p 8084:80 sample_nginx_image

8084:80でポートマッピングしたので、ホストマシンの8084ポートを経由して、Nginx コンテナの80ポートにアクセスできる。

% curl localhost:8084/user/1.json
{"id": 1, "name": "Alice"}
% curl localhost:8084/user/2.json
{"id": 2, "name": "Bob"}
% curl localhost:8084/user/3.json
{"id": 3, "name": "Carol"}
% curl localhost:8084/user/4.json
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.21.3</center>
</body>
</html>

だがこれは、ホストマシンとコンテナの通信である。
繰り返しになるが、network を使うことで、コンテナとコンテナで通信できるようになる。

Nginx コンテナが問題なく動いていることを確認できたので、sample_nginxは、停止、削除しておく。

% docker stop sample_nginx
% docker rm sample_nginx

network の作成

docker network create ネットワークの名前で、ネットワークを作成できる。

% docker network create sample_network

そして作成されたネットワークとコンテナを紐付けることで、同一のネットワークに紐付けられているコンテナ同士で通信できるようになる。

まず、sample_nginx_imageを元にしたコンテナを、sample_networkと紐付ける形で作成する。
--net ネットワークの名前オプションを付けることで、そのネットワークと紐付ける形でコンテナを作れる。

% docker run --name sample_nginx -d --net sample_network sample_nginx_image

Node.js コンテナの作成

あとは、sample_networkと紐付ける形で、Node.js のコンテナを作ればよい。

以下の内容のnode/index.jsnode/package.jsonを作る。

import fetch from "node-fetch";
import http from "http";

http
  .createServer(async function (req, res) {
    let result;
    try {
      const fetchResponse = await fetch("http://sample_nginx:80/user/1.json"); // 通信するコンテナの名前を host 部分に書く
      result = await fetchResponse.json();
    } catch (e) {
      result = e.message;
    }
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(`${JSON.stringify(result)}\n`);
  })
  .listen(3000);
{
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "node-fetch": "3.2.3"
  }
}

重要なのはnode/index.jsfetchの部分。
ここで、リクエスト先のホストをsample_nginxとしている。こうすることで、sample_nginxコンテナと通信が行われる。

最後にnode/Dockerfileを作成し、そこからsample_node_imageというイメージを作成する。

FROM node:16
WORKDIR /scripts

COPY package.json ./

RUN npm i -G yarn
RUN yarn install

COPY index.js ./
CMD ["yarn", "run", "start"]
% docker build -t sample_node_image ./node

そして、sample_networkと紐付ける形でコンテナを作成、起動する。

% docker run --name sample_node --net sample_network -d -p 8080:3000 sample_node_image

sample_nginxsample_nodesample_networkに紐付けられているため、両者間で通信ができるようになり、fetchで情報を取得できるようになった。

curl で Node.js コンテナにアクセスしてみると、sample_nginxから取得したデータが返されていることを確認できる。

% curl http://localhost:8080
{"id":1,"name":"Alice"}

Node.js コンテナと MySQL コンテナを接続する

最後により実践的な内容として、Node.js コンテナと MySQL コンテナの間でやり取りできるようにしてみる。また、MySQL コンテナには volume を紐付け、データが永続化されるようにする。

まずは volume と network の作成。

% docker volume create app_volume
% docker network create app_network

そして、これらと紐付けた形で、MySQL コンテナを作成する。

% docker run --name app_db -dit -e MYSQL_ROOT_PASSWORD=password -v app_volume:/var/lib/mysql --net app_network mysql:8

そして作成されたコンテナの MySQL に接続し、動作確認用のテーブルやデータを用意する。

% docker exec -it app_db mysql -p

mysql> CREATE DATABASE sample_db;
Query OK, 1 row affected (0.00 sec)

mysql> USE sample_db;
Database changed
mysql> CREATE TABLE users (id INT AUTO_INCREMENT, name TEXT NOT NULL, PRIMARY KEY (id));
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO users(name) VALUES('Alice');
Query OK, 1 row affected (0.02 sec)

具体的には、sample_dbというデータベースを作成し、そのなかにusersというテーブルを作成、そしてそこに 1 件のレコードを作成している。

mysql> SELECT * FROM users;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

Node.js コンテナからこのデータを取得できるようにするのが、ゴールである。

あとで必要になるのでポート番号も確認しておく。

mysql> show variables like 'port';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| port          | 3306  |
+---------------+-------+
1 row in set (0.01 sec)

MySQL 側の準備はこれで完了。

次は、Node.js コンテナの準備。
以下の内容でindex.jspackage.jsonDockerfileを作成する。

import fetch from "node-fetch";
import http from "http";
import mysql from "mysql2";

http
  .createServer(async function (req, res) {
    let result;
    try {
      const connection = mysql.createConnection({
        host: "app_db", // MySQL のコンテナの名前
        port: 3306, // 先程調べたポート番号
        user: "root",
        password: "password",
        database: "sample_db", // 接続したいデータベースの名前
      });
      connection.connect();
      result = await connection
        .promise()
        .query("SELECT * FROM users;")
        .then(([rows]) => {
          return rows;
        })
        .catch((err) => err);
    } catch (e) {
      result = e.message;
    }
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(`${JSON.stringify(result)}\n`);
  })
  .listen(3000);
{
  "type": "module",
  "license": "MIT",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "mysql2": "2.3.3",
    "node-fetch": "3.2.3"
  }
}
FROM node:16
WORKDIR /scripts

COPY package.json ./

RUN npm i -G yarn
RUN yarn install

COPY index.js ./
CMD ["yarn", "run", "start"]

index.jsのなかで MySQL との接続を行っているが、その際にhostとしてapp_dbを指定することで、app_dbコンテナと通信できるようになる。もちろん、Node.js コンテナとapp_dbコンテナが同一のネットワーク(今回の場合はapp_network)に紐付いていることが前提となる。

イメージを作成し、そこからコンテナを作成、起動する。

$ docker build -t app_node_image .
$ docker run --name app_node -d -p 8080:3000 --net app_network app_node_image

無事、Node.js を経由して MySQL からデータを取得することができた。

% curl http://localhost:8080
[{"id":1,"name":"Alice"}]`