サイドスリーブログ

Docker Composeで簡単便利な砂場作り


                Docker Composeで簡単便利な砂場作り

プログラマーのやまさんです。
今回は黒い画面もといCUIも操作する話なので抵抗のある方も多いかもしれませんが、使えるようになるととても便利なツールについてなので、ぜひお付き合いください。

みなさんはローカルの開発環境ってどうやって準備していますか?
本ブログにも過去にamppsでローカル環境を作成する記事がありましたが、今回はこの記事でも少し触れられていたDocker、もう少し踏み込んでDocker Composeを使って、「かゆいところに手が届く」仮想のLinuxサーバーをローカル上に作ります。個人や中小企業なら無料で使用できます。

とりあえず最低限のPHPが動けばいいという方は基本編だけ、以降の応用編の設定は必要に応じて追加していってください。

※Windowsの場合
筆者はホストPC(Dockerの外)がMacの環境で作業しているため、Windowsの場合の内容に不足があるかもしれませんが、ターミナル→コマンドプロンプトやPowerShellなど、適宜読み替えてください。また、Windowsには事前にWSL2やLinuxディストリビューションなどがインストールおよび有効化されている必要があります。
Windowsの事前準備

(また、Windows環境の場合の手順にとどまらず、本記事の内容全体を検証してくれたサイドスリー2人目のプログラマーの「シェフ」、ありがとうございました!)

基本編 〜 まずは簡単に動かす 〜

最初に、Docker Desktopをインストールします。

※Windowsの場合
インストール時の設定の選択肢に「WSL2」と書かれたものがあり、チェックされていることを確認してください。「Hyper-V」という選択肢が出る場合もありますが、WSL2の選択肢があればそちらだけで大丈夫です。
これは歴史的経緯から来る実行環境の違いですが、現在公式が推奨しているものはWSL2版なので、以降はそちらを使う前提で書いていきます。Hyper-V版を使う場合は少し以降の手順が変わるかもしれません。

インストールできたら起動してみます。
初回起動時にはチュートリアルが表示されます。今回はスキップしますが、このチュートリアルはメニューからいつでも呼び出すことができます。WindowsならLearn、MacならQuick Start Guideと書いてあります。
起動後、しばらくしたらステータスがrunningになることを確認してください。

Docker Desktop is running
RUNNING

※Macの場合
実際にファイルを置いていく前に、Docker Desktopで、歯車アイコン→Resources→FILE SHARINGを見て、
これからファイルを配置する予定のディレクトリがこのリストの中になければ追加します。
今回のサンプルではディレクトリをホームディレクトリ直下に置くことにしていますが、ホームディレクトリは /Users の中なので、追加不要ですね。
Windows版の場合もこの設定項目自体がなく、Cドライブ全体が設定されている扱いになっているため、不要だそうです。

ファイル共有の設定

好きなディレクトリにこんな感じでファイルを配置します。
すべて自分で準備する場合は、ファイルの中身はこのあと書いていくので一旦空で大丈夫ですが、ちょっとこの画像だけでは分かりづらいかもしれないので、最初に作るサンプル構成のファイルもあらかじめ用意しました。
基本編サンプルファイル一式

最初に準備するディレクトリの構造

index.phpは一旦あとにして、docker-compose.ymlとDockerfileという2つの設定ファイルに中身を書きます。
docker-compose.ymlはDocker Compose全体の、DockerfileはDockerイメージを個別に作成(ビルド)するときの設定ファイルです。
ymlという拡張子は馴染みがない方も多いかもしれませんが、YAMLという形式で記述します。Dockerfileには独自の記法を使います。

docker-compose.yml

# YAMLでは井桁から行末までがコメントになります
# 2回出てきている ./ の部分はホストPCの
# docker-compose.ymlがあるディレクトリを示しています
version: '3'
services:
  php_service:
    container_name: sample_php_container
    build:
      context: './php/'
    volumes:
      # コロンより後ろは作成するコンテナ側のディレクトリです
      - './php/html:/var/www/html'
    ports:
      - '80:80'

