Docker でチーム開発用の PHP 8.3 + Apache + Laravel 11 + MySQL 8 + phpMyAdmin 環境を構築

[また今度](https://qiita.com/naente/items/d259ea84c172deeff7d8)が来るまでに 5年かかったゾ。

# 対象読者
- 私
- これまで XAMPP インストールして hosts 書いて httpd.conf に VirtualHost 書いてローカル開発環境を構築していたが、そろそろ Docker に移行したい私
- 上記の工程を開発メンバー各自にも行なってもらっていて、今時 Docker じゃなくてゴメンと思っていた私
- 他の Docker 環境構築記事のレベルが高すぎて理解できなかった私

# 動作確認環境
あらかじめ Docker Desktop のインストールを完了しておいてください。

## Windows
- Windows 10 Enterprise 22H2
- Docker Desktop 4.31.1
  - Docker 26.1.4
  - Docker Compose v2.27.1-desktop.1

## Mac
- macOS Ventura 13.0.1
- Docker Desktop 4.31.0
  - Docker 26.1.4
  - Docker Compose v2.27.1-desktop.1

# 構築内容
- Apache 2.4.59 (Debian)
- PHP 8.3.7
- Composer Latest
- MySQL 8.0.37
- phpMyAdmin


# ディレクトリ構造
以下のように案件リポジトリとして Git 管理する想定。

```
  .
  ├── app/
  │    └── laravel/
  ├── docker/
  │    ├── mysql/
  │    │    ├── data/
  │    │    │    └── .gitignore
  │    │    ├── initdb.d/
  │    │    │    └── initdb.sql
  │    │    ├── Dockerfile
  │    │    └── my.cnf
  │    ├── php/
  │    │    ├── Dockerfile
  │    │    └── php.ini
  │    └── phpmyadmin/
  │         ├── sessions/
  │         │    └── .gitignore
  │         └── Dockerfile
  ├── .git
  └── compose.yaml
```

サンプル
https://github.com/naente/tutorial-docker

## ファイル詳細
```yml:compose.yaml
services:
  db:
    build: ./docker/mysql
    container_name: db-container
    volumes:
      - ./docker/mysql/data:/var/lib/mysql
      - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d
    environment:
      - MYSQL_ROOT_PASSWORD=mysql
      - MYSQL_DATABASE=database
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - TZ=Asia/Tokyo
    ports:
      - '3306:3306'
  app:
    build: ./docker/php
    container_name: app-container
    volumes:
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini
      - ./app/laravel:/var/www
    ports:
      - '8080:80'
    depends_on:
      - db
  phpmyadmin:
    build: ./docker/phpmyadmin
    container_name: phpmyadmin-container
    volumes:
      - ./docker/phpmyadmin/sessions:/sessions
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=db
      - PMA_USER=root
      - PMA_PASSWORD=mysql
    ports:
      - '4040:80'
    depends_on:
      - db
```

### db サービス

`compose.yaml` 内 `db` サービスの `volumes` ではマウントを使用したホスト側へのデータ保存、初期データの投入を設定しています。
初期データの投入はお試しで書いたので、不要であれば割愛して問題ありません。

---

```docker/mysql/data/.gitignore
*
!.gitignore
```
当該フォルダをあらかじめ Git 管理に含めておきたいので `.gitignore` を置きます。

```sql:docker/mysql/initdb.d
CREATE TABLE IF NOT EXISTS pet
(
    name VARCHAR(20),
    owner VARCHAR(20),
    species VARCHAR(20),
    sex CHAR(1),
    birth DATE,
    death DATE
);

INSERT INTO pet
VALUES ('Puffball', 'Diane', 'hamster', 'f', '1999-03-30', NULL);
```
初期データ投入のお試し用です。

```dockerfile:docker/mysql/Dockerfile
FROM mysql:8.0.37

COPY ./my.cnf /etc/mysql/conf.d/my.cnf
RUN chmod 644 /etc/mysql/conf.d/my.cnf
```
Windows 環境の場合、マウントでパーミッション問題が発生するので `Dockerfile` の中で良い感じにしてやります。

参考：[Docker で MySQL コンテナを作る方法](https://lazesoftware.com/ja/blog/230222/)

```docker/mysql/my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4
```
照合順序は信頼と実績の `utf8mb4_general_ci` を採用していますが、お好みでどうぞ。

参考：[MySQL の utf8mb4 の文字照合順序まとめ](https://zenn.dev/zoeponta/articles/090c68ba820a24)

### app サービス

Laravel の場合、ドキュメントルートの変更（下記 `Dockerfile` 参照）が必要です。その上で `public` と同階層のディレクトリおよびファイルもコンテナへ同期する（`compose.yaml` 内 `app` サービスの `volumes` 参照）必要があります。

---

```dockerfile:docker/php/Dockerfile
FROM php:8.3.7-apache

# ドキュメントルートを変更
ENV APACHE_DOCUMENT_ROOT /var/www/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

# パッケージインストール
RUN apt-get update && apt-get install -y \
    libicu-dev \
    libonig-dev \
    libzip-dev \
    zlib1g-dev \
    unzip \
    locales \
    vim \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Apache の rewrite_module を有効化
RUN a2enmod rewrite

# PHP のエクステンションをインストール
RUN docker-php-ext-install \
    # ctype \
    # curl \
    # dom \
    # fileinfo \
    # filter \
    # hash \
    intl \
    # json \
    # mbstring \
    # openssl \
    # pcre \
    # PDO \
    pdo_mysql \
    # pdo_sqlite \
    # session \
    # tokenizer \
    # xml \
    zip

# Composer インストール
COPY --from=composer /usr/bin/composer /usr/bin/composer

# コンテナログイン時のパス指定
WORKDIR /var/www
```
エクステンション関連はお好みで。
上記では Laravel 11 で[必須要件](https://laravel.com/docs/11.x/deployment#server-requirements)となっている拡張と自分がよく使う CodeIgniter 4 で[必須要件](https://codeigniter.com/user_guide/intro/requirements.html)となっている拡張をインストールしています。
※コメントアウトは採用したコンテナイメージで標準インストールされており不要だったものです。

```ini:docker/php/php.ini
[Date]
date.timezone = Asia/Tokyo

[mbstring]
mbstring.language = Japanese
```
ここもお好みで。

### phpmyadmin サービス

`compose.yaml` の `environment` > `PMA_HOST` には接続するデータベースのサービス名、`PMA_PASSWORD` には接続するデータベースサービスの `environment` > `MYSQL_ROOT_PASSWORD` で定義された値を指定する。

---

```docker/phpmyadmin/sessions/.gitignore
*
!.gitignore
```
当該フォルダをあらかじめ Git 管理に含めておきたいので `.gitignore` を置きます。

```dockerfile:docker/phpmyadmin/Dockerfile
FROM phpmyadmin:apache
```

# Docker 動作確認

## 現在のコンテナ稼働状況を確認
```shell
> docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

稼働中のコンテナが存在していないため、何も表示されません。

## コンテナの作成とサービスの開始
```shell
> docker compose up -d
```

コンテナの稼働状況を確認します。

```bash
> docker ps
CONTAINER ID   IMAGE        COMMAND                  CREATED          STATUS          PORTS                               NAMES
227d617fbbfd   phpmyadmin   "/docker-entrypoint.…"   37 seconds ago   Up 24 seconds   0.0.0.0:4040->80/tcp                phpmyadmin-container
d33c7973bec6   app          "docker-php-entrypoi…"   37 seconds ago   Up 26 seconds   0.0.0.0:8080->80/tcp                app-container
b83d0a07dfb0   db           "docker-entrypoint.s…"   39 seconds ago   Up 30 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   db-container
```

### 補足
Mac の場合、以下のようなエラーが発生する可能性があります。
```bash
Error response from daemon: Mounts denied: 
The path /Users/xxx/docker/mysql/initdb.d is not shared from the host and is not known to Docker.
You can configure shared paths from Docker -> Preferences... -> Resources -> File Sharing.
See https://docs.docker.com/desktop/mac for more info.
```
その場合、Docker Desktop の `Settings`（歯車アイコン）から `Resources` > `File sharing` にエラーの発生したディレクトリを追記、`Apply & restart` を押します。

![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243093/0c257159-17e4-faee-066f-53a212ceccb6.png)

ターミナルも再起動して、再度 `docker compose up -d` を実行で解消される筈です。

### Laravel の導入

#### プロジェクトの作成・インストール
チーム開発において、最初の人が実施する想定。

コンテナ内の Composer を利用して Laravel をインストールします。
```shell
> docker container exec -it app-container composer create-project --prefer-dist laravel/laravel .
```

Laravel 11 では `bootstrap/cache` 及び `storage` ディレクトリに Web サーバーからの書き込み権限が必要な為、付与しておきます。

参考：[Deployment - Server Configuration - Directory Permissions](https://laravel.com/docs/11.x/deployment#directory-permissions)

```shell
> docker container exec -it app-container chmod -R 777 storage bootstrap/cache
```

#### インストール
チーム開発において、後続のメンバーはプロジェクト作成済みのリポジトリをクローン後、`composer.lock` の情報を基に Laravel をインストールする想定。

```shell
> docker container exec -it app-container composer install
```

`.env` ファイルの作成とアプリケーションキーの設定を行います。
```shell
> docker container exec -it app-container cp .env.example .env
> docker container exec -it app-container php artisan key:generate
```

#### 共通
Laravel 11 からはデフォルトのデータベース管理システムが SQLite となっています。
本記事では MySQL を採用している為、以下のように `.env` を変更します。

```diff:app/laravel/.env
- DB_CONNECTION=sqlite
- # DB_HOST=127.0.0.1
- # DB_PORT=3306
- # DB_DATABASE=laravel
- # DB_USERNAME=root
- # DB_PASSWORD=
+ DB_CONNECTION=mysql
+ DB_HOST=db
+ DB_PORT=3306
+ DB_DATABASE=database
+ DB_USERNAME=user
+ DB_PASSWORD=password

- SESSION_DRIVER=database
+ SESSION_DRIVER=file
```

#### 未解決
Laravel プロジェクトを作成した人とリポジトリからクローンした人でコンテナ内のパーミッションが異なる問題。
この件、どのように対応するべきなのかは未解決です。

プロジェクトを作成した人
```bash
/var/www# ls -al --time-style="+"
total 316
drwxrwxrwx 1 root root   4096  .
drwxr-xr-x 1 root root   4096  ..
-rw-r--r-- 1 root root    258  .editorconfig
-rw-r--r-- 1 root root   1113  .env
-rw-r--r-- 1 root root   1075  .env.example
-rw-r--r-- 1 root root    186  .gitattributes
-rw-r--r-- 1 root root    258  .gitignore
-rw-r--r-- 1 root root   4109  README.md
drwxr-xr-x 1 root root   4096  app
-rwxr-xr-x 1 root root    350  artisan
drwxr-xr-x 1 root root   4096  bootstrap
-rw-r--r-- 1 root root   1925  composer.json
-rw-r--r-- 1 root root 287463  composer.lock
drwxr-xr-x 1 root root   4096  config
drwxr-xr-x 1 root root   4096  database
-rw-r--r-- 1 root root    244  package.json
-rw-r--r-- 1 root root   1191  phpunit.xml
drwxr-xr-x 1 root root   4096  public
drwxr-xr-x 1 root root   4096  resources
drwxr-xr-x 1 root root   4096  routes
drwxrwxrwx 1 root root   4096  storage
drwxr-xr-x 1 root root   4096  tests
drwxr-xr-x 1 root root   4096  vendor
-rw-r--r-- 1 root root    263  vite.config.js
```

リポジトリからクローンした人
```bash
/var/www# ls -al --time-style="+"
total 316
drwxrwxrwx 1 root root   4096  .
drwxr-xr-x 1 root root   4096  ..
-rwxrwxrwx 1 root root    258  .editorconfig
-rwxr-xr-x 1 root root   1113  .env
-rwxrwxrwx 1 root root   1075  .env.example
-rwxrwxrwx 1 root root    186  .gitattributes
-rwxrwxrwx 1 root root    258  .gitignore
-rwxrwxrwx 1 root root   4109  README.md
drwxrwxrwx 1 root root   4096  app
-rwxrwxrwx 1 root root    350  artisan
drwxrwxrwx 1 root root   4096  bootstrap
-rwxrwxrwx 1 root root   1925  composer.json
-rwxrwxrwx 1 root root 287463  composer.lock
drwxrwxrwx 1 root root   4096  config
drwxrwxrwx 1 root root   4096  database
-rwxrwxrwx 1 root root    244  package.json
-rwxrwxrwx 1 root root   1191  phpunit.xml
drwxrwxrwx 1 root root   4096  public
drwxrwxrwx 1 root root   4096  resources
drwxrwxrwx 1 root root   4096  routes
drwxrwxrwx 1 root root   4096  storage
drwxrwxrwx 1 root root   4096  tests
drwxr-xr-x 1 root root   4096  vendor
-rwxrwxrwx 1 root root    263  vite.config.js
```


#### 補足
Docker を利用した Laravel のローカル開発環境構築には [Laravel Sail](https://github.com/laravel/sail) という選択肢もあるようです。
本記事では Laravel に限定せず Docker での環境構築を学ぶ主目的があった為、使用していません。

### Laravel の確認
[http://localhost:8080/](
http://localhost:8080/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243093/90750a8b-0edd-2b39-6552-455976e21eee.png)

続いてアプリケーションサーバーからデータベースへ通信できることを確認しておきます。

```php:app/laravel/public/db.php
<?php
const DB_HOST = 'db';
const DB_NAME = 'database';
const DB_USER = 'user';
const DB_PASSWORD = 'password';

try {
    $dbh = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME, DB_USER, DB_PASSWORD);
    echo 'Connection successful.';
} catch (PDOException $e) {
    echo $e->getMessage();
    exit;
}
```

接続先のホスト名は `compose.yaml` で定義したサービス名となる点に注意します。

[http://localhost:8080/db.php](
http://localhost:8080/db.php) にアクセスして Connection successful. と表示されていれば、疎通成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243093/7694b890-177d-dbd1-1bcc-f98ac0c04c33.png)

### phpMyAdmin の確認
[http://localhost:4040/](
http://localhost:4040/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243093/44ec1242-41d4-0be9-cead-d4337a044466.png)


## サービスの停止
`compose.yaml` に記載されているサービスのコンテナが全て停止されます。

```shell
> docker compose stop
```

コンテナの稼働状況を確認します。
停止しているコンテナを表示する為に `-a` オプションを使用します。

参考：[docker ps](https://docs.docker.jp/engine/reference/commandline/ps.html)

```shell
> docker ps -a
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS                      PORTS     NAMES
227d617fbbfd   phpmyadmin   "/docker-entrypoint.…"   9 minutes ago   Exited (0) 35 seconds ago             phpmyadmin-container
d33c7973bec6   app          "docker-php-entrypoi…"   9 minutes ago   Exited (0) 35 seconds ago             app-container
b83d0a07dfb0   db           "docker-entrypoint.s…"   9 minutes ago   Exited (0) 30 seconds ago             db-container
```

## サービスの開始
`compose.yaml` に記載されているサービスのコンテナが稼働します。
あらかじめ、コンテナが作成されている必要があります。

```shell
> docker compose start
```

```shell
> docker ps
CONTAINER ID   IMAGE        COMMAND                  CREATED          STATUS          PORTS                               NAMES
227d617fbbfd   phpmyadmin   "/docker-entrypoint.…"   13 minutes ago   Up 45 seconds   0.0.0.0:4040->80/tcp                phpmyadmin-container
d33c7973bec6   app          "docker-php-entrypoi…"   13 minutes ago   Up 45 seconds   0.0.0.0:8080->80/tcp                app-container
b83d0a07dfb0   db           "docker-entrypoint.s…"   13 minutes ago   Up 49 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp   db-container
```

## コンテナの削除
`compose.yaml` に記載されているサービスのコンテナを停止、コンテナとネットワークが削除されます。

```shell
> docker compose down
```

```shell
> docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

# 空白の5年間
いいですか、落ち着いて聞いてください。私が XAMPP を擦り続けている間に

- Docker Compose の `docker-compose` コマンドではなく Docker のサブコマンドである `docker compose` コマンド推奨に変わっていた
  - [Migrate to Compose V2 | Docker Docs](https://docs.docker.com/compose/migrate/)
- 設定ファイルは `docker-compose.yml` ではなく `compose.yaml` 推奨に変わっていた
  - [How Compose works | Docker Docs](https://docs.docker.com/compose/compose-application-model/)
- 設定ファイル内のバージョン指定 `version` が廃止されていた（下位互換性があり、指定していても問題はないが非推奨）
  - [Version and name top-level elements | Docker Docs](https://docs.docker.com/compose/compose-file/04-version-and-name/)

# おわり
バイバイ XAMPP ワールド

![xampp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243093/27a86c2c-f747-0df3-9baa-577cce5ae179.jpeg)