WordPress から Hugo への記事データ移行は、WordPress から Hugo へ移行する でまとめた。
この記事では、その次の作業として、Raspberry Pi 上で動いている実際の WordPress 環境を Hugo に切り替える手順を整理する。
自分で作った環境なのに、時間が経つと何をどう動かしていたのか忘れてしまう。
過去に書いた記事を見返すと、このサイトの WordPress は Raspberry Pi 上で直接 PHP と MySQL を入れて動かしていたわけではなく、Docker を使って動かしていた。
さらに現在の状態を確認すると、サーバーは Raspberry Pi 5 に変わっていた。
移行前の構成
大まかな構成は次のようなものだった。
Internet
|
v
Raspberry Pi 5 / Debian
|
+-- Docker Compose
|
+-- wordpress-web-1 : nginx。80 / 443 を受ける
+-- wordpress-wordpress-1 : WordPress
+-- wordpress-db-1 : MySQL
+-- wordpress-certbot-1 : Certbot
過去記事に残っていた古い環境メモは次の通り。
Raspberry Pi 4
Ubuntu 20.10 arm64
nginx 1.18.0
Docker 20.10.2
docker-compose 1.25.0
WordPress official Docker image
mysql/mysql-server:latest
ただし、現在の実機で確認した状態は次の通りだった。
Hostname: rpi5
Raspberry Pi 5 Model B Rev 1.0
Revision: d04170
Debian GNU/Linux 12 (bookworm)
Linux 6.12.93+rpt-rpi-2712 aarch64
Memory: 7.9GiB
Disk: NVMe 953.9GB
Docker 29.6.0
Docker Compose v5.1.4
ディスクは SD カードやUSB SSDではなく、NVMe で起動している。
/dev/nvme0n1p2 938G 39G 852G 5% /
/dev/nvme0n1p1 511M 79M 433M 16% /boot/firmware
Pi 5 へ移行したとき、M.2 NVMe SSD ブートでも少し苦労した。
作業メモは残していたものの、記事にまとめるのが面倒でそのままになっていた。今回あらためて確認すると、現在のルートファイルシステムは /dev/nvme0n1p2 にあり、無事 NVMe SSD から起動していることがわかった。
今回の Hugo 移行とは直接関係ないが、WordPress を動かしている土台は Raspberry Pi 5 + NVMe SSD になっている。メモが見つかったら、M.2 SSD ブートの話は別の記事としてまとめたい。
ホスト側にも nginx は入っているが、systemd のサービスとしては動いていなかった。
nginx version: nginx/1.22.1
systemctl is-active nginx: inactive
Raspberry Pi のファームウェア情報も取れた。
BOOTLOADER: update available
CURRENT: Thu 8 May 14:13:17 UTC 2025
LATEST: Tue 26 May 15:01:25 UTC 2026
BOOT_ORDER=0xf461
外部公開している 80 / 443 は Docker の wordpress-web-1 コンテナが受けている。
wordpress-web-1 nginx Up 12 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
動いている WordPress 関連のコンテナは次のあたり。
wordpress-wordpress-1 wordpress
wordpress-web-1 nginx
wordpress-db-1 mysql:8.0
wordpress-certbot-1 certbot/certbot:latest
Compose ファイルは次の場所にある。
<home>/wordpress/docker-compose.yml
WordPress のファイルは次の場所にある。
<home>/wordpress/html
<home>/wordpress/html/wp-content
つまり、Hugo へ移行するときに置き換える対象は、Raspberry Pi 全体ではなく、主に wordpress-web-1 -> wordpress-wordpress-1 の部分になる。
Hugo で生成した静的ファイルを nginx からそのまま配信できるようにすれば、WordPress コンテナ、MySQL コンテナ、PHP の実行環境は不要になる。
公開中の状態を確認する
切り替え前に、公開中のサイトが本当に WordPress として動いているかを外側から確認した。
https://nosubject.io/ の HTTP ヘッダーを見ると、次の情報が返ってきた。
Server: nginx/1.27.1
X-Powered-By: PHP/8.2.23
Link: <https://nosubject.io/wp-json/>; rel="https://api.w.org/"
RSS フィードには次の generator が含まれていた。
<generator>https://wordpress.org/?v=7.0</generator>
HTML を見ると、テーマは Cocoon 系だった。
wp-content/themes/cocoon-master
wp-content/themes/cocoon-child-master
公開されている REST API からも、WordPress として動いていることがわかる。
https://nosubject.io/wp-json/
wp-json では cocoon/v1、akismet/v1 などの namespace も見えていた。
この時点で、移行前の実体はだいたい次のように整理できる。
- Raspberry Pi 5 上の Debian 12 で運用している
- 外部公開は Docker コンテナの nginx
- WordPress は Docker コンテナで動いている
- DB も Docker 側の MySQL コンテナ
- HTTPS は Let’s Encrypt / Certbot
- 現在の公開面では nginx、PHP、WordPress、Cocoon が見えている
Raspberry Pi 側で確認する
公開されている情報だけでは、実際のOSバージョン、Docker Compose の構成ファイルの場所、ボリュームの状態まではわからない。
実環境を Hugo に置き換える前に、Raspberry Pi に SSH して、現在の状態をメモしておく。
このリポジトリには確認用のスクリプトも置いておいた。
OS、Docker、nginx に加えて、rpi-eeprom-update、rpi-eeprom-config などが使える環境なら Raspberry Pi のファームウェア情報も確認する。
chmod +x scripts/check-raspberry-pi-wordpress-hugo-env.sh
./scripts/check-raspberry-pi-wordpress-hugo-env.sh
サイトURLや Hugo の配置先を変えたい場合は、環境変数で指定できる。
SITE_URL=https://nosubject.io/ HUGO_PUBLIC_DIR=$HOME/wordpress/hugo-public ./scripts/check-raspberry-pi-wordpress-hugo-env.sh
スクリプトの出力では、ユーザー名とホームディレクトリはデフォルトで <user>、<home> に置き換えるようにしている。
マスクせずに確認したい場合は、MASK_PRIVATE=0 を付ける。
MASK_PRIVATE=0 ./scripts/check-raspberry-pi-wordpress-hugo-env.sh
手で確認するなら、以下のようなコマンドを実行する。
lsb_release -a
uname -a
nginx -v
php -v
docker --version
docker compose version || docker-compose --version
動いているコンテナも確認する。
docker ps
コンテナ名とイメージだけ見たい場合は、次のようにする。
docker inspect --format '{{.Name}} {{.Config.Image}}' $(docker ps -q)
nginx の設定も、置き換え前に必ず確認しておく。
ホスト側 nginx を使っている場合は次のように確認できる。
sudo nginx -T | grep -E 'server_name|root|proxy_pass|ssl_certificate|client_max_body_size'
今回の実機では公開用 nginx が Docker コンテナなので、Compose ファイルと nginx 設定ファイルのマウント先も確認する。
cd ~/wordpress
docker compose config
docker inspect wordpress-web-1 --format '{{json .Mounts}}'
ディスク構成と空き容量も見ておく。
df -h
lsblk -e 7
Hugo 化したあとに戻したくなる可能性もあるので、Docker Compose のディレクトリ、docker-compose.yml、WordPress の html ディレクトリ、DB ボリュームのバックアップ方針は先に決めておいたほうがいい。
今回の実機では、Hugo の配置予定ディレクトリはまだ存在していなかった。
Hugo public directory does not exist: <home>/wordpress/hugo-public
Hugo の公開ファイルを置く
ローカルで Hugo の表示確認ができたら、Raspberry Pi に Hugo の生成結果を配置する。
既存の ./html は WordPress 本体なので、Hugo の出力先として上書きしない。
Compose ディレクトリの中に、Hugo 用の公開ディレクトリを別に作る。
cd ~/wordpress
mkdir -p hugo-public
ファイルの配置は rsync で行う。
rsync -av --delete public/ ~/wordpress/hugo-public/
--delete を使うと、ローカルで削除したファイルが本番側からも消える。便利だけど強いオプションなので、最初は --dry-run を付けて確認したほうがいい。
rsync -av --delete --dry-run public/ ~/wordpress/hugo-public/
nginx を Hugo 配信へ切り替える
古い構成では、nginx が WordPress コンテナの localhost:8080 へリバースプロキシしていた。
proxy_pass http://localhost:8080/;
現在の実機では wordpress-web-1 という nginx コンテナが 80 / 443 を受けて、Docker Compose 内の WordPress コンテナへ流している。
Hugo へ移行したあとは、この nginx コンテナが Hugo の生成した静的ファイルを直接返すようにする。
今回の実機では公開用 nginx はホスト側ではなく Docker コンテナなので、実際には <home>/wordpress/docker-compose.yml と、その nginx 設定を確認してから変更する。
docker-compose.yml では、web と certbot が参照する webroot を WordPress の ./html から Hugo 用の ./hugo-public に切り替える。
services:
web:
image: nginx
restart: always
volumes:
- ./templates:/etc/nginx/templates
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./sites-enabled:/etc/nginx/sites-enabled
- ./letsencrypt:/etc/letsencrypt
- ./hugo-public:/var/www/html:ro
ports:
- "80:80"
- "443:443"
certbot:
image: certbot/certbot:latest
restart: always
volumes:
- ./letsencrypt:/etc/letsencrypt
- ./hugo-public:/var/www/html
wordpress と db は、最初から消さない。切り替え後に問題がなければ停止する。
下の設定例は、nginx から Hugo の public を配信する場合のイメージ。
server {
server_name nosubject.io;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/nosubject.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nosubject.io/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
ただし、いきなり WordPress の設定を消すのは少し怖い。
まず Compose ディレクトリごとバックアップしてから、Hugo 用の設定に切り替える。
cd ~
cp -a wordpress wordpress.wordpress-backup
nginx 設定を書き換えたら、コンテナ内の nginx で設定を確認してから reload する。
cd ~/wordpress
docker compose exec web nginx -t
docker compose exec web nginx -s reload
サービス名が web ではない場合は、docker compose ps で nginx コンテナのサービス名を確認してから実行する。
実際に変更した docker-compose.yml
最終的に、Compose 側は次のような形にした。
ポイントは次の通り。
- nginx の公開ディレクトリを
./hugo-publicにする - certbot も同じ
./hugo-publicを webroot として使う - WordPress と DB は削除せず、
profilesに入れて通常起動から外す restart: "no"にして、意図せず復活しないようにする- Compose のプロジェクト名は
name: wordpressで固定しておく
name: wordpress
services:
web:
image: nginx
restart: always
volumes:
- ./templates:/etc/nginx/templates
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./sites-enabled:/etc/nginx/sites-enabled
- ./letsencrypt:/etc/letsencrypt
- ./hugo-public:/var/www/html:ro
ports:
- "80:80"
- "443:443"
environment:
- NGINX_HOST=nosubject.io
- NGINX_PORT=80
certbot:
image: certbot/certbot:latest
restart: always
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/html; sleep 12h & wait $${!}; done;'"
volumes:
- ./letsencrypt:/etc/letsencrypt
- ./hugo-public:/var/www/html
wordpress:
image: wordpress
profiles:
- wordpress
restart: "no"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_passw0rd
WORDPRESS_DB_NAME: wp_db
volumes:
- ./html:/var/www/html
- ./php.ini:/usr/local/etc/php/php.ini
db:
image: mysql:8.0
profiles:
- wordpress
restart: "no"
environment:
MYSQL_DATABASE: wp_db
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_passw0rd
MYSQL_RANDOM_ROOT_PASSWORD: "1"
volumes:
- db:/var/lib/mysql
volumes:
db:
通常は web と certbot だけを起動対象にする。
docker compose up -d web certbot
WordPress と DB を一時的に戻したい場合は、profile を指定して起動する。
docker compose --profile wordpress up -d wordpress db
certbot の再起動ループを直す
切り替え作業中に certbot コンテナが再起動ループになっていることに気づいた。
docker compose ps
docker compose logs --tail 80 certbot
ログには次のような内容が出ていた。
Certbot doesn't know how to automatically configure the web server on this system.
Please run "certbot certonly" to do so.
原因は、certbot/certbot イメージを引数なしで起動していたこと。
Docker Compose で常駐させるなら、certbot renew を定期的に実行する entrypoint を明示しておく。
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/html; sleep 12h & wait $${!}; done;'"
修正後にログを見ると、証明書はまだ更新期限ではないことが確認できた。
Certificate not yet due for renewal
/etc/letsencrypt/live/nosubject.io/fullchain.pem expires on 2026-08-09
403 Forbidden になった原因
nginx を Hugo 配信へ切り替えた直後、https://nosubject.io/ が 403 Forbidden になった。
nginx のログを確認する。
docker compose logs --tail 80 web
ログには次のように出ていた。
directory index of "/var/www/html/" is forbidden
原因は単純で、./hugo-public が空だった。
Hugo の index.html が存在しない状態で / にアクセスすると、nginx は /var/www/html/ のディレクトリを見に行く。ディレクトリ一覧は無効なので 403 になる。
Hugo を本番用ディレクトリへビルドすれば解消する。
cd ~/nosubject-hugo
hugo --destination <home>/nosubject-docker/hugo-public --cleanDestinationDir
このときのビルド結果は次のようになった。
Pages JA 185 / EN 178
Paginator pages JA 21 / EN 21
Static files JA 195 / EN 195
その後、記事を公開したあとに再ビルドしたときは次の状態になった。
Pages JA 194 / EN 187
Paginator pages JA 21 / EN 21
Static files JA 195 / EN 195
Hugo をアップデートした
Raspberry Pi 側に入っていた Hugo は最初 v0.140.0 だった。
hugo version
hugo v0.140.0+extended linux/arm64
今回は古いバージョンに合わせてテンプレートを直すのではなく、Hugo を更新することにした。
Raspberry Pi 5 は linux/arm64 なので、GitHub Releases から extended 版を取得して差し替えた。
curl -L -o /tmp/hugo_extended_0.164.0_linux-arm64.tar.gz \
https://github.com/gohugoio/hugo/releases/download/v0.164.0/hugo_extended_0.164.0_linux-arm64.tar.gz
mkdir -p /tmp/hugo-0.164.0
tar -xzf /tmp/hugo_extended_0.164.0_linux-arm64.tar.gz -C /tmp/hugo-0.164.0
/tmp/hugo-0.164.0/hugo version
sudo cp /usr/local/bin/hugo /tmp/hugo-v0.140.0.backup
sudo install -m 0755 /tmp/hugo-0.164.0/hugo /usr/local/bin/hugo
hugo version
更新後は次のバージョンになった。
hugo v0.164.0+extended linux/arm64
管理ディレクトリ名を変更する
ここまでの作業では、Compose 管理ディレクトリが ~/wordpress のままになっていた。
Hugo へ切り替えた後は、実態としては WordPress 専用ディレクトリではないので、たとえば ~/nosubject-docker のような名前のほうが分かりやすい。
ただし、ディレクトリ名を変えると Docker Compose のプロジェクト名も変わり、コンテナ名や volume 名が変わることがある。
既存の wordpress-web-1 などをそのまま管理したい場合は、先に docker-compose.yml へプロジェクト名を明示しておく。
name: wordpress
今回はプロジェクト名を固定したうえで、実際にディレクトリを変更した。
cd ~
mv wordpress nosubject-docker
cd ~/nosubject-docker
docker compose config
docker compose up -d web certbot
Hugo の出力先も新しいディレクトリに合わせる。
cd ~/nosubject-hugo
hugo --destination <home>/nosubject-docker/hugo-public --cleanDestinationDir
web と certbot は新しい bind mount に付け替えるため再作成した。
cd ~/nosubject-docker
docker compose up -d --force-recreate web certbot
--remove-orphans は使わなかった。wordpress と db は profile 付きの停止コンテナとして残しておきたかったため。
再作成後に docker inspect で確認すると、nginx と certbot は新しいディレクトリを参照していた。
<home>/nosubject-docker/hugo-public -> /var/www/html
<home>/nosubject-docker/letsencrypt -> /etc/letsencrypt
Compose プロジェクト内のコンテナも、余計に増えていないことを確認した。
wordpress-web-1 Up
wordpress-certbot-1 Up
wordpress-wordpress-1 Exited
wordpress-db-1 Exited
切り替え後に確認する
切り替え直後は、次の点を確認する。
- トップページが表示されるか
- 個別記事のURLが変わっていないか
/wp-content/uploads/...の画像が表示されるか- HTTP から HTTPS へのリダイレクトが動くか
404ページが意図した表示になるか- RSS が生成されているか
ブラウザだけでなく、curl でも HTTP ヘッダーを見ておく。
curl -I https://nosubject.io/
Hugo に切り替わっていれば、X-Powered-By: PHP/... や wp-json の Link ヘッダーは出なくなるはず。
今回、実際に確認したときは、対象記事も nginx 経由で 200 OK が返っていることを確認した。
curl -k -I https://localhost/wordpress-to-hugo-migration/ -H 'Host: nosubject.io'
curl -k -I https://localhost/en/wordpress-to-hugo-migration/ -H 'Host: nosubject.io'
HTTP/1.1 200 OK
WordPress コンテナを止める
Hugo の表示に問題がなければ、WordPress と MySQL のコンテナは停止できる。
ただし、しばらくは docker-compose.yml、WordPress の html ディレクトリ、DB ボリュームは残しておく。
docker compose stop
古い docker-compose を使っている環境なら、次の形になる。
docker-compose stop
しばらく運用して問題がなければ、不要になった Docker 環境を整理する。
まとめ
Raspberry Pi 上の WordPress を Hugo に切り替える流れは、次のようになる。
- 現在の WordPress / Docker / nginx 構成を確認する
- Hugo 側の記事と画像を移行して、ローカルで表示確認する
- Hugo の
publicを Raspberry Pi に配置する - nginx の向き先を WordPress コンテナから Hugo の静的ファイルに変える
- 表示とURLを確認する
- 問題なければ WordPress / MySQL の Docker コンテナを停止する
いきなり WordPress 環境を削除せず、nginx の設定と Docker 環境を戻せる形で残しておけば、切り替え作業は落ち着いて進められそうだ。
今回の切り替えでは、WordPress と DB は削除せず停止にとどめた。
docker compose stop wordpress db
しばらく運用して問題がなければ、WordPress の html ディレクトリと DB volume をバックアップしてから整理する。