Dockerfile

# 厳密には細かなルールもありますが、
# Dockerfileも基本的には井桁から始まる行がコメントです
# PHPのバージョンはお好みでどうぞ
FROM php:8.0-apache

さあ、いよいよサーバーの起動です!
ターミナルを開いてcdコマンドで作成したディレクトリの中へ移動し、起動コマンドdocker-compose up --detachを実行しましょう!

だだーっとログが流れたあと、最後に done と表示されれば成功です!

$ cd ~/docker_sample
$ docker-compose up --detach

早速ブラウザからhttp://localhost/にアクセスしてみます。
サンプルzipファイルを使用していない方はまだindex.phpの中身が空なので、何も表示されないかと思います。
phpinfo()で、PHPの設定を表示してみます。

index.php

<?php
phpinfo();

ブラウザをリロードしてみて、Dockerfileに書いたPHPのバージョンなどが表示されましたか?

以上で基本編は終了です。お疲れさまでした!
応用編へ進む方は表示されているScan this dir for additional .ini filesの値を控えておいてください。あとで使います。

phpinfo

応用編1 〜 httpsリダイレクト 〜

いま作成したサーバーは最低限の設定ということでHTTPでだけアクセスできるようになっていますが、今どきの実運用サーバーでは常時SSL化されていることが普通です。
まずは作成した開発用サーバーにSSL接続できるようにし、さらにhttpでアクセスしてもhttpsにリダイレクトされるようにしてみましょう。

新しい設定でサーバーを作り直すので、今作った分はdocker-compose downで一旦消してしまいましょう。

サーバーにSSL接続できるようにするにはSSL証明書が必要です。
正式に公開するようなサーバーには信頼できる発行元の証明書が必要ですが、今回はローカルのテスト環境なのでローカル認証局の証明書で大丈夫です。

$ docker-compose down
$ brew install mkcert
$ cd php/
$ mkcert -install
$ mkcert localhost

ホストPCにWindowsならChocolatey、MacならHomebrewなどのパッケージ管理ツールをあらかじめインストールしておき、それらを使ってmkcertをインストールします。Mac(Homebrew)だとbrewとしているコマンド部分が、Windows(Chocolatey)だとchocoのようになります。
ホストにmkcertをインストールできたら、Dockerfileがあるのと同じディレクトリの中でlocalhostというホスト名用の証明書と鍵を発行します。
localhost.pem, localhost-key.pemという2つのファイルが作成されるはずです。

※パッケージ管理ツールについて
パッケージ管理ツールって何?と思った方もいるかもしれません。本題ではないのでここでは詳しく触れませんが、これもあると便利なものなので使ったことがない方はこの機会についでに覚えてみてください。今回はホストPCで使っていますが、Dockerfileの中でaptを使ってコンテナの中に必要なライブラリを追加したりということもよくあります。

設定ファイルに追記していきます。

ホストPCからDocker内のサーバーへアクセスするには、Docker内からホストPCへポートを公開する必要があります。非SSL接続のみの場合は80番ポートだけで大丈夫でしたが、今度はSSL接続したいので公開ポートのリストに443番を追加します。

先程ホストPC側で作成した証明書と鍵をDocker側で読み込むことも忘れずに。COPYという命令でホスト側のファイルをDockerイメージ内に取り込むことができます。このときホスト側のパスはDockerfile自体があるディレクトリをトップとして、それより上に置いてあるファイルは読み込めないので注意してください。
あとは、RUN命令内に各種設定を有効化するコマンドを書きます。a2enmodコマンドでApacheのSSLモジュールが、a2ensiteコマンドでデフォルトSSLサイトが有効になります。

docker-compose.yml

