Skip to main content

在 HMS VPS 上使用 Bedrock 实现 WordPress GitOps

本指南将介绍如何在 HostMyServers VPS 上托管 WordPress 网站,并借助 Bedrock(Roots 提供的 WordPress 脚手架)以 GitOps 方式进行管理。您的 Git 仓库将成为唯一可信源:WordPress 核心、插件和主题都以代码形式声明并纳入版本控制,然后在每次 git push 时自动部署到您的 VPS。

VPS 为您提供 root 权限、SSH 访问以及对整套软件栈(Nginx、PHP-FPM、MariaDB)的完全控制,这是这种部署方式所必需的,而共享主机无法提供这些条件。

为什么选择 Bedrock?

传统的 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-22 GB20 GB NVMe
中等流量、多站点2-44 GB40 GB NVMe
WooCommerce / 高流量4+8 GB+80 GB NVMe+

下单时,请选择 Ubuntu 24.04 LTSDebian 12 作为操作系统。VPS 交付后,IP 地址和 root 登录凭据会通过邮件发送给您,同时也可以在您的 HostMyServers 客户空间中查看。

更大规模的项目

对于多个高流量站点,同样的流程也适用于 EcoPerformance 专用服务器。

将 GitOps 原则应用于 WordPress

元素存放位置是否纳入 Git 版本控制?
WordPress 核心、公共插件和主题composer.json + composer.lock是(声明 + 精确版本)
您自己开发的主题和插件web/app/themes/web/app/plugins/是(源代码)
配置(常量、环境)config/
密钥(密码、密钥、盐值)每台服务器上的 .env
媒体文件(上传内容)服务器上的 web/app/uploads/(需要备份)
内容(文章、页面、设置)MySQL/MariaDB 数据库(需要备份)

生命周期变为:

  1. 在本地修改代码或更新依赖
  2. 您提交并推送到 main 分支
  3. CI 流水线安装依赖,并在服务器上部署一个新的发布版本(release)
  4. 如果出现问题,您可以回退到上一个发布版本(git revert 或切换符号链接)
不受 Git 管理的内容

数据库和媒体文件属于数据,而非代码,不由 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

在本指南中,composer install 由 CI 流水线执行。VPS 上只接收已经准备好的文件:它既不需要 Composer,也不需要 Git,也不需要访问软件包仓库。

步骤 1:在本地创建 Bedrock 项目

  1. 创建项目:

    composer create-project roots/bedrock mon-site
    cd mon-site
  2. 了解目录结构:

    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.php
    info

    使用 Bedrock 时,管理后台地址为 https://your-domain.com/wp/wp-admin,且 wp-contentweb/app 取代。

  3. 基于模板创建本地 .env 文件:

    cp .env.example .env
    .env
    DB_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'
  4. 每个环境生成独立的密钥和盐值:

    for k in AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT; do
    echo "$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 镜像。

  1. 安装插件或主题:

    composer require wpackagist-plugin/wordpress-seo
    composer require wpackagist-plugin/wp-mail-smtp
    composer require wpackagist-theme/twentytwentyfive

    软件包名称对应 WordPress.org 的 slughttps://wordpress.org/plugins/<slug>/)。

  2. 更新依赖:

    # 根据 composer.json 的约束更新所有内容
    composer update

    # 仅更新 WordPress 核心
    composer update roots/wordpress --with-all-dependencies

    # 查看可用的更新
    composer outdated
  3. 删除插件:

    composer remove wpackagist-plugin/wp-mail-smtp
始终提交 composer.lock

composer.lock 文件确保生产环境安装的版本与本地测试过的版本完全一致,因此必须始终纳入版本控制。

付费或私有插件

付费插件不在 WPackagist 上。有两种解决方案:

  • 推荐做法:使用发布方提供的 Composer 仓库(ACF Pro、Gravity Forms、WPML 等均提供此类仓库),并将认证密钥存放在 auth.json(未纳入版本控制)以及 CI 的密钥管理中。

  • 备选方案:在 .gitignore 中添加例外规则,将插件直接纳入仓库版本控制:

    .gitignore
    web/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/application.php
Config::define('WP_POST_REVISIONS', 10);
Config::define('WP_MEMORY_LIMIT', '256M');

步骤 4:初始化 Git 仓库

Bedrock 提供了一份合适的 .gitignorevendor/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
/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 永远不会被暴露。

/etc/nginx/sites-available/mon-site
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

使用 $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_USERdeploy
SSH_PRIVATE_KEYdeploy_key 文件的内容(私钥)
SSH_KNOWN_HOSTSssh-keyscan 命令的输出结果

随后从您的机器上删除 deploy_keydeploy_key.pub 文件。

创建工作流

.github/workflows/deploy.yml
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 版本

GitLab CI 也适用同样的原理:使用 composer:2 镜像执行 composer install,然后用 rsyncssh,以受保护的 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_ENVstagingdevelopment 的环境中,站点会要求搜索引擎不要将其编入索引(DISALLOW_INDEXING)。因此只有生产环境会出现在搜索结果中。

日常工作

更新 WordPress 和插件

composer update
# 在本地测试网站,然后:
git add composer.json composer.lock
git commit -m "chore: update WordPress and plugins"
git push

部署会自动完成。若想在有新版本时收到通知,可在仓库中启用 Dependabotcomposer 生态系统)或 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
caution

代码回滚并不会恢复数据库。如果某次更新修改了数据库结构,还需要恢复部署前所做的 SQL 备份。

将现有 WordPress 网站迁移到 Bedrock

  1. 列出旧站点上已安装的插件和主题:wp plugin listwp theme list

  2. 使用 composer require wpackagist-plugin/<slug> 将它们添加到 Bedrock 项目中(并将您自己开发的内容纳入版本控制)

  3. 将旧的 wp-content/uploads/ 目录复制到 /var/www/mon-site/shared/uploads/

  4. 导入数据库,然后修正媒体文件路径,将其从 wp-content 改为 app

    cd /var/www/mon-site/current
    sudo -u www-data wp db import /path/to/backup.sql
    sudo -u www-data wp search-replace '/wp-content/uploads' '/app/uploads' --all-tables
  5. 如果数据表前缀不是 wp_,请在 .env 中相应调整 DB_PREFIX

tip

在执行任何 wp search-replace 之前,先加上 --dry-run 进行试运行,以检查替换的数量。

最佳实践

  • 切勿直接在服务器上修改文件:所有改动都必须通过 Git 完成
  • 始终提交 composer.lock,并在 composer.json 中锁定关键版本
  • 将密钥保存在仓库之外(.envauth.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_KEYSSH_KNOWN_HOSTS 密钥,并从您的机器上测试 ssh deploy@vps_ip_address
  • 无法从管理后台安装插件:这在生产环境中是正常现象(DISALLOW_FILE_MODS),请使用 Composer