Docker Compose を使わずに App / DB / Mail コンテナの開発環境を構築する

# はじめに
この記事では、App / DB / Mail コンテナの開発環境の構築手順について記載します。
今回は Docker Compose を使わずに構築します。

※Docker Compose を使った構築手順については、こちらの記事に記載しています。

https://qiita.com/Yasushi-Mo/items/d2e920112adc6e31e4e2

# 開発環境
開発環境は以下の通りです。

- Windows 11
- Docker Engine 26.1.1
- PHP 8.3
- MySQL 8.4.0
- Mailpit 1.18

# 要件を整理する
まず今回構築するコンテナの要件を整理します。

## 全体
- コンテナ停止時に自動削除
- コンテナはバックグラウンド実行
- 日本標準時で動作
- コンテナ間通信可能

## App コンテナ `app`
- ブラウザでアクセス可能
- ソースコードはホストマシンで実装

## DB コンテナ `db`
- デバッグ用にホストマシンから接続可能
- データはコンテナを削除しても残す
- 初期データを用意

## Mail コンテナ `mail`
- ブラウザでアクセス可能
- メールデータはコンテナを削除しても残す

# ディレクトリを準備する
今後 Dockerfile や各種設定ファイルなどを作成していくために以下の構成のディレクトリを準備します。

```powershell
├─docker
│  ├─app
│  ├─db
│  └─mail
└─src
```

# 各コンテナのイメージを準備して、コンテナを起動する
次は各コンテナのイメージを準備して、コンテナを起動してみます。

## App コンテナ `app`
ベースイメージはタグが [8.3](https://hub.docker.com/layers/library/php/8.3/images/sha256-9d55083779a32244cae2d874020ce9b7d58bf915b885d2038f2d7cd0e55a2bde?context=explore) の PHP イメージを利用します。

https://hub.docker.com/_/php

また、MySQL サーバーとの接続やメール送信のための SMTP クライアント用に拡張モジュールが必要なため、`/docker/app` ディレクトリに Dockerfile を作成し、ベースイメージを拡張・ビルドします。

 ```Dockerfile
 FROM php:8.3

RUN docker-php-ext-install pdo_mysql

RUN apt-get update
RUN apt-get install -y msmtp-mta

COPY ./msmtprc /etc/msmtprc

CMD ["/usr/local/bin/php", "--server", "0.0.0.0:8000", "--docroot", "/my-work"]
 ```

- `RUN docker-php-ext-install pdo_mysql` : MySQL を利用するためのモジュール `PDO_MYSQL` をインストール

https://www.php.net/manual/en/ref.pdo-mysql.php

- `RUN apt-get install -y msmtp-mta` : SMTP クライアント `msmtp` をインストール

https://marlam.de/msmtp/

- `COPY ./msmtprc /etc/msmtprc` : メールサーバーのホストなどを指定するための設定ファイル `msmtprc` をホストマシンの `./msmtprc` からイメージの `/etc/msmtprc` にコピー
- `CMD ["/usr/local/bin/php", "--server", "0.0.0.0:8000", "--docroot", "/my-work"]` : コンテナ起動時、ビルドイン Web サーバーを起動
  - `--server` オプションで起動するアドレスとポートを指定
  - `--docroot` オプションでドキュメントルートを指定

https://www.php.net/manual/en/features.commandline.webserver.php

:::note
php コマンドの絶対パスは、`which` コマンドで確認します。

```powershell
docker run --rm php:8.3 which php
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/a150fcd9-6442-80b8-dff1-3bb1dac8f59f.png)

:::

次は、上記の `COPY` インストラクションでコピー元となる `msmtprc` を`/docker/app` ディレクトリに作成します。

```msmtprc
host mail
port 1025
from "service@example.com"
timeout 5
```

- `host` : Mail コンテナ
- `port` : Mail コンテナのポート
- `from` : メールの送信元アドレス

:::note
`port` の `1025` は、Mail イメージとして利用する Mailpit の SMTP サーバーのデフォルト値になります。[^1]
:::

[^1]:https://mailpit.axllent.org/docs/configuration/runtime-options/

必要なファイルが揃ったので、Dockerfile からイメージをビルドします。

```powershell
docker image build --tag app:0.1.0 docker/app
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/78b6fbc9-93ab-1d7e-0b78-356fbfcbb48d.png)

イメージがビルドできたか確認します。

