在 HMS VPS 上使用 Bedrock 实现 WordPress GitOps
本指南将介绍如何在 HostMyServers VPS 上托管 WordPress 网站,并借助 Bedrock(Roots 提供的 WordPress 脚手架)以 GitOps 方式进行管理。您的 Git 仓库将成为唯一可信源:WordPress 核心、插件和主题都以代码形式声明并纳入版本控制,然后在每次 git push 时自动部署到您的 VPS。
VPS 为您提供 root 权限、SSH 访问以及对整套软件栈(Nginx、PHP-FPM、MariaDB)的完全控制,这是这种部署方式所必需的,而共享主机无法提供这些条件。
传统的 WordPress 会将代码(核心、插件、主题)、配置(wp-config.php)和数据(上传文件)混杂在同一个目录中,并且通过管理后台进行更新。这使得版本控制和环境复现变得困难。Bedrock 带来了以下改进:
- 使用 Composer 将 WordPress 核心、插件和主题当作依赖项管理(版本锁定在
composer.lock中) - 通过环境变量进行配置(
.env文件),不在 Git 中存放任何密钥 - 按环境区分配置文件(开发、预发布、生产)
- 更安全的目录结构:Web 服务器只 暴露
web/目录 - 在生产环境中禁用后台修改文件的功能(
DISALLOW_FILE_MODS)
订购 HostMyServers VPS
本指南适用于 HostMyServers VPS。请根据您的流量情况选择合适的方案:
- NVMe VPS - 性价比极高,适合搭建展示型网站或博客
- Performance VPS - 推荐用于 WooCommerce 商城和高流量网站
| 用途 | vCPU | 内存 | 存储 |
|---|---|---|---|
| 展示型网站 / 博客 | 1-2 | 2 GB | 20 GB NVMe |
| 中等流量、多站点 | 2-4 | 4 GB | 40 GB NVMe |
| WooCommerce / 高流量 | 4+ | 8 GB+ | 80 GB NVMe+ |
下单时,请选择 Ubuntu 24.04 LTS 或 Debian 12 作为操作系统。VPS 交付后,IP 地址和 root 登录凭据会通过邮件发送给您,同时也可以在您的 HostMyServers 客户空间中查看。
对于多个高流量站点,同样的流程也适用于 Eco 和 Performance 专用服务器。
将 GitOps 原则应用于 WordPress
| 元素 | 存放位置 | 是否纳入 Git 版本控制? |
|---|---|---|
| WordPress 核心、公共插件和主题 | composer.json + composer.lock | 是(声明 + 精确版本) |
| 您自己开发的主题和插件 | web/app/themes/、web/app/plugins/ | 是(源代码) |
| 配置(常量、环境) | config/ | 是 |
| 密钥(密码、密钥、盐值) | 每台服务器上的 .env | 否 |
| 媒体文件(上传内容) | 服务器上的 web/app/uploads/ | 否(需要备份) |
| 内容(文章、页面、设置) | MySQL/MariaDB 数据库 | 否(需要备份) |
生命周期变为:
- 您在本地修改代码或更新依赖
- 您提交并推送到
main分支 - CI 流水线安装依赖,并在服务器上部署一个新的发布版本(release)
- 如果出现问题,您可以回退到上一个发布版本(
git revert或切换符号链接)
数据库和媒体文件属于数据,而非代码,不由 Git 管理:请建立定期备份机制(wp db export、备份 shared/uploads 目录、VPS 快照)。
前提条件
在您的开发机器上:
- Git
- PHP ≥ 8.1(推荐 8.3)以及 Composer 2
- 一个用于托管仓库的 GitHub(或 GitLab)账号
在您的 HMS VPS(Ubuntu 24.04 LTS / Debian 12)上:
- root 用户或具有 sudo 权限的用户的 SSH 访问权限
- 一台已加固的 VPS(非 root 用户、SSH、防火墙):参见加固服务器安全
- 一个域名,其 DNS
A记录指向该 VPS 的 IP 地址
在本指南中,composer install 由 CI 流水线执行。VPS 上只接收已经准备好的文件:它既不需要 Composer,也不需要 Git,也不需要访问软件包仓库。
步骤 1:在本地创建 Bedrock 项目
-
创建项目:
composer create-project roots/bedrock mon-sitecd mon-site -
了解目录结构:
mon-site/├── composer.json # 声明 WordPress 核心、插件和主题├── composer.lock # 已安装的精确版本├── .env.example # 配置模板├── wp-cli.yml # 告知 WP-CLI WordPress 所在位置(web/wp)├── config/│ ├── application.php # 主配置(取代 wp-config.php)│ └── environments/│ ├── development.php│ └── staging.php├── vendor/ # Composer 依赖(未纳入版本控制)└── web/ # Web 服务器根目录(document root)├── app/ # 相当于 wp-content│ ├── mu-plugins/│ ├── plugins/│ ├── themes/│ └── uploads/├── wp/ # WordPress 核心(未纳入版本控制)├── wp-config.php└── index.phpinfo使用 Bedrock 时,管理后台地址为
https://your-domain.com/wp/wp-admin,且wp-content被web/app取代。 -
基于模板创建本地
.env文件:cp .env.example .env.envDB_NAME='wordpress_dev'DB_USER='wordpress_user'DB_PASSWORD='local_password'DB_HOST='localhost'DB_PREFIX='wp_'WP_ENV='development'WP_HOME='http://mon-site.test'WP_SITEURL="${WP_HOME}/wp"AUTH_KEY='generateme'SECURE_AUTH_KEY='generateme'LOGGED_IN_KEY='generateme'NONCE_KEY='generateme'AUTH_SALT='generateme'SECURE_AUTH_SALT='generateme'LOGGED_IN_SALT='generateme'NONCE_SALT='generateme' -
为每个环境生成独立的密钥和盐值:
for k in AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT; doecho "$k='$(openssl rand -base64 48 | tr -d '\n')'"done将输出结果复制到您的
.env文件中,替换掉generateme所在的行。您也可以使用 Roots 生成器。
若要在本地运行站点,可使用您喜欢的工具(DDEV、Lando、Laravel Valet、Docker 等),并将 Web 根目录指向 web/ 文件夹。
步骤 2:使用 Composer 管理插件和主题
Bedrock 已预先配置好 WPackagist,这是 WordPress.org 官方目录中所有插件和主题的 Composer 镜像。
-
安装插件或主题:
composer require wpackagist-plugin/wordpress-seocomposer require wpackagist-plugin/wp-mail-smtpcomposer require wpackagist-theme/twentytwentyfive软件包名称对应 WordPress.org 的 slug(
https://wordpress.org/plugins/<slug>/)。 -
更新依赖:
# 根据 composer.json 的约束更新所有内容composer update# 仅更新 WordPress 核心composer update roots/wordpress --with-all-dependencies# 查看可用的更新composer outdated -
删除插件:
composer remove wpackagist-plugin/wp-mail-smtp
composer.lock 文件确保生产环境安装的版本与本地测试过的版本完全一致,因此必须始终纳入版本控制。
付费或私有插件
付费插件不在 WPackagist 上。有两种解决方案:
-
推荐做法:使用发布方提供的 Composer 仓库(ACF Pro、Gravity Forms、WPML 等均提供此类仓库),并将认证密钥存放在
auth.json(未纳入版本控制)以及 CI 的密钥管理中。 -
备选方案:在
.gitignore中添加例外规则,将插件直接纳入仓库版本控制:.gitignoreweb/app/plugins/*!web/app/plugins/.gitkeep!web/app/plugins/mon-plugin-premium
您自己的主题(位于 web/app/themes/)默认会被纳入版本控制。
步骤 3:调整配置
通用配置位于 config/application.php 中,各环境的覆盖配置位于 config/environments/<WP_ENV>.php 中。默认情况下,Bedrock:
- 在生产环境中禁用文件编辑器以及从管理后台安装/更新插件的功能(
DISALLOW_FILE_MODS) - 在
development环境中允许上述操作 - 在生产环境中隐藏 PHP 错误信息
这一行为正是 GitOps 的核心所在:在生产环境中,任何代码修改都必须通过 Git 完成。如果某个插件显示「有可用更新」,请在本地使用 Composer 完成更新、测试,然后提交。
为所有环境添加一个常量的示例:
Config::define('WP_POST_REVISIONS', 10);
Config::define('WP_MEMORY_LIMIT', '256M');
步骤 4:初始化 Git 仓库
Bedrock 提供了一份合适的 .gitignore:vendor/、web/wp/、通过 Composer 安装的插件、上传文件以及 .env 文件都会被忽略。
git init -b main
git add .
git commit -m "Initial Bedrock project"
git remote add origin git@github.com:your-account/mon-site.git
git push -u origin main
检查是否有密钥被意外纳入版本控制:
git ls-files | grep -E '(^|/)\.env$|auth\.json' || echo "OK: 未发现已提交的密钥文件"
步骤 5:准备 HMS VPS
连接到 VPS
使用交付时收到的 IP 地址和登录凭据进行连接:
ssh root@vps_ip_address
安装 Nginx、PHP-FPM 和 MariaDB
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mariadb-server \
php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip php-imagick
php -v
Ubuntu 24.04 会安装 PHP 8.3(Debian 12 则为 PHP 8.2)。在本指南后续内容中,请将 php8.3-fpm 替换为 php -v 显示的实际版本。如需加固 MariaDB,请参阅安装并加固 MariaDB指南。
如果 UFW 防火墙处于启用状态,请开放 Web 端口:
sudo ufw allow 'Nginx Full'
创建部署用户和目录结构
流水线将使用专用用户 deploy 进行连接。代码归属于 deploy 用户,对 PHP(www-data)而言仅可读;只有上传目录允许 PHP 写入。
sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG www-data deploy
sudo mkdir -p /var/www/mon-site/{releases,shared/uploads}
sudo chown -R deploy:www-data /var/www/mon-site
sudo chown -R www-data:www-data /var/www/mon-site/shared/uploads
sudo chmod 2775 /var/www/mon-site/shared/uploads
部署目录结构如下:
/var/www/mon-site/
├── current -> releases/<sha> # 指向当前活跃发布版本的符号链接
├── releases/
│ ├── 3f2a9c1.../ # 每个已部署的提交对应一个发布版本
│ └── 8be41d0.../
└── shared/
├── .env # 生产环境配置(不在 Git 中)
└── uploads/ # 各发布版本间持久保存的媒体文件
创建数据库
sudo mysql -u root -p
CREATE DATABASE wordpress_prod DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON wordpress_prod.* TO 'wordpress_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
创建生产环境的 .env 文件
sudo -u deploy nano /var/www/mon-site/shared/.env
DB_NAME='wordpress_prod'
DB_USER='wordpress_user'
DB_PASSWORD='secure_password'
DB_HOST='localhost'
DB_PREFIX='wp_'
WP_ENV='production'
WP_HOME='https://your-domain.com'
WP_SITEURL="${WP_HOME}/wp"
# 使用步骤 1 中的 openssl 命令生成的密钥和盐值
AUTH_KEY='...'
SECURE_AUTH_KEY='...'
LOGGED_IN_KEY='...'
NONCE_KEY='...'
AUTH_SALT='...'
SECURE_AUTH_SALT='...'
LOGGED_IN_SALT='...'
NONCE_SALT='...'
sudo chown deploy:www-data /var/www/mon-site/shared/.env
sudo chmod 640 /var/www/mon-site/shared/.env
配置 Nginx
Web 根目录指向 current/web:代码、vendor/ 和 .env 永远不会被暴露。
server {
listen 80;
server_name your-domain.com www.your-domain.com;
root /var/www/mon-site/current/web;
index index.php;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$args;
}
# 阻止在上传目录中执行 PHP
location ~* ^/app/uploads/.*\.php$ {
deny all;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
# 每次请求都重新解析 "current" 符号链接
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}
location ~ /\. {
deny all;
}
}
sudo ln -s /etc/nginx/sites-available/mon-site /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
使用 $realpath_root(而不是 $document_root)可以确保 PHP-FPM 在 current 链接切换后立即加载新发布版本的文件,而不会使用缓存中的旧路径。
接下来启用 HTTPS,例如使用 Certbot(另请参阅安装 SSL 证书):
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
授权重新加载 PHP-FPM
流水线会在每次部署后重新加载 PHP-FPM 以清空 OPcache。请仅为 deploy 用户授权该命令:
echo 'deploy ALL=(root) NOPASSWD: /usr/bin/systemctl reload php8.3-fpm' | sudo tee /etc/sudoers.d/deploy
sudo chmod 440 /etc/sudoers.d/deploy
sudo visudo -c
创建部署用 SSH 密钥
在您的机器上,生成一对专用于流水线的密钥:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./deploy_key -N ""
将公钥添加到 VPS 上:
sudo mkdir -p /home/deploy/.ssh
sudo nano /home/deploy/.ssh/authorized_keys # 粘贴 deploy_key.pub 的内容
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh && sudo chmod 600 /home/deploy/.ssh/authorized_keys
获取 VPS 的 SSH 指纹(以防范中间人攻击):
ssh-keyscan -H vps_ip_address
步骤 6:使用 GitHub Actions 自动化部署
声明密钥
在您的 GitHub 仓库中,进入 Settings → Secrets and variables → Actions,创建以下密钥:
| 密钥 | 值 |
|---|---|
SSH_HOST | 您的 HMS VPS 的 IP 地址 |
SSH_USER | deploy |
SSH_PRIVATE_KEY | deploy_key 文件的内容(私钥) |
SSH_KNOWN_HOSTS | ssh-keyscan 命令的输出结果 |
随后从您的机器上删除 deploy_key 和 deploy_key.pub 文件。
创建工作流
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
env:
BASE_DIR: /var/www/mon-site
TARGET: ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
- name: 安装依赖
run: composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
- name: 配置 SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/id_ed25519
- name: 发送发布版本
run: |
rsync -az --delete \
--exclude='.git' --exclude='.github' \
--exclude='.env' --exclude='web/app/uploads' \
./ "$TARGET:$BASE_DIR/releases/$GITHUB_SHA/"
- name: 激活发布版本
run: |
ssh "$TARGET" bash -s -- "$BASE_DIR" "$GITHUB_SHA" <<'EOF'
set -euo pipefail
BASE_DIR="$1"
RELEASE="$BASE_DIR/releases/$2"
# 链接共享的配置和媒体文件
ln -sfn "$BASE_DIR/shared/.env" "$RELEASE/.env"
rm -rf "$RELEASE/web/app/uploads"
ln -sfn "$BASE_DIR/shared/uploads" "$RELEASE/web/app/uploads"
# 原子化切换 "current" 链接
ln -sfn "$RELEASE" "$BASE_DIR/current.tmp"
mv -Tf "$BASE_DIR/current.tmp" "$BASE_DIR/current"
# 清空 OPcache
sudo systemctl reload php8.3-fpm
# 只保留最近 5 个发布版本
cd "$BASE_DIR/releases"
ls -1t | tail -n +6 | xargs -r rm -rf
EOF
提交并推送该文件:首次部署会自动开始。您可以在仓库的 Actions 标签页中查看执行过程。
GitLab CI 也适用同样的原理:使用 composer:2 镜像执行 composer install,然后用 rsync 和 ssh,以受保护的 CI/CD 变量取代 GitHub 密钥。
首次启动
首次部署成功后,安装 WordPress。可以通过浏览器访问 https://your-domain.com/wp/wp-admin/install.php,也可以在服务器上使用 WP-CLI:
cd /var/www/mon-site/current
sudo -u www-data wp core install \
--url="https://your-domain.com" \
--title="Your Site Title" \
--admin_user="admin" \
--admin_password="StrongPassword123!" \
--admin_email="your@email.com"
请参阅使用 WP-CLI 安装 WordPress指南,了解如何在服务器上安装 WP-CLI。
XML 站点地图与 SEO
WordPress 原生会在 https://your-domain.com/wp-sitemap.xml 生成站点地图(sitemap)。配合 Bedrock 和上述 Nginx 配置(try_files ... /index.php?$args),站点地图无需额外配置即可正常工作。可通过以下命令进行验证:
curl -sI https://your-domain.com/wp-sitemap.xml | head -n 1
如果您通过 Composer 安装了 SEO 插件(例如 composer require wpackagist-plugin/wordpress-seo),该插件会用自己的站点地图替换原生站点地图(Yoast SEO 对应 /sitemap_index.xml)。站点地图始终是从数据库动态生成的,因此没有任何内容需要纳入 Git 版本控制。
随后请记得:
- 在 Google Search Console 和 Bing Webmaster Tools 中提交站点地图 URL
- 确认生产环境中设置 → 阅读 → 搜索引擎可见性未被勾选
Bedrock 内置了 bedrock-disallow-indexing 这个 mu-plugin:在 WP_ENV 为 staging 或 development 的环境中,站点会要求搜索引擎不要将其编入索引(DISALLOW_INDEXING)。因此只有生产环境会出现在搜索结果中。
日常工作
更新 WordPress 和插件
composer update
# 在本地测试网站,然后:
git add composer.json composer.lock
git commit -m "chore: update WordPress and plugins"
git push
部署会自动完成。若想在有新版本时收到通知,可在仓库中启用 Dependabot(composer 生态系统)或 Renovate:它们会自动创建更新用的 pull request,您只需审核合并即可。
使用预发布分支
为了在上线前验证改动,可以创建第二套环境(子域名 staging.your-domain.com、独立目录、独立数据库、WP_ENV='staging'),并复 制工作流使其在 staging 分支上触发。这样 config/environments/staging.php 文件就会自动生效。
回滚
GitOps 方式(推荐):撤销有问题的提交,流水线会重新部署此前的状态。
git revert <commit_sha>
git push
应急方式:直接在服务器上手动将 current 链接切回上一个发布版本。
cd /var/www/mon-site
ls -1t releases/ # 找到上一个发布版本
ln -sfn "$PWD/releases/<previous_sha>" current.tmp && mv -Tf current.tmp current
sudo systemctl reload php8.3-fpm
代码回滚并不会恢复数据库。如果某次更新修改了数据库结构,还需要恢复部署前所做的 SQL 备份。
将现有 WordPress 网站迁移到 Bedrock
-
列出旧站点上已安装的插件和主题:
wp plugin list和wp theme list -
使用
composer require wpackagist-plugin/<slug>将它们添加到 Bedrock 项目中(并将您自己开发的内容纳入版本控制) -
将旧的
wp-content/uploads/目录复制到/var/www/mon-site/shared/uploads/ -
导入数据库,然后修正媒体文件路径,将其从
wp-content改为app:cd /var/www/mon-site/currentsudo -u www-data wp db import /path/to/backup.sqlsudo -u www-data wp search-replace '/wp-content/uploads' '/app/uploads' --all-tables -
如果数据表前缀不是
wp_,请在.env中相应调整DB_PREFIX
在执行任何 wp search-replace 之前,先加上 --dry-run 进行试运行,以检查替换的数量。
最佳实践
- 切勿直接在服务器上修改文件:所有改动都必须通过 Git 完成
- 始终提交
composer.lock,并在composer.json中锁定关键版本 - 将密钥保存在仓库之外(
.env、auth.json),并使用 CI 的密钥管理功能 - 保护
main分支(强制要求 pull request 审核) - 定期备份数据库和
shared/uploads目录,并结合 VPS 的快照/备份功能加以补充 - 先部署到预发布环境进行验证
遇到问题时
- 白屏或 500 错误:检查日志
sudo tail -f /var/log/nginx/error.log和/var/log/php8.3-fpm.log,并确认当前发布版本中存在.env链接 - 数据库连接错误:检查
shared/.env文件中的凭据 - 部署后仍显示旧版本:确认 PHP-FPM 已被正确重新加载,且 Nginx 使用了
$realpath_root - 无法上传媒体文件:确认
shared/uploads属于www-data,且web/app/uploads链接正确指向该目录 - 流水线在 SSH 连接阶段失败:检查
SSH_PRIVATE_KEY和SSH_KNOWN_HOSTS密钥,并从您的机器上测试ssh deploy@vps_ip_address - 无法从管理后台安装插件:这在生产环境中是正常现象(
DISALLOW_FILE_MODS),请使用 Composer