勉強したことのメモ

Webエンジニア / プログラマが勉強したことのメモ

AlmaLinux9 / Podmanコンテナ環境にWordPressを移行する方法

AlmaLinux9系にホスト側NginxとPodmanコンテナを構築した環境に、AlamLinux8系のLAMP環境で運用中のWordPressを移行したい。WordPressのチューニングやLet’s EncryptのDNS認証によるSSL証明書取得も行いたい。以下に対応方法をメモ。

 

やりたいこと

  • 運用中のWordPressの移行
  • WordPressの高速化、負荷対策等のチューニング
  • ホスト側Nginxのリバースプロキシ設定
  • コンテナ側Apacheのバーチャルホスト設定
  • Let’s EncryptのDNS認証によるSSL証明書取得

 

環境について

VPS

今回はKAGOYA CLOUD VPSのCPU1コア / メモリ1GBプランで作業した。SSH接続は公式案内の通り以下でログインできる。

  • 接続ポート -> 22
  • ユーザ名 -> root
  • パスワード or 秘密鍵 -> インスタンス作成時に選択したもの

ホスト(VPS)側

  • AlmaLinux release 9.8 (Olive Jaguar)

 

対応方法

事前準備

WordPress関連ファイル

WordPress関連の不要なテーマ、プラグイン、ファイルは削除した上で以下の通りzip圧縮し、wordpress.zipをダウンロードする。

# WordPress設置ディレクトリに移動
cd /var/www/html

# WordPressディレクトリを丸ごとzip圧縮
zip -r wordpress.zip wordpress

また、移行後のサーバにアップロードの上、以下で展開と所有者・パーミッション設定しておく。

# WordPressを設置するディレクトリに移動
cd /var/www/html

# (必要に応じて)zipのインストール
dnf -y install zip

# 展開
unzip wordpress.zip

# zipファイルは削除
rm -r wordpress.zip

# ホスト側の所有者・グループ設定
chown -R test_user:nginx /var/www/html

# ディレクトリとファイルのパーミッション一括設定
find /var/www/html -type d -exec chmod 775 {} +
find /var/www/html -type f -exec chmod 664 {} +

# SGID設定
find /var/www/html -type d -exec chmod g+s {} +

# 現在あるファイル・フォルダに対し、uid33(www-data)のユーザーに全権限を付与
setfacl -R -m u:33:rwx /var/www/html

# 今後作られるファイル・フォルダに対しても上記設定が引き継がれるよう設定
setfacl -R -d -m u:33:rwx /var/www/html

データベース関連(MySQL)

データベースに関して以下の通り、リビジョンの削除やMyISAM設定されているテーブルがある場合はInnoDBに変更しておく。

# リビジョンの削除
DELETE FROM wp_posts 
WHERE post_type = 'revision';

# MyISAM設定されたテーブルを表示
SELECT TABLE_NAME, ENGINE 
FROM information_schema.TABLES 
WHERE TABLE_SCHEMA = DATABASE() 
AND ENGINE = 'MyISAM';

# InnoDBに変更するSQL文を表示
SELECT CONCAT('ALTER TABLE ', TABLE_NAME, ' ENGINE=InnoDB;') AS ExecutionSQL
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() 
AND ENGINE = 'MyISAM';

上記対応後にエクスポートし、新サーバ側でインポートする。尚、以下データベースは移行しないよう注意。

  • information_schema
  • mysql
  • performance_schema
  • sys

また、新サーバ側でWordPress用のユーザーを作成するか、もしくは既存ユーザーに権限を与える。

# ---------- WordPress用ユーザーを追加する場合 ----------

# DBコンテナにログイン
podman exec -it 【データベース用コンテナ名】 mariadb -u root -p【rootパスワード】

# ユーザー作成し権限を付与(データベース名、ユーザー、パスワードは適宜変更)
CREATE USER 'ユーザー'@'%' IDENTIFIED BY 'パスワード';
GRANT ALL PRIVILEGES ON データベース名.* TO 'wp_user'@'%';

# 設定反映
FLUSH PRIVILEGES;

# ユーザー一覧確認
SELECT user, host FROM mysql.user;


# ---------- 既存ユーザーに全権限を付与する場合 ----------

# DBコンテナにログイン
podman exec -it 【データベース用コンテナ名】 mariadb -u root -p【rootパスワード】

# 既存ユーザーに全権限付与
GRANT ALL PRIVILEGES ON *.* TO 'ユーザー'@'%' WITH GRANT OPTION;

# 設定反映
FLUSH PRIVILEGES;

# 既存ユーザーの権限を確認(GRANT ALL PRIVILEGES ONが付いていればOK)
SHOW GRANTS FOR 'ユーザー'@'%';
(必要に応じて)サイト名を一時的に変更