```powershell
docker image ls app
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/b04b9602-8691-c45d-8db5-5f0e1f9d79e9.png)

ビルドしたイメージをもとに App コンテナを起動します。

```powershell
docker container run `
--name app           `
--rm                 `
--detach             `
--publish 8000:8000  `
app:0.1.0            `
/usr/local/bin/php --server 0.0.0.0:8000 --docroot /
```

:::note
Dockerfile にドキュメントルートとして記載した `/my-work` ディレクトリはまだ作成していないので、現時点では `/` としています。
:::

コンテナが起動できている場合、ブラウザで http://localhost:8000/ にアクセスすると、以下の画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/cdbc0480-19d9-a885-f541-6b6aaf976f17.png)


## DB コンテナ `db`
ベースイメージはタグが [`8.4.0`](https://github.com/docker-library/mysql/blob/319db566ac7fef45c22f3df15ee5e194a7c43259/8.4/Dockerfile.oracle) の MySQL イメージを利用します。

https://hub.docker.com/_/mysql

以下のコマンドでコンテナを起動します。

```powershell
docker container run               `
--name db                          `
--rm                               `
--detach                           `
--env MYSQL_ROOT_PASSWORD=password `
--env MYSQL_USER=app               `
--env MYSQL_PASSWORD=password      `
--env MYSQL_DATABASE=sample        `
--env TZ=Asia/Tokyo                `
--publish 3306:3306                `
mysql:8.4.0
```

コンテナが起動できている場合、ホストマシンから DB コンテナに接続できます。

```powershell
mysql --host=127.0.0.1 --port=3306 --user=app --password=password sample
```

また、DB コンテナが日本標準時になっています。

```mysql
select now();
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/6c1d3c61-a0b7-20b2-70c9-b0f7ef6e0f3c.png)


## Mail コンテナ `mail`
ベースイメージはタグが [`v1.18`](https://hub.docker.com/layers/axllent/mailpit/v1.18/images/sha256-fe34d4ac7f071e4fce8a1d2123aefa988df4326e924ed3648a93325585c23d03?context=explore) の Mailpit イメージを利用します。

https://hub.docker.com/r/axllent/mailpit

以下のコマンドでコンテナを起動します。

```powershell
docker container run `
--name mail          `
--rm                 `
--detach             `
--env TZ=Asia/Tokyo  `
--publish 8025:8025  `
axllent/mailpit:v1.18
```

:::note
Web UI にホストマシンのブラウザからアクセスできるようにコンテナの `8025` ポートをホストマシンの `8025` ポートにマッピングしています。`8025` は Mailpit の Web UI のデフォルト値です。[^2]
:::
[^2]:https://mailpit.axllent.org/docs/configuration/runtime-options/

コンテナが起動できている場合、ブラウザで http://localhost:8025/ にアクセスすると、以下の画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/a5c02313-1234-ae97-4fca-cedaa39d9131.png)

:::note
`docker container ls` でコンテナが3つ起動していることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/6c096c64-d20c-d1e9-7075-47bfe48ba45d.png)
:::

起動確認できたので、コンテナを全て停止します。

```powershell
docker container stop app db mail
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/92b416e5-734f-f1b9-1309-e46a797302fd.png)


# 要件の達成状況を確認する
現時点での要件の達成状況を確認します。

## 全体
- ✅コンテナ停止時に自動削除
- ✅コンテナはバックグラウンド実行
- ✅日本標準時で動作
- コンテナ間通信可能

## App コンテナ `app`
- ✅ブラウザでアクセス可能
- ソースコードはホストマシンで実装

## DB コンテナ `db`
- ✅デバッグ用にホストマシンから接続可能
- データはコンテナを削除しても残す
- 初期データを用意

## Mail コンテナ `mail`
- ✅ブラウザでアクセス可能
- メールデータはコンテナを削除しても残す


# 各コンテナで必要なリソースを準備する
残りの要件を満たすため、各コンテナで必要なリソースを準備します。

## App コンテナ `app`
App コンテナで満たす必要がある残りの要件は、以下の2つです。

- コンテナ間通信可能
- ソースコードはホストマシンで実装


### ネットワーク
コンテナ間通信のため、`work-network` というネットワークを作成します。

```powershell
docker network create work-network
```

作成できたか確認します。

```powershell
docker network ls --filter name=work-network
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/213683c1-337c-bbed-ffe5-4ca4d1b64769.png)

コンテナ起動時のパラメーターに `--network` オプションを追加し、作成した `work-network` へ接続します。

- ✅コンテナ間通信可能

### ソースコード
メールを送信するソースコードを実装します。
ホストマシンの `src/index.php` にファイルを追加して実装します。

