Raspberry Pi の WordPress を Hugo に切り替える

Hugo Wordpress RaspberryPi 自宅サーバ 覚書

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/v1akismet/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-updaterpi-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 では、webcertbot が参照する 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

wordpressdb は、最初から消さない。切り替え後に問題がなければ停止する。

下の設定例は、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:

通常は webcertbot だけを起動対象にする。

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

webcertbot は新しい bind mount に付け替えるため再作成した。

cd ~/nosubject-docker
docker compose up -d --force-recreate web certbot

--remove-orphans は使わなかった。wordpressdb は 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-jsonLink ヘッダーは出なくなるはず。

今回、実際に確認したときは、対象記事も 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 に切り替える流れは、次のようになる。

  1. 現在の WordPress / Docker / nginx 構成を確認する
  2. Hugo 側の記事と画像を移行して、ローカルで表示確認する
  3. Hugo の public を Raspberry Pi に配置する
  4. nginx の向き先を WordPress コンテナから Hugo の静的ファイルに変える
  5. 表示とURLを確認する
  6. 問題なければ WordPress / MySQL の Docker コンテナを停止する

いきなり WordPress 環境を削除せず、nginx の設定と Docker 環境を戻せる形で残しておけば、切り替え作業は落ち着いて進められそうだ。

今回の切り替えでは、WordPress と DB は削除せず停止にとどめた。

docker compose stop wordpress db

しばらく運用して問題がなければ、WordPress の html ディレクトリと DB volume をバックアップしてから整理する。