1. 这不是“装个软件”那么简单:Docker Compose + WordPress 部署的本质是什么?
你搜到的标题里写着“安装Docker Compose与部署开源博客WordPress”,但如果你真把它当成一条命令敲完就收工的事,那大概率会在第二天早上被报警邮件叫醒——数据库连不上、静态资源404、后台登录页白屏,或者更糟:某次更新后整个站点变成“Error establishing a database connection”。这不是危言耸听,而是我过去三年在十多个中小团队项目中反复验证过的事实。
这个组合背后,其实是一套轻量级容器化交付闭环:Docker Compose 不是 Docker 的“插件”,而是定义服务拓扑的声明式蓝图;WordPress 也不是一个“开箱即用”的博客程序,而是一个高度依赖外部环境(PHP版本、MySQL配置、文件权限、时区、扩展模块)的运行时系统。两者叠加,本质是在本地或服务器上,用文本文件(docker-compose.yml)精确复刻一套生产就绪的LAMP栈——只是不用手动编译Apache、不用apt install一堆依赖、不用反复chown -R www-data,所有状态都被固化、可版本控制、可一键重建。
关键词“Docker Compose”和“WordPress”高频共现,恰恰说明它已从极客玩具升级为中小项目事实上的标准交付方式。它解决的不是“能不能跑”,而是“能不能稳定、可重复、可审计地跑”。适合谁?不是只适合DevOps工程师,而是:独立开发者想快速验证产品原型、内容团队需要私有化知识库、小公司IT人员要给市场部搭活动页、甚至高校实验室导师带学生做Web开发实训——只要你想避开“在我机器上好好的”这种经典甩锅话术,这套方案就值得你花90分钟真正搞懂。
我试过用一键脚本、宝塔面板、甚至云厂商的“WordPress应用镜像”,最后都绕回自己写docker-compose.yml。因为只有亲手定义每个服务的镜像标签、端口映射、卷挂载路径、健康检查探针、重启策略,你才真正拥有对环境的掌控权。下面我就把这90分钟拆解成可落地的步骤,不讲虚的原理,只说你打开终端后该敲什么、为什么这么敲、以及敲错后怎么救。
2. 内容整体设计与思路拆解:为什么必须用v2.23+、为什么不能直接拉latest
2.1 架构选型:单机部署的黄金三角
我们不碰Kubernetes,不搞Swarm集群,就聚焦最典型的单机场景:一台8GB内存的云服务器(或本地Mac/Windows WSL2),目标是让WordPress能访问、能发文章、能装插件、能上传图片。为此,最小可行架构必须包含三个容器:
- WordPress应用容器:运行PHP-FPM + Apache/Nginx,处理HTTP请求;
- MySQL数据库容器:存储文章、用户、设置等结构化数据;
- phpMyAdmin容器(可选但强烈推荐):图形化管理数据库,排查问题时比命令行快十倍。
这三个容器不是孤立运行的,它们通过Docker内部网络通信。WordPress容器需要知道MySQL容器的主机名(默认是服务名db)、端口(3306)、数据库名(wordpress)、用户名密码——这些信息不能硬编码在wp-config.php里,而应通过环境变量注入。这就是Docker Compose的核心价值:用YAML描述服务间依赖关系与连接参数,让容器自动发现彼此。
提示:不要用
--link参数手动连接容器,这是Docker早期的过时做法。Compose v2+原生支持服务发现,db这个主机名在WordPress容器内直接可解析,无需额外配置。
2.2 版本锁定:为什么latest是生产环境的头号敌人
新手最容易犯的错误,就是docker-compose.yml里写image: wordpress:latest。看起来省事,实则埋雷。WordPress官方镜像每发布一个新版本,latest标签就会指向它,而新版本可能:
- 要求PHP 8.2,但你的主题插件只兼容PHP 7.4;
- 默认禁用
mysql_connect()函数,导致老插件报错; - 修改了
wp-content目录挂载权限逻辑,造成上传失败。
我踩过的最深的坑:某次docker-compose pull后,WordPress自动升级到6.3,结果一个依赖mysqli::real_escape_string()的老备份插件彻底失效,客户三天无法导出数据。后来查日志才发现,新镜像默认启用了MYSQLI_OPT_CONNECT_TIMEOUT超时,而旧插件没做异常捕获。
所以我的硬性规则是:所有镜像标签必须锁定到具体版本号。当前(2024年中)经实测稳定的组合是:
wordpress:6.2.2-php8.1-apachemysql:8.0.33phpmyadmin/phpmyadmin:5.2.1
这三个版本在PHP扩展兼容性、MySQL协议支持、Apache模块加载上完全匹配。你可能会问:为什么不是更新的6.3?因为6.3要求PHP 8.2,而PHP 8.2的mbstring扩展在某些Linux发行版上编译不稳定。6.2.2+PHP 8.1是目前社区验证最充分的“稳态”。
2.3 数据持久化:卷(Volume)不是可选项,是生命线
WordPress的wp-content目录存着所有主题、插件、上传的图片,MySQL的数据文件更是核心资产。如果不用Docker Volume持久化,每次docker-compose down,所有数据就永久消失。但很多人只做了一半:给MySQL配了volume,却忘了WordPress的wp-content。
更隐蔽的陷阱是挂载路径权限。Linux下,WordPress容器以www-data用户(UID 33)运行,而宿主机目录可能属于root(UID 0)。当容器尝试往wp-content/plugins写文件时,会因权限不足失败,表现为“安装插件时提示无写入权限”。
解决方案不是简单chmod 777(极度危险),而是:
- 创建专用系统用户
wpuser(UID 1001),chown -R wpuser:wpuser /path/to/wp-content; - 在docker-compose.yml中指定
user: "1001:1001",让容器以该UID运行; - 或者更稳妥:用named volume,由Docker自动管理权限。
我最终选择后者,因为named volume在不同宿主机(Ubuntu/CentOS/macOS)上行为一致,且避免了手动chown的运维负担。
3. 核心细节解析与实操要点:从零开始的逐行配置指南
3.1 环境准备:三步确认基础就绪
在敲任何docker-compose命令前,请先执行以下三步验证。跳过这一步,90%的问题都源于此。
第一步:确认Docker Engine已安装且版本≥24.0.0
docker --version # 输出应为:Docker version 24.0.7, build afdd53b旧版本(如20.x)对cgroup v2支持不完善,可能导致MySQL容器启动后立即退出。若版本过低,按官方文档升级,不要用apt install docker.io(Ubuntu源里的版本太老)。
第二步:确认Docker Compose Plugin已启用
Docker 23.0+已将Compose作为plugin集成,不再需要单独安装docker-compose二进制。验证命令:
docker compose version # 注意是 'docker compose'(两个词),不是 'docker-compose'(一个词) # 输出应为:Docker Compose version v2.23.0如果提示command not found,说明plugin未安装。在Ubuntu上执行:
sudo apt-get update && sudo apt-get install -y docker-compose-plugin第三步:确认系统时间与时区正确
WordPress严重依赖系统时间生成文章时间戳、验证码有效期。用timedatectl status检查:
System clock synchronized: yesNTP service: activeTime zone: Asia/Shanghai(根据你所在地区调整)
如果时区错误,WordPress后台显示的时间会比实际晚8小时,且cron任务(如定时发布)完全失效。修复命令:
sudo timedatectl set-timezone Asia/Shanghai注意:不要在docker-compose.yml里用
environment: - TZ=Asia/Shanghai去覆盖容器时区。这只能改PHP层时间,MySQL容器仍用UTC,导致NOW()函数返回值与PHPdate()不一致,引发评论时间错乱。必须从宿主机层面统一。
3.2 docker-compose.yml:每一行背后的实战考量
下面是我经过27次迭代后确定的生产级配置。请逐行理解,不要直接复制粘贴:
version: '3.8' services: db: image: mysql:8.0.33 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: rootpass123 MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wppass456 volumes: - db_data:/var/lib/mysql - ./mysql-init:/docker-entrypoint-initdb.d command: --default-authentication-plugin=mysql_native_password healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass123"] timeout: 20s retries: 10 wordpress: image: wordpress:6.2.2-php8.1-apache restart: unless-stopped ports: - "8080:80" environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wppass456 WORDPRESS_CONFIG_EXTRA: | define('WP_DEBUG', false); define('WP_MEMORY_LIMIT', '256M'); if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; } volumes: - wp_content:/var/www/html/wp-content - ./custom-apache.conf:/etc/apache2/sites-available/000-default.conf depends_on: db: condition: service_healthy phpmyadmin: image: phpmyadmin/phpmyadmin:5.2.1 restart: unless-stopped ports: - "8081:80" environment: PMA_HOST: db PMA_PORT: 3306 PMA_ARBITRARY: 1 depends_on: db: condition: service_healthy volumes: db_data: wp_content:关键行深度解析:
version: '3.8':不选最新3.9,因为部分云服务商(如阿里云ECI)对3.9支持不全。3.8是当前最广泛兼容的版本。command: --default-authentication-plugin=mysql_native_password:MySQL 8.0+默认用caching_sha2_password,但WordPress 6.2.2的PHP MySQLi扩展对此支持不稳定,强制切回传统插件,避免“Client does not support authentication protocol”错误。healthcheck:为db服务添加健康检查。depends_on的condition: service_healthy确保WordPress容器只在MySQL真正能响应SQL ping后才启动,避免WordPress因连不上DB而反复崩溃重启。WORDPRESS_CONFIG_EXTRA:这是官方镜像提供的魔法变量,允许你在wp-config.php末尾追加任意PHP代码。这里做了三件事:关闭调试模式(生产环境必须)、提升内存限制(防止大图上传OOM)、适配反向代理(如果你前端有Nginx,需识别X-Forwarded-Proto头来正确生成HTTPS链接)。volumes段定义named volume:db_data和wp_content。它们在宿主机上的真实路径由Docker管理(通常在/var/lib/docker/volumes/下),你无需关心,只需确保磁盘空间充足。
3.3 自定义Apache配置:解决80%的URL重写问题
WordPress固定链接(Permalink)依赖Apache的mod_rewrite模块。官方镜像虽已启用,但默认配置不支持.htaccess覆盖。当你在后台设置固定链接为/post-name/时,会看到404。
解决方案是挂载自定义Apache配置文件custom-apache.conf。内容如下:
<VirtualHost *:80> ServerAdmin webmaster@localhost DocumentRoot /var/www/html <Directory /var/www/html> Options Indexes FollowSymLinks AllowOverride All # 关键!允许.htaccess覆盖 Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost>注意AllowOverride All这一行。没有它,WordPress生成的.htaccess文件(含重写规则)将被完全忽略。挂载后,重启WordPress容器即可生效。
实操心得:不要试图在容器内
a2enmod rewrite,因为官方镜像的Apache是精简版,a2enmod命令不存在。挂载配置是唯一可靠方式。
4. 实操过程与核心环节实现:从启动到可用的完整流水线
4.1 初始化部署:五步完成首次启动
准备好上述文件后,按顺序执行:
步骤1:创建项目目录并进入
mkdir wordpress-site && cd wordpress-site步骤2:创建必需的空目录
mkdir -p mysql-init wp-content # 注意:wp-content目录必须为空,否则Docker会把宿主机目录内容覆盖进容器步骤3:创建自定义Apache配置
cat > custom-apache.conf << 'EOF' <VirtualHost *:80> ServerAdmin webmaster@localhost DocumentRoot /var/www/html <Directory /var/www/html> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost> EOF步骤4:下载并启动
# 下载docker-compose.yml(假设已保存为当前目录) # 然后执行: docker compose up -d-d参数表示后台运行。此时Docker会:
- 拉取三个镜像(约500MB,首次较慢);
- 创建
db_data和wp_content两个named volume; - 启动MySQL容器,执行初始化(创建数据库、用户);
- 等待MySQL健康检查通过(约30秒);
- 启动WordPress容器,连接数据库并生成
wp-config.php; - 启动phpMyAdmin。
步骤5:验证服务状态
docker compose ps # 输出应显示三个服务状态均为"running" # 若有"unhealthy",用 docker compose logs db 查看MySQL错误4.2 首次访问与安装:绕过常见卡点
打开浏览器,访问http://localhost:8080(或你的服务器IP:8080)。如果看到WordPress安装向导,恭喜,基础环境成功。但别急着点“现在就开始”。
卡点1:数据库连接失败
页面提示“Error establishing a database connection”。此时:
- 执行
docker compose logs db | tail -20,看是否有Access denied for user 'wordpress'@'172.20.0.3'; - 如果有,说明环境变量
MYSQL_USER/MYSQL_PASSWORD与WORDPRESS_DB_USER/WORDPRESS_DB_PASSWORD不一致,必须严格相同; - 检查
docker compose config输出,确认两个服务的环境变量值完全匹配。
卡点2:安装向导空白页
页面一片空白,无任何错误。这是PHP致命错误(Fatal Error)的典型表现。原因通常是:
wp-content目录权限问题(宿主机UID与容器UID不匹配);wp-content下存在非法文件(如.DS_Store、Thumbs.db);- 解决方案:
rm -rf wp-content/*清空目录,再docker compose restart wordpress。
卡点3:安装完成后首页404
后台设置固定链接后,文章页404。这是.htaccess未生效。检查:
docker exec -it wordpress-site-wordpress-1 ls -l /var/www/html/.htaccess,确认文件存在;docker exec -it wordpress-site-wordpress-1 cat /etc/apache2/sites-available/000-default.conf,确认AllowOverride All已生效;- 若仍无效,临时进入容器启用重写模块:
docker exec -it wordpress-site-wordpress-1 a2enmod rewrite && apache2ctl restart(仅调试用,非持久化)。
4.3 生产就绪加固:四道安全防线
部署完成不等于安全。以下是必须执行的加固操作:
防线1:修改默认管理员密码
不要用安装向导生成的密码。登录后台→用户→我的个人资料→新密码,生成强密码(12位以上,含大小写字母、数字、符号)。
防线2:禁用文件编辑功能
在wp-config.php中(通过WORDPRESS_CONFIG_EXTRA已注入),添加:
define('DISALLOW_FILE_EDIT', true);防止黑客通过后台“外观→主题编辑器”直接修改PHP文件。
防线3:限制XML-RPC接口
XML-RPC是WordPress的远程调用接口,也是暴力破解常用入口。在custom-apache.conf的<Directory>块内添加:
<Files "xmlrpc.php"> Require all denied </Files>然后docker compose restart wordpress。
防线4:配置MySQL只允许本地连接
在docker-compose.yml的db服务中,删除ports:字段(即不暴露3306端口到宿主机)。这样MySQL只能被同网络的WordPress和phpMyAdmin访问,外部无法直连。
注意:phpMyAdmin仍可通过
PMA_HOST: db正常工作,因为它与MySQL在同一Docker网络内。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 问题速查表:症状、原因、解决命令
| 症状 | 可能原因 | 快速诊断命令 | 解决方案 |
|---|---|---|---|
docker compose up后MySQL容器立即退出 | MySQL初始化脚本语法错误 | docker compose logs db | head -50 | 检查./mysql-init/*.sql文件,确保以;结尾,无BOM头 |
| WordPress后台上传图片失败,提示“无法创建目录” | wp-content挂载目录权限不足 | ls -ld wp-content | sudo chown -R 1001:1001 wp-content,然后docker compose restart wordpress |
访问http://ip:8081(phpMyAdmin)提示“无法连接到MySQL服务器” | phpMyAdmin配置的PMA_HOST与MySQL服务名不一致 | docker compose config | grep -A5 phpmyadmin | 确认PMA_HOST: db,且db是services下的服务名 |
| 固定链接开启后,除首页外全部404 | Apache未加载mod_rewrite或AllowOverride未启用 | docker exec -it [wordpress-container-id] apache2ctl -M | grep rewrite | 确认输出含rewrite_module (shared);检查custom-apache.conf中AllowOverride All位置是否正确 |
docker compose down后重新up,WordPress提示“另一份安装正在进行中” | MySQL数据卷残留wp_options表中的core_updater.lock | docker exec -it [db-container-id] mysql -uwordpress -pwppass456 wordpress -e "DELETE FROM wp_options WHERE option_name='core_updater.lock';" | 手动清理锁记录 |
5.2 独家避坑技巧:来自27次重装的经验
技巧1:用docker compose down -v彻底清理,但慎用-v参数会删除所有named volume(包括db_data和wp_content),数据永久丢失。我只在两种情况用:
- 开发环境调试配置,想100%还原初始状态;
- 确认数据已通过phpMyAdmin导出备份。
日常重启,永远用docker compose restart或docker compose up -d --force-recreate。
技巧2:备份不是可选项,是每日必做动作
WordPress的备份分两层:
- 数据库层:
docker exec [db-container-id] mysqldump -uwordpress -pwppass456 wordpress > backup.sql - 文件层:
tar -czf wp-content-backup.tar.gz wp-content/
我写了一个backup.sh脚本,每天凌晨2点自动执行,并保留最近7天备份。脚本核心逻辑:
DATE=$(date +%Y%m%d) docker exec wordpress-site-db-1 mysqldump -uwordpress -pwppass456 wordpress > /backups/db-$DATE.sql tar -czf /backups/wp-content-$DATE.tar.gz -C . wp-content find /backups -name "db-*.sql" -mtime +7 -delete技巧3:升级WordPress不是docker-compose pull就完事
升级流程必须是:
- 备份数据库和
wp-content; - 修改
docker-compose.yml中wordpress镜像标签(如6.2.2-php8.1-apache→6.3.0-php8.2-apache); docker compose pull wordpress拉取新镜像;docker compose up -d wordpress仅重启WordPress容器;- 访问
/wp-admin/upgrade.php执行数据库升级; - 逐一测试主题、插件兼容性。
跳过第4步直接docker compose up -d,会导致MySQL容器也重启,可能中断正在运行的查询。
技巧4:调试PHP错误,别只看WordPress后台
当页面空白时,真正的错误日志在Apache错误日志里:
docker compose logs wordpress \| grep "PHP Fatal" # 或直接查看日志文件 docker exec -it wordpress-site-wordpress-1 tail -50 /var/log/apache2/error.log常见错误如PHP Fatal error: Uncaught Error: Call to undefined function mb_convert_encoding(),说明缺少mbstring扩展——这正是我坚持用6.2.2-php8.1-apache的原因,该镜像已预装所有必需扩展。
5.3 性能优化:让小服务器跑得更稳
一台2核4GB的云服务器,同时跑WordPress+MySQL+phpMyAdmin,内存压力不小。三个关键优化点:
优化点1:限制MySQL内存
在docker-compose.yml的db服务中,添加mem_limit和mem_reservation:
deploy: resources: limits: memory: 1g reservations: memory: 512m并修改command为:
command: --default-authentication-plugin=mysql_native_password --innodb_buffer_pool_size=256Minnodb_buffer_pool_size设为物理内存的25%,避免MySQL吃光所有内存导致OOM Killer干掉进程。
优化点2:启用OPcache
PHP 8.1默认启用OPcache,但需确认配置。在WORDPRESS_CONFIG_EXTRA中追加:
if (function_exists('opcache_get_status')) { $status = opcache_get_status(); if ($status && $status['opcache_enabled']) { // OPcache已启用 } }若未启用,在custom-apache.conf同级目录创建php.ini:
opcache.enable=1 opcache.memory_consumption=128 opcache.max_accelerated_files=4000 opcache.revalidate_freq=60然后挂载:- ./php.ini:/usr/local/etc/php/conf.d/opcache.ini
优化点3:静态资源CDN化(低成本方案)
不买CDN,用Cloudflare免费版。在Cloudflare后台添加你的域名,将DNS记录指向服务器IP,开启Proxy(橙色云朵)。Cloudflare会自动缓存/wp-content/uploads/下的图片,减轻服务器带宽压力。WordPress侧只需在wp-config.php中添加:
define('WP_CONTENT_URL', 'https://yourdomain.com/wp-content');确保生成的图片链接走CDN。
6. 后续演进:从博客到企业级应用的平滑路径
这个Docker Compose方案不是终点,而是起点。当你的需求增长,可以无缝演进:
演进路径1:增加Redis缓存
当并发超过50,首页加载变慢。只需在docker-compose.yml中添加redis服务:
redis: image: redis:7.2-alpine restart: unless-stopped command: redis-server --save 60 1 --loglevel warning volumes: - redis_data:/data然后在WordPress中安装Redis Object Cache插件,配置REDIS_HOST: redis,缓存命中率立刻提升至95%以上。
演进路径2:接入HTTPS
用certbot自动签发证书。在宿主机安装certbot,运行:
certbot certonly --standalone -d yourdomain.com然后修改docker-compose.yml,将证书挂载进WordPress容器,并在custom-apache.conf中配置SSL虚拟主机。整个过程无需停机。
演进路径3:多环境分离
将docker-compose.yml拆分为:
docker-compose.base.yml(基础服务)docker-compose.dev.yml(开发环境,挂载源码、开启Xdebug)docker-compose.prod.yml(生产环境,启用OPcache、禁用调试)
用docker compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d一键切换。
我自己维护的一个客户项目,就是从这个单机Compose起步,两年内逐步加入了Elasticsearch全文搜索、MinIO对象存储替代本地上传、GitLab CI自动部署——所有扩展都基于同一套YAML规范,没有一次推倒重来。
最后分享一个小技巧:每次修改docker-compose.yml后,先用docker compose config验证语法。它会输出最终解析的配置,如果看到ERROR或WARN,说明YAML格式有误(如缩进不对、冒号后少空格),比等up失败后再排查快得多。这个命令,我每天至少用五次。