```index.php
<?php

$users = [];

try {
  // Connect to DB
  $dsn = 'mysql:host=db;port=3306;dbname=sample';
  $username = 'root';
  $password = 'password';
  $pdo = new PDO($dsn, $username, $password);

  // Get users table
  $statement = $pdo->query('select * from users');
  $statement->execute();
  while ($row = $statement->fetch()) {
    $users[] = $row;
  }

  // Disconnect
  $pdo = null;
} catch (PDOException $e) {
  echo '<p>Database connection failed: ' . $e . '</p>';
}
// Display users information
foreach ($users as $user) {
  echo '<p>id: ' . $user['id'] . ', name: ' . $user['name'] . '</p>';
}

// Send mail
$subject = 'Test Mail';
$message = 'Here is Docker Hub => https://hub.docker.com/';
foreach ($users as $user) {
  $success = mb_send_mail($user['email'], $subject, $message);
  if ($success) {
    echo '<p>Mail sent to ' . $user['name'] . '</p>';
  } else {
    echo '<p>Mail sending failed</p>';
  }
}

```

### バインドマウント
ホストマシンに追加したソースコードをコンテナと共有するため、バインドマウントを利用します。
コンテナ起動時のパラメーターに `--mount` オプションを追加します。オプションの `<key>` / `<value>` は以下の通りです。

- `type=bind`
- `source="$(pwd)" /src`
- `target=/my-work`

:::note
マウント先の `/my-work` は、Dockerfile にドキュメントルートとして記載したディレクトリです。
:::

- ✅ソースコードはホストマシンで実装

## DB コンテナ
DB コンテナで満たす必要がある残りの要件は、以下の2つです。

- コンテナ間通信可能
- データはコンテナを削除しても残す
- 初期データを用意

### ネットワーク
コンテナ間通信のため、App コンテナと同じネットワークに接続する必要があります。
コンテナ起動時のパラメーターに `--network` オプションを追加し、`work-network` へ接続します。

- ✅コンテナ間通信可能

### ボリューム
データが消えないようにボリュームを利用します。
まず DB コンテナ用のボリュームを作成します。

```powershell
docker volume create --name work-db-volume
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/d5ee263d-89ac-766f-85b8-91f73a2a4573.png)

次にコンテナ起動時のパラメーターへ `--mount` オプションを追加し、`work-db-volume` をマウントします。オプションの `<key>` / `<value>` は以下の通りです。

- `type=volume`
- `source=work-db-volume`
- `target=/var/lib/mysql`

:::note
マウント先の `/var/lib/mysql` は、MySQL サーバーがデータを保存するデフォルトのディレクトリになります。MySQL 設定ファイル（`/etc/my.cnf`）で定義されています。

```powershell
docker container run --rm mysql:8.4.0 cat /etc/my.cnf
```

```powershell
[mysqld]
...
datadir=/var/lib/mysql
...
```
:::

- ✅データはコンテナを削除しても残す

### 初期データ
初期データとしてコンテナ起動時、`users` テーブルを作成し、Wyatt と Billy を登録するようにします。
上記のクエリをコンテナ起動時に実行する SQL ファイルをホストマシンの `/docker/init/init-users.sql` に作成します。

```init-users.sql
create table users(id int, name varchar(32), email varchar(32));
insert users (id, name, email) values (1, "Wyatt", "wyatt@example.com");
insert users (id, name, email) values (2, "Billy", "billy@example.com");
```

### バインドマウント
ホストマシンに追加した SQL ファイルをコンテナと共有するため、バインドマウントを利用します。
コンテナ起動時のパラメーターに `--mount` オプションを追加します。オプションの `<key>` / `<value>` は以下の通りです。

- `type=bind`
- `source="$(pwd)"/docker/db/init`
- `target=/docker-entrypoint-initdb.d`

:::note
マウント先の値 `/docker-entrypoint-initdb.d` は、Docker Hub の MySQL リポジトリの Initializing a fresh instance に記載があります。[^3]
:::
[^3]:https://hub.docker.com/_/mysql

- ✅初期データを用意

## Mail コンテナ `mail`
Mail コンテナで満たす必要がある残りの要件は、以下の2つです。

- コンテナ間通信可能
- メールデータはコンテナを削除しても残す

### ネットワーク
コンテナ間通信のため、App / DB コンテナと同じネットワークに接続する必要があります。
コンテナ起動時のパラメーターに `--network` オプションを追加し、`work-network` へ接続します。

- ✅コンテナ間通信可能

### ボリューム
データが消えないようにボリュームを利用します。
まず Mail コンテナ用のボリュームを作成します。

```powershell
docker volume create --name work-mail-volume
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/669d4906-5b6d-2005-db28-1acd79511d5e.png)

次にコンテナ起動時のパラメーターへ `--mount` オプションを追加し、`work-mail-volume` をマウントします。オプションの `<key>` / `<value>` は以下の通りです。

- `type=volume`
- `source=work-mail-volume`
- `target=/data`

