SNS

本番は3つの WordPress(別DB・入れ子のディレクトリ構成)。ローカルは Docker Compose 1つで動かす

同じサーバー上で、3つの WordPress が別々の DB で動いているサイトがあります。ローカルの開発環境を、Docker Compose 1つでまとめて動かせるようにしました。

本番は手動 FTP で運用している昔ながらの構成で、ディレクトリの入れ子や、WordPress の外にある PHP が本番のパスを前提に書かれている、といった癖があります。本番のコードを書き換えずにローカルで動かすために工夫したところを紹介します。

構成

サイト 本番のパス ローカル DB
メイン /(WP 本体は /cms) :8080 main
サブ A /sub-a(WP 本体は /sub-a/cms) :8082 sub_a
サブ B /sub-b(WP 本体は /sub-b/cms) :8083 sub_b

サービスは次のとおりです。

  • db:MariaDB 1つ(3サイト共通。DB はサイトごとに分ける)
  • wordpress・sub_a・sub_b:同じイメージから起動
  • phpmyadmin、mailpit(送信メールの確認用)

リポジトリで管理するのは、各サイトのテーマと WordPress の外にある独自の PHP だけです。WordPress のコアはリポジトリに置かず、イメージから入れます。

1. イメージは1つだけビルドして、3サービスで使い回す

3つの WordPress サービスで同じ設定を書かないよう、YAML のアンカーにまとめました。

x-wordpress: &wordpress
  image: myproject-wordpress:local
  pull_policy: never
  depends_on:
    db:
      condition: service_healthy

services:
  wordpress:
    <<: *wordpress
    build: ./docker        # ビルドはこのサービスだけに持たせる
    ...
  sub_a:
    <<: *wordpress
    ...
  sub_b:
    <<: *wordpress
    ...

build: を3サービスすべてに書くと、同じイメージを同時にビルドしようとして衝突することがあります。ビルドは1サービスだけに持たせ、残りは同じイメージ名を参照させています。

WordPress のバージョンを固定する

公式の wordpress イメージは、PHP のバージョンを示すタグ(php8.3-apache)を使うと、WordPress のバージョンが勝手に上がります。本番に上げる前の検証に使いたいので、コアは別に指定した版で差し替えています。

FROM wordpress:php8.3-apache

ENV WP_VERSION=7.1.3 \
    WP_SHA1=<公式の sha1>
RUN set -eux; \
    curl -fsSL -o /tmp/wordpress.tar.gz "https://wordpress.org/wordpress-${WP_VERSION}.tar.gz"; \
    echo "${WP_SHA1} */tmp/wordpress.tar.gz" | sha1sum -c -; \
    cd /usr/src/wordpress; \
    find . -mindepth 1 -maxdepth 1 ! -name wp-content ! -name wp-config-docker.php ! -name .htaccess -exec rm -rf {} +; \
    tar -xzf /tmp/wordpress.tar.gz -C /usr/src/wordpress --strip-components=1 --exclude=wordpress/wp-content; \
    rm /tmp/wordpress.tar.gz; \
    chown -R www-data:www-data /usr/src/wordpress

公式イメージは、初回起動時に /usr/src/wordpress の中身をドキュメントルートへコピーします。その元を差し替えるのがポイントです。あわせて、見積もりシステムが使う pdo_mysql、メール用の msmtp、運用用の wp-cli もこのイメージに入れています。

2. コアは名前付きボリューム、開発するものだけバインドマウント

  wordpress:
    volumes:
      - wp_core:/var/www/html                        # コア一式(イメージが初回に展開)
      - ./docker/wp-config.php:/var/www/html/wp-config.php:ro
      - ./docker/.htaccess:/var/www/html/.htaccess:ro
      - ./cms/wp-content/themes/main:/var/www/html/wp-content/themes/main
      - ./cms/wp-content/plugins:/var/www/html/wp-content/plugins
      - ./cms/wp-content/uploads:/var/www/html/wp-content/uploads
  • コアは名前付きボリュームに置き、サイトごとに分けます(wp_core・wp_core_sub_a・wp_core_sub_b)。リポジトリにコアのファイルを置く必要がありません。
  • テーマ、プラグイン、メディアだけをリポジトリのディレクトリからマウントするので、テーマの編集はそのままブラウザに反映されます。プラグインとメディアはディスクに置きますが、Git では追跡しません。