尚、移行後のサーバなのか分かりやすくするためwp_optionsテーブルのoption_name=blognameのoption_valueに「(サーバ移行中)」とかを追記しておくとWordPressのサイト側やダッシュボード側でその旨表示される。必要に応じて設定しておくと良い。

Let’s Encrypt設定

# epalリポジトリの有効化
dnf install -y epel-release

# 念のためアップデート
dnf update -y

# Certbot本体のインストール
dnf install -y certbot

# Let's Encryptのステージング環境でテスト
certbot certonly --manual --preferred-challenges dns -d test.com --staging

# 以下メッセージが表示される
Please deploy a DNS TXT record under the name:
_acme-challenge.【ドメイン】
with the following value:
【ワンタイムトークン】

DNSに以下の通り設定する。

  • サブドメイン -> _acme-challenge
  • 種別 -> TXT
  • 内容 -> ワンタイムトークン

設定後、数分~数十分ほどで反映される。また、以下から反映されたかどうか確認できる。

https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.【ドメイン】

反映されたらSSHクライアントソフト側でエンターキーを押す。また、ここまでで問題無いようなら以下の本番環境用コマンドで証明書を取得する。

# 問題無ければ本番環境でSSL証明書導入
certbot certonly --manual --preferred-challenges dns -d test.com

Nginx設定

/etc/nginx/nginx.conf

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
    use epoll;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    server_tokens off;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # オープンファイルキャッシュ
    open_file_cache max=1000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # gzip圧縮設定
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 4;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
    gzip_min_length 1024;

    # メモリ設定
    client_body_buffer_size 16k;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    output_buffers 1 32k;
    postpone_output 1460;

    # keepalive設定
    keepalive_timeout 30s;
    keepalive_requests 100;

    # カスタム設定ファイルの読み込み
    include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/conf.d/custom.conf

# =========================================================
# IPアドレス直接指定・未定義ドメインのアクセスを拒否
# =========================================================
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;

    # SSL証明書の設定
    ssl_certificate     /etc/letsencrypt/live/test.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/test.com/privkey.pem;

    # レスポンスは何も返さずに接続を拒否
    return 444;
}

# =========================================================
# HTTP -> HTTPSリダイレクト
# =========================================================
server {
    listen 80;
    listen [::]:80;
    server_name test.com www.test.com;

    # すべてHTTPSへ301リダイレクト
    return 301 https://test.com$request_uri;
}

# =========================================================
# wwwあり -> wwwなしへリダイレクト
# =========================================================
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.test.com;

    # SSL証明書の設定
    ssl_certificate     /etc/letsencrypt/live/test.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/test.com/privkey.pem;

    # wwwなしへ301リダイレクト
    return 301 https://test.com$request_uri;
}