version: '3'
services:
  php_service:
    container_name: sample_php_container
    build:
      context: './php/'
    volumes:
      - './php/html:/var/www/html'
    # ホストとコンテナの80番・443番ポートをそれぞれマッピングします
    # 左がホスト側のポート、右がコンテナ側のポートです
    ports:
      - '80:80'
      - '443:443'

Dockerfile

FROM php:8.0-apache
# こちらも左がホスト側のファイル、右がイメージ内のファイルのパスです
COPY localhost.pem /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY localhost-key.pem /etc/ssl/private/ssl-cert-snakeoil.key
# RUN命令にはイメージ内で実行したいコマンドを書きます
# apt(apt-get)などを使う場合もここです
RUN a2enmod ssl && a2ensite default-ssl

もう一度サーバーを起動(docker-compose up --detach)して、ChromeやEdge、Safariから(Firefoxの場合のみ手順が増えるのでそれ以外のブラウザで)HTTPSで最初に作ったPHPのページにアクセスすると、ちゃんと同じページがSSLで表示されるかと思います。
ただ、現状ではまだ非SSLでもアクセスできてしまうので、次はHTTPSリダイレクト設定を追加してみます。

$ cd ../
$ docker-compose build
$ docker-compose up --detach

Apacheのリダイレクト機能が使えるよう、Rewriteモジュールを有効化します。

Dockerfile

FROM php:8.0-apache
COPY localhost.pem /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY localhost-key.pem /etc/ssl/private/ssl-cert-snakeoil.key
RUN a2enmod ssl rewrite && a2ensite default-ssl

htmlディレクトリの中にhttpsリダイレクトのおまじないを書いたおなじみの.htaccessを置きましょう。
そして、これを置いただけでApacheのrewriteモジュールを有効化していない状態ではまだ動かないので、設定を反映するために先程と同じように今のサーバーを消して作り直します。そろそろ慣れたと思いますがdownしてupです。
httpでアクセスしてもhttpsへリダイレクトされるようになっているはずです。

.htaccess

RewriteEngine on
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

応用編2 〜 メール送信テスト 〜

ここまではPHPのコンテナのみを扱ってきましたが、Docker Composeというのは複数のDockerコンテナを組み合わせて使うためのツールです。
今度はDockerの中だけでメール送信テストができる環境を作っていきましょう。
MailHogというツールを使います。

まず、mailhog.iniというファイルを作り、PHPイメージのDockerfileも修正します。具体的にはmailhog.iniでPHPサーバとなるコンテナからメールサーバとなるコンテナへの宛先を指定し、 Dockerfileでその設定ファイルをPHPの追加設定ファイル用のディレクトリへ取り込むように設定します。
ここで基本編の最後に控えたScan this dir for additional .ini filesがPHPの追加設定ファイル用ディレクトリのパスなので、 この例の中で/usr/local/etc/php/conf.d/となっているパスが控えてあるものと違った場合はそれに修正してください。

mailhog.ini

[mail function]
sendmail_path = "/usr/local/bin/mhsendmail --smtp-addr=mailhog_service:1025"

php/Dockerfile

FROM php:8.0-apache
COPY localhost.pem /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY localhost-key.pem /etc/ssl/private/ssl-cert-snakeoil.key
COPY mailhog.ini /usr/local/etc/php/conf.d/mailhog.ini
# 専用のメール送信コマンドをダウンロードして使えるようにします
RUN a2enmod ssl rewrite && a2ensite default-ssl && \
    curl --location --output mhsendmail https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64 && \
    chmod +x mhsendmail && \
    mv mhsendmail /usr/local/bin/mhsendmail

ところで、するどい方は応用編1でお気づきかもしれませんが、ひとつのRUN命令の中に複数のコマンドを連結しています。実は、RUNは本来COPYと同様に複数書くことができますが、増えれば増えるほど重くなるという弱点があります。数行ぶんくらいではあまり差は出ないので分けて書いてしまっても問題ないのですが、ここは通例に従って1文にまとめてしまいましょう。
続けて実行したいコマンドは && で繋ぎ、改行前にはバックスラッシュを入れます。

