WordPress開発でハマるDocker・Git・Apacheの「5つの罠」
WordPressをローカルのDocker環境で開発していると、コードのバグとは別次元のところでつまずくことがよくあります。
root権限、Gitのpush失敗、テーマ関数の取り違え、Apacheの設定漏れ、コンテナ再作成によるデータ消失など、どれも「知っていれば5分で終わるのに、知らないと半日溶ける」タイプのトラブルです。
この記事では、WordPress開発でよく遭遇する5つのトラブルを、原因の切り分け方まで含めて整理します。
Docker Desktopとroot権限、認証情報が衝突する理由
WordPressの公式Dockerイメージ(wordpressイメージ)は、Apache+PHPの構成でコンテナ内のプロセスを動かしています。
ここで多くの人がつまずくのが、ホスト側とコンテナ内側でファイルの所有者(UID/GID)が食い違うという問題です。
具体的には、Docker Composeでボリュームを使ってWordPressを起動すると、wp-content/uploadsなどのディレクトリがコンテナ起動時にroot所有で作成されてしまうことがあります。
WordPress本体(Apache配下のPHP)は通常www-dataユーザーで動いているため、root所有のディレクトリに対してプラグインやメディアアップロード機能が書き込みできず、「アップロードに失敗しました」というエラーが出ます。
これはよく知られたパターンで、対処法としては次のようにコンテナ内で所有者を明示的に変更する方法が使われます。
docker compose exec -u root wordpress chown -R www-data:www-data wp-content/uploads
ポイントは、これはDocker自体のバグではなく、Linuxのファイルパーミッションの仕組みがそのままコンテナに持ち込まれているだけという点です。
ホストがWindowsやmacOSの場合はDocker Desktopが仮想化レイヤーを挟むため症状が見えにくくなりますが、Linux上でDocker Engineを直接使う場合は、UID/GIDのズレがより顕著に表面化しやすくなります。
もう一つ紛らわしいのが、Docker Desktopの認証情報(credential)まわりの衝突です。
Docker CLIはレジストリ認証の際に、~/.docker/config.json内のcredHelpers(認証情報ヘルパー)とauths(Base64エンコードされた認証情報)の両方を参照できますが、両方に同じレジストリの設定が存在する場合、credHelpersが優先されauths側の設定は無視されます。
プライベートなDockerレジストリやプラグイン配布用のコンテナレジストリを使っている場合、この優先順位を知らないと「設定したはずのパスワードが反映されない」という別種のハマりどころにつながります。
docker compose execにユーザーを指定する-uオプションは、コンテナ内で実行するプロセスのユーザーを一時的に切り替えるだけで、Dockerfile自体のUSER設定を変更するわけではありません。恒久的に権限を揃えたい場合は、Dockerfile側でユーザーとグループを作成しておくほうが再現性が高くなります。
なぜGit pushが失敗するのか?リポジトリ肥大化の切り分け方
WordPressのテーマやプラグインを開発していると、wp-content/uploadsのメディアファイルや、wp-content/cacheのキャッシュファイルをうっかりGitの管理対象に含めてしまうことがあります。
ローカルでは気づかず、ある日突然pushだけが失敗するようになるのが典型的なパターンです。
GitHubの公式ドキュメントを確認すると、リポジトリのサイズ制限は次のように定められています。
| 項目 | 基準値 |
|---|---|
| 1オブジェクトの推奨サイズ | 1MB以下 |
| 1ファイルの強制上限 | 100MB(超えるとpushがブロックされる) |
| ファイルサイズの警告ライン | 50MB(警告は出るがpush自体は可能) |
| 1回のpushサイズの上限 | 2GB |
| リポジトリ全体の推奨サイズ | 1GB未満、5GBを超えると運営から連絡が来る場合がある |
つまり、100MBを超えるファイルが1つでも履歴に含まれていると、そのファイルが原因でpushそのものがブロックされます。
ここで注意したいのは、該当ファイルを最新コミットで削除しても解決しないという点です。
Gitは差分ではなく履歴全体を保持する仕組みなので、過去のコミットに一度でも大きなファイルが含まれていれば、その履歴を書き換えない限りリポジトリは肥大化したままになります。
原因の切り分けは、まずリポジトリの実サイズを確認するところから始めます。
git count-objects -vH
size-packの値が想定より大きい場合は、過去のコミットに巨大なファイルが紛れ込んでいる可能性が高いです。どのファイルが原因かを特定するには、パックファイルの中身を大きい順に並べる方法が定番です。
git rev-list --objects --all | \
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
sed -n 's/^blob //p' | sort --numeric-sort --key=2 -r | head -n 20
原因が特定できたら、履歴から該当ファイルを取り除く作業に進みます。
かつてはgit filter-branchがよく使われていましたが、現在は公式にもgit filter-repoの利用が推奨されており、git filter-branchは非推奨の扱いになっています。
BFG Repo-Cleanerも選択肢としてよく紹介されますが、こちらも更新が止まっているという指摘があるため、これから履歴整理をするならgit filter-repoを優先するのが無難です。
git filter-repo --path wp-content/uploads --invert-paths
履歴の書き換えはリモートへの強制push(force push)を伴う破壊的な操作です。共同開発をしているリポジトリでは、他の開発者に事前共有し、必要であれば別ブランチでバックアップを取ってから実行してください。個人開発であっても、作業前にリポジトリ全体を別フォルダにコピーしておくと安心です。
根本的な再発防止としては、wp-content/uploadsやwp-content/cacheのようなユーザー生成データ・キャッシュ類を最初から.gitignoreに入れておくことに尽きます。
テーマやプラグインのソースコードだけをバージョン管理の対象にする方針が、結局いちばんの近道です。
templateとstylesheet、子テーマで差が出る理由
WordPressのテーマ開発では、CSSや画像の読み込みパスを取得するためにget_template_directory_uri()とget_stylesheet_directory_uri()という似た名前の関数が登場します。
この2つを混同したまま子テーマを作ると、「親テーマのファイルは表示されるのに、子テーマで上書きしたファイルが反映されない」という現象に遭遇しがちです。
両者の違いは次のとおりです。
-
get_template_directory_uri()は、親テーマ(テンプレート)のディレクトリURIを返します。子テーマを使っていても、常に親テーマのURLになります。 -
get_stylesheet_directory_uri()は、現在有効化されているテーマ(子テーマがあれば子テーマ)のディレクトリURIを返します。
たとえば子テーマのfunctions.phpで親テーマのスタイルシートを読み込みつつ、子テーマ独自のスタイルも読み込みたい場合、次のように書き分けるのが定石です。
function my_theme_enqueue_styles() {
// 親テーマのstyle.cssを読み込む
wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
// 子テーマ自身のstyle.cssを読み込む(親と依存関係を明示)
wp_enqueue_style( 'child-style', get_stylesheet_directory_uri() . '/style.css', array( 'parent-style' ) );
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_styles' );
子テーマを使わず親テーマだけで開発している場合は、この2つの関数はまったく同じ値を返すため違いに気づきにくいのですが、後から子テーマ運用に切り替えた途端に挙動が変わって混乱するケースが多いです。
「今後、子テーマを作る可能性があるかどうか」を基準に、最初からどちらを使うか意識しておくと事故を防ぎやすくなります。
なお、WordPress 4.7以降ではget_theme_file_uri()という関数も追加されており、こちらは子テーマにファイルがあればそれを優先し、なければ親テーマにフォールバックするという、より柔軟な挙動をします。
親子どちらのテーマにも対応させたい場合は、こちらの利用も選択肢に入ります。
AllowOverrideはなぜHTTPとHTTPSで別々に書く必要があるのか?
WordPressのパーマリンク設定(投稿ページを/blog/sample-post/のような形式で表示する機能)を有効にすると、.htaccessにリライトルールが書き込まれます。
ところが、HTTPでは正常に動くのに、HTTPSでアクセスした途端に404エラーになるという不具合がよく起こります。
これは、ApacheがHTTP(ポート80)用の設定とHTTPS(ポート443)用の設定を別々のVirtualHostブロックとして扱っていることに原因があります。
多くの環境では、HTTP用の設定はhttpd.confや000-default.confにあり、HTTPS用の設定はssl.confやdefault-ssl.confのように別ファイルに分かれています。
.htaccessに書かれたディレクティブが実際に有効になるかどうかは、その場所に対応する<Directory>ブロックのAllowOverride設定に完全に依存します。
つまり、HTTP用のVirtualHostにAllowOverride Allを書いていても、HTTPS用のVirtualHostに同じ設定がなければ、HTTPS側では.htaccessの内容が一切反映されません。
イメージとしては、次のように両方のVirtualHostブロックに同じDirectory設定を書く必要があります。
<VirtualHost *:80>
DocumentRoot /var/www/html
<Directory /var/www/html>
AllowOverride All
</Directory>
</VirtualHost>
<VirtualHost *:443>
DocumentRoot /var/www/html
<Directory /var/www/html>
AllowOverride All
</Directory>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/example.crt
SSLCertificateKeyFile /etc/ssl/private/example.key
</VirtualHost>
Let's EncryptなどでSSL証明書を自動発行するツール(Certbotなど)を使っている場合、SSL用のVirtualHostブロックが自動生成されることがあります。その際に元のHTTP設定に書いていたDirectoryディレクティブがコピーされず、AllowOverrideの記述が漏れるケースが実務でもよく報告されています。SSL化した直後にパーマリンクが崩れたら、まずここを疑ってみましょう。
切り分け方としては、まず.htaccessが読み込まれているかどうかを、意図的に無効な記述を書いて確認する方法が手っ取り早いです。
HTTPでは500エラーになるのにHTTPSでは何も起きない場合、HTTPS側のAllowOverrideが反映されていない可能性が高いと判断できます。
設定を変更したあとは、必ずapachectl configtestのような構文チェックを行ってからApacheを再起動する習慣をつけておくと安全です。
コンテナを再作成したらデータが消えた!?ボリュームマウントとの違い
Docker上でWordPress環境を作っていると、docker compose downを実行した後に「せっかく作った記事や設定が全部消えた」というトラブルに遭遇することがあります。
これは多くの場合、データの永続化先を正しく設定していなかったことが原因です。
docker compose downは基本的に以下のものだけを削除します。
- Composeファイルで定義したサービスのコンテナ
- Composeファイルの
networksセクションで定義したネットワーク - 使用していたデフォルトのネットワーク
一方で、名前を持たない匿名ボリューム(anonymous volume)は、明示的に-vまたは--volumesオプションを付けない限りデフォルトでは削除されません。
ただし匿名ボリュームは名前が固定されていないため、再度upしたときに元のボリュームが自動的に再アタッチされることはなく、結果として「新しい空のボリュームでコンテナが起動し、データが消えたように見える」という現象が起こります。
ここで整理しておきたいのが、Dockerにおけるデータ永続化の3つの方式の違いです。
| 方式 | データの保存場所 | 主な用途 | コンテナ削除時の挙動 |
|---|---|---|---|
| 名前付きボリューム | Dockerが管理する領域(/var/lib/docker/volumes配下) |
DBデータ、メディアファイルなど | 名前を指定していれば残る |
| バインドマウント | ホストOS上の任意のディレクトリ | ソースコード、設定ファイルなど | ホスト側にそのまま残る |
| 匿名ボリューム | Dockerが管理するが名前がない | 一時的なデータ |
down -vで消える、指定しなくても再アタッチされない |
WordPress用のCompose定義でよく見られる、データベースを永続化する典型的な書き方は次のようになります。
services:
db:
image: mysql:8.0
volumes:
- db_data:/var/lib/mysql # 名前付きボリュームでDBデータを永続化
wordpress:
image: wordpress:latest
volumes:
- ./wp-content:/var/www/html/wp-content # バインドマウントでテーマ・プラグインを同期
depends_on:
- db
volumes:
db_data:
このように名前付きボリューム(db_data)を明示しておけば、docker compose downだけではデータは消えません。
消えるのは、docker compose down --volumesのように意図的にボリュームごと削除した場合や、そもそもボリュームを指定せずコンテナ内のファイルシステムだけにデータを置いていた場合です。
「データを消したくないなら、DBのデータディレクトリだけは必ず名前付きボリュームかバインドマウントに逃がしておく」というのが、コンテナ運用における最低限の鉄則です。
逆に言えば、検証用に毎回まっさらな状態から始めたい場合は、あえて匿名ボリュームのままにしておくという使い方もできます。
目的に応じてどちらの挙動が欲しいのかを意識して設計することが、事故を防ぐいちばんの近道です。
まとめ
今回紹介した5つのトラブルは、それぞれ独立した問題に見えますが、根っこにある考え方は共通しています。
「ホストとコンテナ」「HTTPとHTTPS」「親テーマと子テーマ」「Gitの現在の状態と履歴」「ボリュームの種類ごとの挙動」というように、一見同じに見えるものが実は別の設定・別のレイヤーで管理されているという点です。
エラーに遭遇したときは、まずどの階層で何が起きているのかを切り分けることが、遠回りに見えて実はいちばんの近道になります。
この記事が、同じ沼で何時間も溶かしてしまう人を一人でも減らす助けになれば幸いです。