3. wp-config.php はローカル専用を1つだけ用意して使い回す

本番の wp-config.php はリポジトリに置かず、ローカル専用のものを1つ用意して3サイトにマウントします。サイトごとの違いは、すべて環境変数で渡します。

define( 'DB_NAME',     getenv( 'WORDPRESS_DB_NAME' ) ?: 'wordpress' );
define( 'DB_USER',     getenv( 'WORDPRESS_DB_USER' ) ?: 'wordpress' );
define( 'DB_PASSWORD', getenv( 'WORDPRESS_DB_PASSWORD' ) ?: 'wordpress' );
define( 'DB_HOST',     getenv( 'WORDPRESS_DB_HOST' ) ?: 'db' );

// 本番 DB のダンプに入っている URL を書き換えずに済むよう、URL は環境変数で上書きする
$base = getenv( 'WP_BASE_URL' ) ?: 'http://localhost:8080';
define( 'WP_HOME',    $base );
define( 'WP_SITEURL', $base );

$debug = filter_var( getenv( 'WP_DEBUG' ), FILTER_VALIDATE_BOOLEAN );
define( 'WP_DEBUG',         $debug );
define( 'WP_DEBUG_LOG',     $debug );
define( 'WP_DEBUG_DISPLAY', false );
x-wordpress-env: &wordpress-env       # 3サイト共通の環境変数
  WORDPRESS_DB_HOST: db
  WORDPRESS_DB_USER: ${DB_USER:-wordpress}
  WORDPRESS_DB_PASSWORD: ${DB_PASSWORD:-wordpress}
  WP_DEBUG: ${WP_DEBUG:-true}