※Dockerfileのヒアドキュメント
余談ですが、近い将来にヒアドキュメント構文が追加され && \が不要な書き方もできるようになるそうです。ヒアドキュメントといえばPHPでもときどき使う、複数行テキストを扱うときに便利な書き方ですね。
2, 3行で済んでいるうちはまだいいですが、書くことが増えてくるとときどき記号をつけ忘れたりして少し面倒に思うこともあったりするので、この新しい書き方が使えるようになるのは個人的には地味ながらかなり嬉しいです。

RUN <<EOF
a2enmod ssl rewrite
a2ensite default-ssl
curl --location --output mhsendmail https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64
chmod +x mhsendmail
mv mhsendmail /usr/local/bin/mhsendmail
EOF

今度は、docker-compose.ymlと同じ階層にmailhogというディレクトリを作り、phpのディレクトリと同じようにDockerfileを置いてください。

最後に、PHPのサーバー内に簡単なメール送信プログラムを置きましょう。

メール送信テスト用の設定ファイル等を追加

docker-compose.yml

version: '3'
services:
  php_service:
    container_name: sample_php_container
    build:
      context: './php/'
    volumes:
      - './php/html:/var/www/html'
    ports:
      - '80:80'
      - '443:443'
  # 新しいコンテナ用の設定を追加します
  mailhog_service:
    container_name: sample_mailhog_container
    build:
      context: './mailhog/'
    ports:
      - '8025:8025'

mailhog/Dockerfile

# PHPではバージョンを指定していた部分が
# ここではlatestになっていますが、
# 基本的にはどのイメージでも
# latestを指定することで最新バージョンを使用できます
FROM mailhog/mailhog:latest

mail.php

<?php
// アクセスするとfromからtoへメールを送信するだけのごく簡単なプログラム
$from = 'from_address@example.com';
$to = 'to_address@example.com';

mb_send_mail($to, 'テストメール', 'こんにちは!', "From: {$from} \r\n");

再起動して、メール送信プログラムを動かしてみます。
宛先部分にはあなたのメールアドレスを設定してあったことと思いますが、いつものメールボックスにメールは届いていないはずです。
焦る必要はありません。今度はhttp://localhost:8025/にアクセスしてみます。今送信したメールはこの画面で見ることができます。

このDockerのPHPから送信されたメールは、送信元・宛先のアドレスにかかわらずすべてMailHogコンテナ内に溜めこまれるようになっているので、実際の宛先にメールを送ることなくPHPプログラムのメール送信テストができます。

応用編3 〜 MySQLとphpMyAdmin 〜

応用編のラストです。PHPからMySQLのデータベースを操作できるようにします。また、それだけだとデータベースの中身が分かりづらいので、ついでにphpMyAdminも追加します。

phpディレクトリの中身は一旦そのままです。MySQLとphpMyAdminのためのディレクトリやファイルを配置して、またまたdocker-compose.ymlに追記していきます。
データベース用のDockerfileでは、MySQLのバージョンもPHPと同じように指定できます。便宜上MySQLと言っていますが、MySQLと互換性があるMariaDBのイメージもほぼ同じように使うことができます。
環境変数の設定にはENV命令を使います。
entrypointという名前のディレクトリの中にSQLファイルを入れていますが、こうすることでentrypointディレクトリの中のファイルの内容のテーブルをDockerfileで指定したデータベースの中に自動的に準備してくれます。
phpMyAdminのバージョンは古いものを使う理由はとくにないので、MailHogと同様にlatastでいいでしょう。

データベース関連の設定ファイル等を追加

docker-compose.yml

version: '3'
services:
  (中略)
  mysql_service:
    container_name: sample_mysql_container
    build:
      context: './mysql/'
  phpmyadmin_service:
    container_name: sample_phpmyadmin_container
    build:
      context: './phpmyadmin/'
    # 80番ポートはPHP本体サーバにアクセスするときに使っているので、
    # ここではphpMyAdminにアクセスするのに8080番を使います
    ports:
      - '8080:80'