# =========================================================
# コンテナへリバースプロキシ(メイン設定)
# =========================================================
server {

    # -----------------------------------------------------
    # 共通設定
    # -----------------------------------------------------
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name test.com;

    # SSL証明書の設定
    ssl_certificate     /etc/letsencrypt/live/test.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/test.com/privkey.pem;

    # セキュリティヘッダーの設定
    add_header X-Frame-Options "SAMEORIGIN" always; 
    add_header X-Content-Type-Options "nosniff" always; 
    add_header X-XSS-Protection "1; mode=block" always; 

    # プロキシタイムアウトの設定
    proxy_connect_timeout 60s;
    proxy_send_timeout    60s;
    proxy_read_timeout    60s;

    # セキュリティ・TLS設定
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # クライアントからの最大アップロードサイズ
    client_max_body_size 20M;

    # ログ設定
    access_log /var/log/nginx/test.com.access.log;
    error_log  /var/log/nginx/test.com.error.log;

    # 公開用ディレクトリ
    root /var/www/html/podman/test.com/public;
    index index.php index.html;

    # バックエンドでの圧縮を無効化
    proxy_set_header Accept-Encoding "";

    # -----------------------------------------------------
    # 静的ファイルのキャッシュ設定
    # -----------------------------------------------------
    location ~* \.(?:css|js|gif|png|jpe?g|pdf|swf|rss|atom|ico|woff2?|svg)$ {
        access_log off;
        log_not_found off;

        add_header Cache-Control "public, max-age=31536000, no-transform" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        # ホスト側を探して無ければ下の@containerブロックへ
        try_files $uri @container;
    }

    location @container {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # -----------------------------------------------------
    # WordPress
    # -----------------------------------------------------

    # ログインページにアクセスIP制限をかける場合
    location = /wordpress/wp-login.php {
        # 許可するIP
        allow xxx.xxx.xxx.xxx;
        deny all;

        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /wordpress/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # -----------------------------------------------------
    # セキュリティ系
    # -----------------------------------------------------

    # 直アクセスを念のためブロック
    location ~ /(^\.git|static_config) {
        deny all;
    }

    location ^~ /vendor/ {
        deny all;
    }

    # basic認証をかける場合
    location ^~ /basic_auth/ {
        auth_basic "Restricted Area";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # -----------------------------------------------------
    # 他のどれにも当てはまらない通常アクセス
    # -----------------------------------------------------

    # 通常のコンテナへのリバースプロキシ設定
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

再起動

上記2ファイル編集後、再起動する。

nginx -t
systemctl reload nginx

Apache設定

/opt/podman/apache/vhost.conf

LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
LoadModule expires_module /usr/lib/apache2/modules/mod_expires.so

<VirtualHost *:80>
    ServerName test.com
    ServerAlias www.test.com

    DocumentRoot /var/www/html/test.com/public

    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 127.0.0.1 10.0.2.2 172.17.0.1 10.89.0.0/16

    <Directory /var/www/html/test.com/public>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

再起動

上記ファイル編集後、Cockpitから再起動する。

hosts設定

まだDNSは切り替えずに動作確認するためローカル環境のhostsファイルで以下を設定しておく。

xxx.xxx.xxx.xxx test.com

この時点でブラウザからドメインにアクセスできる筈。ただ、WordPressのフロント側はDB接続エラー、ダッシュボード側は500エラーになると思われるので以下の通りWordPress設定を行う。

WordPress設定

.htaccess

.htaccess内の設定により500エラーになる場合はバックアップを取った上で、以下のデフォルトファイルで上書きしておく。

# BEGIN WordPress
# "BEGIN WordPress" から "END WordPress" までのディレクティブ (行) は
# 動的に生成され、WordPress フィルターによってのみ修正が可能です。
# これらのマーカー間にあるディレクティブへのいかなる変更も上書きされてしまいます。
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /blog/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /blog/index.php [L]
</IfModule>

# END WordPress

私の環境だとReally Simple Securityで.htaccessを書き換えており500エラーになっていたっぽい。

尚、Really Simple SecurityでログインページURLを変更していたような場合、上記デフォルトファイルで上書きすると元のURLに戻るが、その辺りの制御や設定が難しそうなのでNginxによるアクセスIP制限の導入を検討する。

wp-config.php

データベース接続情報の変更と以下を追記して上書き。接続情報はDB_HOSTがコンテナ名になる点に注意。

# プロキシサーバ経由かつHTTPS接続の場合はPHPの環境変数を書き換え
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

# Webサーバ権限でファイル書き換え
define('FS_METHOD', 'direct');

# ディレクトリ・ファイルを作成した際のパーミッション
define('FS_CHMOD_DIR', 0775);
define('FS_CHMOD_FILE', 0664);

# 新規作成ファイルのデフォルトパーミッションを設定
umask(0002);

キャッシュ設定

W3 Total Cacheプラグインをファイルアップロード形式で追加し以下設定を行う。

  • ページキャッシュ -> ディスク
  • データベースキャッシュ -> なし
  • オブジェクトキャッシュ -> apcu

動作確認

サイト側とダッシュボード側で各種動作確認を行う。

ダッシュボード側でAll in One SEOの「記事一覧のAIOSEO 詳細」「ダッシュボードTOPのAIOSEO ChecklistやAIOSEO 概要」等はブラウザ拡張機能のAdGuardでブロックされ、ずっと読み込み中となっていた。こちらは拡張機能をOFFにすると表示される。

ここまでで問題無いようならドメインのDNSを切り替えて良い筈。

 

その他

Let's Encryptの自動更新

DNS認証でSSL証明書を取得しているので恐らく過去記事のように自動更新時にエラーが起こる筈。

そのため自動更新設定が必要になる。また、今回はサブドメインをHTTP認証で取りたいためメインドメインとあわせて再取得する。

まずはHTTP認証できるようにNginx / Apache設定を行う。

Nginx設定(/etc/nginx/conf.d/costom.conf)

server {
    server_name sub.test.com;

    # ログ設定
    access_log /var/log/nginx/sub.test.com.access.log;
    error_log  /var/log/nginx/sub.test.com.error.log;

    # 公開用ディレクトリ
    root /var/www/html/podman/sub.test.com/public;
    index index.php index.html;

    # コンテナへのリバースプロキシ設定
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Apache設定(/opt/podman/apache/vhost.conf)

<VirtualHost *:80>
    ServerName sub.test.com

    DocumentRoot /var/www/html/sub.test.com/public

    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 127.0.0.1 10.0.2.2 172.17.0.1 10.89.0.0/16

    <Directory /var/www/html/sub.test.com/public>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

HTTP認証で証明書再取得と自動更新設定

# Nginx用のCerbotをインストール
dnf install python3-certbot-nginx

# 証明書の取得テスト
certbot certonly --nginx -d test.com -d sub.test.com --dry-run

# テストで問題無ければNginxプラグイン経由で証明書を再取得とNginx自動再起動設定
certbot certonly --nginx -d test.com -d sub.test.com --deploy-hook "systemctl reload nginx" --force-renewal

# 設定ファイルにフック(renew_hook = systemctl reload nginx)が正しく書き込まれたか確認
cat /etc/letsencrypt/renewal/test.com.conf

# 自動更新のテスト
certbot renew --dry-run --deploy-hook "systemctl reload nginx"

# certbotの全体設定ファイルを開く
vi /etc/letsencrypt/cli.ini

以下を追記する。

# メールアドレス設定
email = test@test.com

# 自動化用の設定(規約同意を自動化)
agree-tos = true

# 全ドメイン共通の更新後フック(Nginx自動リロード)
deploy-hook = systemctl reload nginx

# 暗号鍵のデフォルト設定
key-type = ecdsa

ファイルを保存後、以下で最終確認を行う。

# Nginxの再起動も含めて自動更新時の一連の流れがエラーなく通るかシミュレーション
certbot renew --dry-run --deploy-hook "systemctl reload nginx" --run-deploy-hooks

# 自動更新タイマーがアクティブか確認
systemctl status certbot-renew.timer

いずれも問題無かった場合、Nginx側でhttps接続できるよう以下変更・追記を行う。

# リダイレクト用ブロックを変更
server {
    listen 80;
    listen [::]:80;
    server_name test.com www.test.com sub.test.com;

    # すべてHTTPSへ301リダイレクト
    return 301 https://$host$request_uri;
}

# 以下ブロックを追記
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name sub.test.com;

    # ログ設定
    access_log /var/log/nginx/sub.test.com.access.log;
    error_log  /var/log/nginx/sub.test.com.error.log;

    # 公開用ディレクトリ
    root /var/www/html/podman/sub.test.com/public;
    index index.php index.html;

    # SSL証明書の設定
    ssl_certificate      /etc/letsencrypt/live/test.com/fullchain.pem;
    ssl_certificate_key  /etc/letsencrypt/live/test.com/privkey.pem;
    include              /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam          /etc/letsencrypt/ssl-dhparams.pem;

    # 通常のコンテナへのリバースプロキシ設定
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

サブドメインは取得せず自動更新設定のみ行いたい場合

以下で自動更新設定を行い、各種テストを行う。

certbot renew --nginx --deploy-hook "systemctl reload nginx" --force-renewal

新たにSSL証明書を取得する場合

別ドメインを取得する場合は「/etc/letsencrypt/cli.ini」の設定が効くはずだけど、サブドメインを増やして尚且つメインドメインも取り直す場合は念のためフックを指定する。

# 別ドメインを取得時
certbot certonly --nginx -d newdomain.com

# サブドメインを増やす場合(他もまとめて取得する場合)
certbot certonly --nginx -d test.com -d sub.test.com -d sub2.test.com --deploy-hook "systemctl reload nginx" --force-renewal

 

所感

今回はNginx側の設定で色々難儀した。わかる範囲で設定してみたものの全体的に正しい設定なのか分からずちょっと不安なところ。

何かあった際、元に戻せるように旧サーバはしばらく置いておこうと思う。尚、Indigoでインスタンスを削除してスナップショットだけ残そうかと考えたがそれは無理みたい。

 - WordPress サーバー CMS Podman

  関連記事

サーバ移行の際にLet’s EncryptをDNS認証で対応する方法
サーバ移行の際にLet’s EncryptをDNS認証で対応する方法

近々CentOS7系からAlmaLinux8系への移行(サーバも移行)を検討して ...

AlmaLinux8.6にLAMP環境&WordPressを構築する方法
AlmaLinux8.6にLAMP環境&WordPressを構築する方法

だいぶ前にCentOS6系から7系に移行したが、7系も2024/6/30にサポー ...

VPSに無料SSLのLet's Encryptを導入
VPSに無料SSLのLet's Encryptを導入

SSLといえば有料という認識だったが、無料のものも出てきているという話を聞いた。 ...

Let's Encryptの自動更新時「Failed to renew certificate~」エラーの対応方法
Let's Encryptの自動更新時「Failed to renew certificate~」エラーの対応方法

CentOSからAlmaLinuxに移行後、初めてのLet's Encrypt更 ...

AlmaLinux8系にNginxを導入しPodmanコンテナに接続する方法
AlmaLinux8系にNginxを導入しPodmanコンテナに接続する方法

AlmaLinux8でホスト側はApacheからPodmanコンテナ環境に接続し ...