services:
  sub_a:
    <<: *wordpress
    environment:
      <<: *wordpress-env
      WORDPRESS_DB_NAME: sub_a
      WP_BASE_URL: ${SUB_A_BASE_URL:-http://localhost:8082}

WP_HOME・WP_SITEURL を定数で上書きしておくと、本番の DB ダンプをそのまま取り込んでも、管理画面やリンクがローカルの URL で動きます。ダンプの中の URL を置換する必要がありません。

4. DB は初回起動時にサイトごとに作って取り込む

MariaDB の公式イメージは、初回起動時(データのボリュームが空のとき)に /docker-entrypoint-initdb.d/ のファイルを順に実行します。メインの DB は MYSQL_DATABASE とダンプのファイルで作り、サブサイトの DB はシェルスクリプトで作ります。

#!/bin/bash
# docker/db-init/20-sites.sh
set -e
SITES=(
  "sub_a:/seed/sub_a/sub_a.sql.gz"
  "sub_b:/seed/sub_b/sub_b.sql.gz"
)
for site in "${SITES[@]}"; do
  db="${site%%:*}"; dump="${site#*:}"
  mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" <<SQL
CREATE DATABASE IF NOT EXISTS \`${db}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON \`${db}\`.* TO '${MYSQL_USER}'@'%';
SQL
  if [ -f "${dump}" ]; then
    gunzip -c "${dump}" | mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${db}"
  else
    echo "[db-init] 警告: ${dump} が見つかりません(${db} は空のまま)"
  fi
done

ダンプが無くても起動は止まらず、空の DB ができます。ダンプには個人情報が含まれるので、Git では追跡せず、各自のディスクに置く運用にしています。

5. 本番のディレクトリ構成を前提にしたコードを、書き換えずに動かす

サブ A は、WordPress の外にも独自の PHP(共通のヘッダー、フォームなど)があり、本番の構成を前提にファイルを読み込んでいました。

本番:  /sub-a/cms/       ← WP 本体
       /sub-a/inc/       ← 共通の PHP
// テーマの functions.php
require_once ABSPATH . '../inc/common.php';          // /sub-a/cms/ の1つ上の inc/

// inc/parts-header.php(WP の外から WP を読み込む)
require_once dirname(__DIR__) . '/cms/wp-load.php';  // /sub-a/cms/wp-load.php

ローカルの公式イメージは、WordPress 本体がドキュメントルートの直下(/var/www/html/)にあり、cms/ がありません。本番のコードは変えたくないので、ローカル側で次の2つを用意しました。

ABSPATH . '../inc/' 用のシンボリックリンク

inc/ はドキュメントルート直下(/var/www/html/inc)にマウントしています。WordPress から見た ABSPATH . '../inc/' は /var/www/inc なので、そこにリンクを張ります。

RUN ln -s /var/www/html/inc /var/www/inc

realpath で解決されるので、inc/ の中で __FILE__ から計算しているサイトのルートも / になり、ローカルの URL と合います。ほかのサイトでは使わないので、ほかに影響はありません。

cms/wp-load.php の中継ファイル

<?php
// docker/sub-a-wp-load.php を /var/www/html/cms/wp-load.php にマウントする
require_once dirname( __DIR__ ) . '/wp-load.php';
      - ./docker/sub-a-wp-load.php:/var/www/html/cms/wp-load.php:ro

6. メールは msmtp で Mailpit に集める(-f との衝突に注意)

ローカルからは外部にメールを送りたくないので、PHP の sendmail_path を msmtp にして、Mailpit のコンテナへ送ります。

# docker/msmtprc
defaults
tls off
auth off

account mailpit
host mailpit
port 1025
from wordpress@example.local

account default : mailpit

最初は sendmail_path に --read-envelope-from(ヘッダーの From を封筒の送信元に使う)を付けていました。ところが、サブ A のフォームが mb_send_mail(..., '-f 送信元') と -f で送信元を指定していて、msmtp が次のエラーで送信を拒否していました。

msmtp: cannot use both --from and --read-envelope-from

--read-envelope-from を外して、msmtp -t だけにすると解決しました。-f があればそれを、無ければ msmtprc の from を使ってくれます。

COPY msmtprc /etc/msmtprc
RUN printf 'sendmail_path = "/usr/bin/msmtp -t"\n' \
    > /usr/local/etc/php/conf.d/zz-mail.ini

ちなみに、ローカルで wp_mail() が送信元を指定せずに false を返す場合は、msmtp ではなく WordPress 側の問題です。既定の送信元が wordpress@localhost になり、PHPMailer がアドレスとして不正と判断します。送信元を明示すれば届きます。

使い方

cp .env.example .env
docker compose up -d --build
URL
メイン http://localhost:8080/
サブ A / サブ B http://localhost:8082/ ・ http://localhost:8083/
phpMyAdmin http://localhost:8081/
Mailpit http://localhost:8025/

DB ごと作り直すときは docker compose down -v && docker compose up -d --build です。

まとめ

  • イメージは1つだけビルドして、3サービスで使い回す。WordPress のバージョンはイメージの中で固定する。
  • コアは名前付きボリューム、開発するものだけバインドマウントする。
  • wp-config.php はローカル専用を1つにして、違いは環境変数で渡す。WP_HOME・WP_SITEURL を上書きすれば、本番のダンプをそのまま使える。
  • 本番のパスを前提にしたコードは、シンボリックリンクと中継ファイルで吸収し、本番のコードは書き換えない。

CONTACT

サイト訪問ありがとうございます。
お仕事のご依頼、ご相談がありましたらお気軽にご連絡ください。
*は必須項目となります。

お名前 *
メールアドレス *
会社・組織名
お問い合わせ内容 *
0/300

当サイトはreCAPTCHAによって保護されており、Google のプライバシーポリシーおよび利用規約が適用されます。
当サイトのプライバシーポリシーをご一読の上ご連絡ください。