mysql/Dockerfile

FROM mysql:8.0
# あるいは FROM mariadb:10.6 など
# いい感じのパスワードを設定してください
ENV MYSQL_ROOT_PASSWORD='********'
ENV MYSQL_DATABASE='sample_db'
COPY entrypoint/ /docker-entrypoint-initdb.d/

member.sql

-- 例としてIDと名前が3行入ったテーブルを用意しています
SET NAMES utf8;

CREATE TABLE `member` (
  `id` int NOT NULL PRIMARY KEY,
  `name` text
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

INSERT INTO `member` (`id`, `name`) VALUES
(1, '佐藤'),
(2, '鈴木'),
(3, '高橋');

phpmyadmin/Dockerfile

FROM phpmyadmin/phpmyadmin:latest
# 環境変数でホスト名としてサービス(コンテナ)を指定します
ENV PMA_HOST='mysql_service'

再起動して、今度はhttp://localhost:8080/にアクセスしてみます。
ユーザ名はroot、パスワードはさきほど設定したパスワードでログインします。ログインできるようになるには少し時間がかかるので、もしログインできないようなら1, 2分待ってから試してみてください。
ログインすると、各ファイルで設定したようにデータベースやテーブル、中身のデータが入っていることが確認できます。

このデータベースはありがたいことに、いくらこのphpMyAdminや自作のPHPプログラムからINSERTUPDATEDELETEなどで中身を変更、あるいはALTER TABLEなどで構造まで編集しても、再起動するたびにSQLファイルを読み込んで作り直すので、読み込む元のSQLファイルが同じ限りすべて元通りになります。
元に戻したくなったらそのまま再起動、再起動しても編集後の状態を維持したければ読み込み元のSQLファイルをphpMyAdminからエクスポートしたものに入れ替えればいいというわけです。

データが自動で入っています

phpMyAdminが入っているサーバーと最初に作ったサーバーは別々に動いているので、最初のPHPサーバーからもMySQLにアクセスできるようにします。PHPからMySQLを扱うにはPDOやmysqliなどのモジュールが必要ですが、PHPのデフォルトDockerイメージにはどちらも入っていないので、Dockerfileに専用のコマンドdocker-php-ext-installを書き足してインストールします。
再起動すると、phpMyAdminから見えていたテーブルの中身をPHPのプログラムからも見ることができるようになります。

php/Dockerfile

FROM php:8.0-apache
COPY localhost.pem /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY localhost-key.pem /etc/ssl/private/ssl-cert-snakeoil.key
COPY mailhog.ini /usr/local/etc/php/conf.d/mailhog.ini
RUN (中略) && \
    docker-php-ext-install pdo_mysql

database.php

<?php
// サンプルテーブルをvar_dump()するだけのプログラム
$dsn = 'mysql:' . implode(';', array(
  'host=mysql_service',
  'dbname=sample_db',
));
$username = 'root';
$password = '********';
$options = array(
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
);
$dbh = new PDO($dsn, $username, $password, $options);
$sth = $dbh->prepare('SELECT * FROM member');
$sth->execute();
echo '<pre>';
var_dump($sth->fetchAll());
echo '</pre>';

色々試してみてください

最初は簡単にというつもりでしたがとんでもなく長くなってしまいました。どこが簡単だ!と思った方はすみません、書き終えて私も思っています。
ざっくり読み飛ばされた方がほとんどかと思いますが、ここまですべて実践してくださった方がもしいらっしゃればありがとうございます、本当にお疲れさまでした。
各編ごとにやりたいことを実現するための最低限の内容しか書いていませんが、設定次第でもっと色々なことができるので、まだ余裕があれば複数のPHPサーバーを立ち上げたりPostgreSQLやMemcachedを使ったりなど、ここには書いていないようなことも試してみてください。

あなただけの最強の開発環境を作り上げましょう!