:::note
マウント先の `value` である `/data` は、Docker Hub の Mailpit リポジトリの Setting Mailpit options のサンプルでデータの保存先を指定する環境変数 `MP_DATABASE` と同じにしています。[^4]

```powershell
docker run -d \
--name=mailpit \
--restart unless-stopped \
-v /path/to/mailpit-data:/data \
-e MP_DATABASE=/data/mailpit.db \
-e MP_UI_AUTH_FILE=/data/authfile \
-e TZ=Europe/London \
-p 8025:8025 \
-p 1025:1025 \
axllent/mailpit
```
:::

[^4]:https://hub.docker.com/r/axllent/mailpit

- ✅メールデータはコンテナを削除しても残す

# コンテナを起動する
全ての要件を満たすことができたので、3つのコンテナを起動します。

## App コンテナ `app`

```powershell
docker container run                                      `
--name app                                                `
--rm                                                      `
--detach                                                  `
--publish 8000:8000                                       `
--mount type=bind,source="$(pwd)"/src,target=/my-work     `
--network=work-network                                    `
app:0.1.0                                                 `
/usr/local/bin/php --server 0.0.0.0:8000 --docroot /my-work
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/6a52ca3c-57df-138c-31f2-3ee62cc5d613.png)

コンテナの起動状況を確認します。

```powershell
docker container ls
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/d5f58ad9-fb53-85d9-1957-acc43cfce85b.png)

## DB コンテナ `db`

```powershell
docker container run               `
--name db                          `
--rm                               `
--detach                           `
--env MYSQL_ROOT_PASSWORD=password `
--env MYSQL_USER=app               `
--env MYSQL_PASSWORD=password      `
--env MYSQL_DATABASE=sample        `
--env TZ=Asia/Tokyo                `
--publish 3306:3306                `
--mount type=volume,source=work-db-volume,target=/var/lib/mysql `
--mount type=bind,source="$(pwd)"/docker/db/init,target=/docker-entrypoint-initdb.d `
--network=work-network             `
mysql:8.4.0
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/e81d3c63-9a38-9429-0508-c56c7f04b9b9.png)

コンテナの起動状況を確認します。

```powershell
docker container ls
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/0f812396-840b-4443-43b3-e9a171eff555.png)

## Mail コンテナ `mail`

```powershell
docker container run                                          `
--name mail                                                   `
--rm                                                          `
--detach                                                      `
--env TZ=Asia/Tokyo                                           `
--env MP_DATABASE=/data/mailpit.db                            `
--publish 8025:8025                                           `
--network=work-network                                        `
--mount type=volume,source=work-mail-volume,target=/data `
axllent/mailpit:v1.18
```

コンテナの起動状況を確認します。

```powershell
docker container ls
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/107f426b-a70a-d4f8-93da-ddeca864515c.png)

# 動作確認
ブラウザで動作確認をします。

App コンテナ（ http://localhost:8000/ ）にアクセスすると、以下の画面が表示されます。
ユーザー一覧と各ユーザーにメールを送信したことが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/a96872d9-bda6-ce73-8f31-9e8810b87e86.png)

Mail コンテナ（ http://localhost:8025/ ）にアクセスすると、以下の画面が表示されます。
2通のメールが確認できます。また、メールのタイトル、本文ともに実装通りの文言になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/fdefca55-49bc-76c3-ca75-3a772735453a.png)

# 所感

- Docker を利用した Web サービスの開発環境構築の全体像の理解度が向上した
- コンテナ一つひとつ管理するのは大変（Docker Compose 便利）[^5]


[^5]:環境をはじめて起動するために必要なコマンド実行は全部で7回になります（`docker image build` : 1、`docker volume create` : 2、`docker network create` : 1、`docker container run` : 3）。また、`docker container run` のパラメーターは、30 も指定しています。
:::

# 参考
- [Built-in web server](https://www.php.net/manual/en/features.commandline.webserver.php)
- [msmtp](https://marlam.de/msmtp/)
- [MySQL Functions (PDO_MYSQL)](https://www.php.net/manual/en/ref.pdo-mysql.php)
- [mysql - Official Image](https://hub.docker.com/_/mysql)
- [php - Official Image](https://hub.docker.com/_/php)
- [Runtime options | Mailpit](https://mailpit.axllent.org/docs/configuration/runtime-options/)
- [	鈴木亮「開発系エンジニアのためのDocker絵とき入門」秀和システム、2024](https://www.shuwasystem.co.jp/book/9784798071503.html)

# 関連URL

https://github.com/yasushi-mo/docker-mail-app


https://qiita.com/Yasushi-Mo/items/d2e920112adc6e31e4e2