在 CentOS 7 上装 Docker,再做一套 WordPress 站点,这套组合我前前后后在不同机器上重复过不下十几次。它看起来是个“入门级”的活儿,但真到生产环境里跑起来,前面那些没被注意到的细节——内核版本、SELinux、数据目录、字符集、反代后的混合内容——每一个都能让人多耗一下午。所以这篇内容想解决的,就是把 CentOS 7 上 Docker 安装与 WordPress 部署这条链路完整梳理清楚:该装哪些包、仓库怎么配、daemon.json 里哪些参数不改迟早出事、编排文件怎么写、数据怎么备份、出问题去哪儿看日志。全文偏向实操,适合手上正好有台 CentOS 7 老机器、想快速把站点跑起来的读者,也适合已经跑起来但总觉得哪里不稳、想回头补课的人。
1. 这套组合到底解决什么问题
1.1 传统 LAMP 部署的三个老毛病
不借助容器直接在一台机器上装 Apache、PHP、MariaDB,最麻烦的地方不在装的时候,而在后面每一次“动一下”。第一个毛病是版本锁死。CentOS 7 自带的 PHP 版本偏低,想用新版 WordPress,得额外挂第三方源,挂完还要处理依赖冲突,处理不好连 yum 的依赖关系都乱掉。第二个毛病是环境不可复现。这台机器上装的扩展、改过的 php.ini、手动编译的 GD 库,换一台机器根本对不上,出问题时只能靠回忆。第三个毛病是迁移成本。业务跑了一年想换机器,代码好拷,数据库好导,但服务和配置要重新捋一遍,中间任何一处不一样,表现就是白屏或 500。
容器化之后,这三个问题被压缩成了“镜像 + 编排文件”。PHP 版本、扩展、Apache 配置全部写死在镜像里,迁移的时候只要带走数据目录和 compose 文件,理论上即可原地复活。这个“理论上”是关键,能不能真的复活,取决于数据持久化做得够不够干净,这一点后面第 5 章会展开。
1.2 为什么还是绕不开 CentOS 7
说句实在的,CentOS 7 的官方维护周期已经结束,这一点在做长期项目时必须心里有数,尤其是软件源可用性和安全补丁这两块。但现实是,大量存量服务器还在跑它,迁移不是一句话的事。所以更务实的做法是:在既能维护又不激进的前提下,把服务跑稳,并且把“将来要迁”这件事提前设计进去。
好在 CentOS 7 的内核版本是 3.10 系列,最终的 3.10.0-1160 这个版本,对 Docker 的 overlay2 存储驱动支持是完整的。overlay2 要求内核 3.10.0-514 及以上,CentOS 7.4 之后默认就满足了。所以性能上不至于退而求其次去用 devicemapper,这一点可以放心。
需要提醒的是,较新的 docker-ce 版本正在逐步收窄对 CentOS 7 的支持。生产环境上不要无脑yum install docker-ce装最新,先去看一眼可用版本列表,锁一个社区里验证过的版本,比如 20.10 或者 24.0 系列,把版本号写进部署文档里。装完能跑不等于下次重装还能跑。
1.3 部署形态的取舍
WordPress 的容器化有三种常见形态:一是全手工docker run,二是 docker-compose 编排,三是各种一键脚本。
第一种适合临时玩玩,两条 run 命令就能起来,但一旦涉及网络别名、依赖顺序、健康检查,命令会膨胀得没法维护。第三种看着省事,实际是把别人的黑盒引进来,出问题连日志在哪都不知道。我一般选第二种,理由很朴素:编排文件本身就是文档,任何一个同事拿到这个文件都能看懂服务之间怎么连的,端口怎么映射的,环境变量有哪些。这一点在交接场景里价值极高。
另外还有一个容易忽略的决策:数据库放容器里还是放宿主机。我的判断标准是——如果这台机器只跑这一个站点,数据库放容器里完全没问题,数据和容器解耦靠卷来做;如果宿主机上已经有别的服务共用 MySQL,那就别塞进来了,用host网络模式或者外部连接串指过去,避免两套数据库互相打架。
2. 系统层面的准备
2.1 版本体检:三个命令定基调
动手之前先做体检,这三个命令的输出决定了后面怎么走。
cat /etc/redhat-release uname -r df -h /var/lib/docker 2>/dev/null || df -h /第一条看系统小版本,CentOS Linux release 7.9.2009是最常见的。第二条看内核,只要大于3.10.0-514就满足 overlay2 的条件;如果低于这个值,说明系统很老,得先yum update kernel升级并重启,否则 Docker 起来会退回 vfs 驱动,性能和磁盘占用都很难看。第三条看磁盘,/var/lib/docker所在分区至少留 20G 起步,镜像层、容器日志、数据库文件全在这里。
注意:CentOS 7 的默认分区方案里根分区往往不大,如果
df显示根分区只剩几 G,先别急着装 Docker,把数据目录规划到有空间的盘上,具体做法在 3.5 节。
2.2 SELinux 与防火墙的处理逻辑
SELinux 在 CentOS 7 上默认是 enforcing 状态,它对容器的 bind mount 会做标签校验。表现是:容器日志里报Permission denied,但你在宿主机上用ls -l看权限明明是 777。这种情况十有八九是 SELinux 上下文的问题。
处理有两条路。稳妥一点的是保留 SELinux,给挂载目录加:z或者:Z后缀,让 Docker 自动打标签,- ./data/html:/var/www/html:z这样写。省事一点的是把 SELinux 关掉。我一般的处理是:内部测试机直接关,生产机保留但把卷标签写对。关闭的命令是:
setenforce 0 sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config改成 permissive 而不是 disabled,好处是不用重启,而且真有策略问题时日志里还能看到 AVC 记录,方便排查。改完getenforce输出Permissive就生效了。
防火墙这块,CentOS 7 默认用 firewalld。WordPress 最终要通过 80/443 对外,如果前面有 Nginx 做反代,那 Docker 映射的端口只绑在 127.0.0.1 上就够了,防火墙只需要放行 80 和 443。
firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload2.3 软件源与时间同步
CentOS 7 维护周期结束后,官方源的可达性会出问题,yum makecache卡住是常态。换一个国内公共镜像站是必要操作,把/etc/yum.repos.d/CentOS-Base.repo里的mirrorlist注释掉,baseurl指向镜像站地址即可。同理,Docker 的软件源如果直接用官方地址慢得离谱,也可以在docker-ce.repo里把baseurl换掉,或者用yum-config-manager的加速配置。
时间同步这块别省。容器里的证书校验、WordPress 的定时任务、日志时间戳都依赖系统时间。CentOS 7 用 chronyd:
systemctl enable --now chronyd chronyc sources -v timedatectl set-timezone Asia/Shanghaichronyc sources -v里看到^*开头的一行才算同步成功。时间不对导致 Let's Encrypt 申请证书失败,我都遇到过两次,每次都以为是网络问题,实际是机器走了几十秒。
3. Docker 引擎安装实操
3.1 先卸干净旧版本再动手
CentOS 7 的 extras 源里有个叫docker的包,是早期版本,跟官方 docker-ce 不是一回事,共存会出各种诡异问题。所以第一件事是清理:
yum remove -y docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine顺便把残留目录也删掉,避免新装的时候读到旧的元数据:
rm -rf /var/lib/docker这一步要确认目录里没有还需要的数据,删了就没了。如果这台机器之前跑过容器,先docker ps -a看一眼再动手。
3.2 仓库配置与版本锁定
装依赖,加仓库,这两步顺序不能反:
yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repodevice-mapper-persistent-data和lvm2这两个包是给 devicemapper 驱动准备的,虽然我们最终用 overlay2,但装上是官方推荐的通用做法,避免某些场景下引擎启动失败。
接着看可用版本,这一步很重要:
yum list docker-ce --showduplicates | sort -r输出会按版本倒序列出来。挑一个社区验证过、和 CentOS 7 兼容稳定的版本,用版本号-3.el7的格式指定安装:
yum install -y docker-ce-20.10.24-3.el7 \ docker-ce-cli-20.10.24-3.el7 \ containerd.io-1.6.20-3.5.el7把三个组件的版本显式写出来,而不是只写包名,是为了避免 yum 自己挑最新版导致组件版本错配。containerd.io 和 docker-ce 之间的版本对应关系,去官方 release notes 对一下最稳。
如果安装时报Requires: container-selinux >= 2.9,说明本地源里没有这个包,单独装一下yum install -y container-selinux再重试。
3.3 启动、自检与非 root 用户
systemctl enable --now docker systemctl status docker docker version docker infodocker version能同时看到 Client 和 Server 两段输出,说明引擎和 CLI 通信正常。docker info重点看三处:Storage Driver是不是overlay2,Cgroup Driver是cgroupfs还是systemd,Docker Root Dir是不是你期望的目录。这三项和后面的 daemon.json 配置强相关。
日常操作不希望每条命令都带 sudo,可以把用户加进 docker 组:
usermod -aG docker youruser注意:docker 组的权限等价于 root,组内用户可以通过挂载宿主机目录的方式拿到任意文件读写权限。多人共用的机器上,这一步要不要做,取决于你对这台机器上其他账号的信任程度。
3.4 daemon.json:四个必须动的参数
/etc/docker/daemon.json是引擎的全局配置,不改的话默认值在生产上有几个明显短板。我的常用配置:
{ "data-root": "/data/docker", "storage-driver": "overlay2", "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }, "live-restore": true, "default-ulimits": { "nofile": { "Name": "nofile", "Hard": 65535, "Soft": 65535 } }, "registry-mirrors": ["https://<你的镜像加速地址>"] }逐个说为什么。
>systemctl daemon-reload systemctl restart docker docker info | grep -E "Storage Driver|Docker Root Dir"
配置语法错的话,docker 服务会直接起不来,journalctl -u docker -n 50能看到具体报错位置。JSON 里多一个逗号、少一个引号都会挂,改完最好用python -m json.tool /etc/docker/daemon.json验一下格式。
3.5 数据目录迁移的两种时机
如果>yum install -y python3 python3-pip pip3 install --upgrade pip pip3 install docker-compose==1.29.2
1.29.2 是 1.x 系列的最后一个版本,对 Python 3.6 兼容良好,docker-compose.yml的version: "3.8"语法也完全支持。生产环境中我倾向使用 1.29.2,原因不是它更好,而是它在此环境中运行更稳定。
装完顺手验证:
docker-compose --version4.2 compose 文件逐行拆解
目录结构我习惯这样组织,把数据和配置都放在项目目录下,跟容器生命周期解耦:
/data/wp/ ├── docker-compose.yml ├── php.ini └── data/ ├── mysql/ └── html/docker-compose.yml内容:
version: "3.8" services: db: image: mariadb:10.6 restart: always command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --innodb-buffer-pool-size=512M - --max-connections=200 environment: MARIADB_ROOT_PASSWORD: ChangeThisRootPwd MARIADB_DATABASE: wordpress MARIADB_USER: wpuser MARIADB_PASSWORD: ChangeThisWpPwd volumes: - ./data/mysql:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 6 start_period: 30s wordpress: image: wordpress:6.4-php8.2-apache restart: always depends_on: db: condition: service_healthy ports: - "127.0.0.1:8080:80" environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_NAME: wordpress WORDPRESS_DB_USER: wpuser WORDPRESS_DB_PASSWORD: ChangeThisWpPwd WORDPRESS_CONFIG_EXTRA: | define('WP_MEMORY_LIMIT', '256M'); define('WP_MAX_MEMORY_LIMIT', '512M'); define('DISALLOW_FILE_EDIT', true); define('AUTOMATIC_UPDATER_DISABLED', true); volumes: - ./data/html:/var/www/html - ./php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro几个关键点值得单独说。
数据库选择。MySQL 8 默认的caching_sha2_password认证方式,在部分旧客户端上会有兼容问题。WordPress 6.x 配上 PHP 8.2 的 mysqli 扩展其实已经没问题了,但为了少一个变量,我一般直接用 MariaDB 10.6,行为稳定,社区资料也多。
字符集。utf8mb4和utf8mb4_unicode_ci这两条一定要加。默认的 latin1 会导致中文标题、emoji 表情、特殊符号在写入时报错或乱码,而且这个问题是事后才能发现的——建库的时候就定了,后面改要重建表,非常麻烦。
健康检查。depends_on只控制启动顺序,不控制就绪状态。数据库容器启动后还要初始化数据目录、跑完 innodb 恢复,这段时间大概 20 到 60 秒,WordPress 如果这时候连上来就会失败退出。加了condition: service_healthy之后,Compose 会等健康检查通过才启 WordPress,避免这个经典坑。
端口绑定。127.0.0.1:8080:80前面的 IP 不能省。如果写成8080:80,Docker 会直接改 iptables,绕过 firewalld 规则,端口对整个网络暴露。前面有 Nginx 反代的时候,只绑本地就够了。
php.ini。挂一个自定义的 ini 文件进去,放常用的几个配置:
memory_limit = 256M upload_max_filesize = 64M post_max_size = 64M max_execution_time = 300 max_input_vars = 5000max_input_vars这一条特别容易被忽略。WordPress 装插件、改主题、导入菜单的时候,表单字段一多,默认的 1000 就不够,表现是保存后只有一部分生效,还不报错,排查起来非常费劲。
4.3 启动、初始化与首次访问
cd /data/wp docker-compose up -d docker-compose ps docker-compose logs -f wordpressdocker-compose ps里两个容器的状态都应该是Up,db 的备注列会显示(healthy)。
第一次访问http://127.0.0.1:8080,会进入 WordPress 的安装向导,选语言、填站点名、管理员账号密码。提交之后,容器里的 entrypoint 脚本会自动用环境变量生成wp-config.php,写进/var/www/html,也就是宿主机的./data/html目录。
这里有个细节要留意:wp-config.php一旦生成,后续修改环境变量就不再生效了。想改数据库密码,得直接改这个文件,或者删掉它重新生成。知道这一点能省掉不少困惑。
装完之后,建议做两件事。
第一,调整目录权限。容器内 Apache 以www-data运行,UID 是 33。如果宿主机上的目录 owner 不是 33,上传插件、更新主题、写入缓存都会失败:
chown -R 33:33 /data/wp/data/html/wp-content find /data/wp/data/html -type d -exec chmod 755 {} \; find /data/wp/data/html -type f -exec chmod 644 {} \;第二,把站点地址写对。如果直接用http://127.0.0.1:8080装的,后面换域名会发现后台进不去、图片全挂。这时候要改数据库:
docker-compose exec db mysql -uwpuser -p wordpress -e " UPDATE wp_options SET option_value='https://your-domain.com' WHERE option_name IN ('siteurl','home'); "数据库里还有大量序列化过的旧地址,比如文章正文里的图片链接。这类要用wp search-replace工具处理,直接 SQL 替换会破坏序列化长度,导致数据损坏。
4.4 反向代理与 HTTPS 收尾
前面挂一个宿主机上的 Nginx 做反代,端口映射只留本地,好处是证书管理、缓存、限流都在 Nginx 层做,容器本身不用关心。
server { listen 80; server_name your-domain.com; client_max_body_size 64m; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; 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; proxy_read_timeout 300s; proxy_connect_timeout 60s; } }client_max_body_size和前面 php.ini 的upload_max_filesize、post_max_size三个值要一致,或者 Nginx 这边略大一点。只改一个的话,上传大文件还是会被拦,只不过拦截的位置不同,报错信息也不一样。
TLS 终止在 Nginx 上,证书用 certbot 自动续期。反代之后有个经典问题:WordPress 认为自己是 http,页面里的资源全用 http 加载,浏览器在 https 页面里就会拦掉,控制台一堆 mixed content。解决方式是在wp-config.php里加一段:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; }官方镜像的 Apache 配置里其实已经处理了X-Forwarded-Proto,多数情况下不需要手动加。但如果你前面套了两层代理,或者用了 CDN,这段兜底代码就很有必要。
4.5 上传与性能的几个参数
站点跑起来之后,几个参数调一下体验会明显不同。
php.ini里的max_execution_time从默认 30 提到 300,解决后台装大插件超时。WP_MEMORY_LIMIT提到 256M,解决编辑长文章时白屏。innodb-buffer-pool-size按机器内存的 25% 左右给,2G 内存的机器给 512M 比较合适,再大反而会因为内存不足导致 OOM。
静态资源这块,如果访问量上来了,在 Nginx 里给wp-content/uploads和wp-includes加缓存头,能省掉不少请求打到容器上:
location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ { proxy_pass http://127.0.0.1:8080; expires 30d; add_header Cache-Control "public, immutable"; }注意这个 location 要放在/之前,Nginx 的正则匹配优先级高于前缀匹配。
5. 数据、备份与迁移
5.1 卷的规划原则
容器化部署里,数据卷设计错了,后面的备份和迁移全部会受影响。我的原则是三条。
第一,能用 bind mount 就不用 named volume。named volume 藏在/var/lib/docker/volumes下面,路径长、名字随机,找起来费劲,而且迁移的时候要先导出成 tar。bind mount 直接对应宿主机目录,rsync一同步就完事。
第二,数据库和站点文件分开挂。数据库目录挂了./data/mysql,站点目录挂了./data/html。这样备份策略可以不一样:数据库需要逻辑导出(mysqldump),站点文件直接打包。
第三,挂载点粒度不要太细。有人喜欢把wp-content/plugins、wp-content/themes、wp-content/uploads分别挂三个目录,看着清晰,实际维护起来很痛苦——升级主题要改宿主机的权限,装插件要处理目录不存在的情况。整体挂./data/html就够了。
5.2 备份脚本与验证
备份这件事,脚本写起来不难,难在“备份完了没验证过”。下面这个脚本每周跑一次,放在 cron 里:
#!/bin/bash set -euo pipefail BACKUP_DIR=/data/backup DATE=$(date +%F_%H%M) KEEP_DAYS=14 APP_DIR=/data/wp mkdir -p "$BACKUP_DIR" cd "$APP_DIR" docker-compose exec -T db sh -c \ 'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" \ --single-transaction --quick --routines --triggers \ --databases wordpress' \ | gzip > "$BACKUP_DIR/db_$DATE.sql.gz" tar -czf "$BACKUP_DIR/html_$DATE.tar.gz" \ --exclude='./data/html/wp-content/cache' \ -C "$APP_DIR/data" html find "$BACKUP_DIR" -type f -mtime +$KEEP_DAYS -delete echo "backup done: $DATE"几个参数的意义:--single-transaction让 InnoDB 表在备份时保持一致性快照,不锁表,站点可以继续访问;--quick避免把大表一次性读进内存;--routines --triggers把存储过程和触发器一起导出来,虽然 WordPress 本身不用,但装的插件可能会用。
-T这个参数不能少,它禁用 TTY 分配,否则在 cron 这种没有终端的环境里,docker-compose exec会报the input device is not a TTY。
验证备份有效性的方法,我固定用一个:每季度挑一个备份文件,在测试机上完整恢复一次,跑起来能访问、能登录后台、能发文章,才算合格。没验证过的备份等于没有备份。
5.3 迁移到新机器的完整流程
迁移这件事,只要数据卷规划对了,流程很清晰。
老机器上先停服务,保证数据不再变动:
cd /data/wp docker-compose stop然后打包数据库导出和站点目录,一起传到新机器:
docker-compose start docker-compose exec -T db sh -c 'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction --databases wordpress' | gzip > /tmp/wp_db.sql.gz tar -czf /tmp/wp_html.tar.gz -C /data/wp/data html scp /tmp/wp_db.sql.gz /tmp/wp_html.tar.gz new-host:/tmp/新机器上把 compose 文件和 php.ini 复制过去,数据目录铺好:
mkdir -p /data/wp/data tar -xzf /tmp/wp_html.tar.gz -C /data/wp/data cd /data/wp docker-compose up -d db sleep 30 zcat /tmp/wp_db.sql.gz | docker-compose exec -T db sh -c 'exec mysql -uroot -p"$MARIADB_ROOT_PASSWORD"' docker-compose up -d导入数据库的时候要注意,原备份里可能带CREATE DATABASE语句,如果目标库已经存在会报错。可以在导入前先 drop,或者备份的时候不加--databases参数,只导表。
迁移完第一件事是改站点地址,第二件事是清缓存,第三件事是访问一遍首页、后台、文章详情、图片,确认没有 404 和 500。域名没变的话,还要记得把 DNS 切过来,切之前用 hosts 文件在本地验证一遍,别等 DNS 生效了才发现站点起不来。
6. 常见问题排查速查
6.1 高频故障对照表
| 现象 | 大概率原因 | 排查入口 |
|---|---|---|
| 容器起来几秒就退出 | 数据库连不上 / 端口冲突 | docker-compose logs <服务名> |
| 首页 502 Bad Gateway | 容器没起 / 端口映射错 | docker-compose ps看状态 |
| 上传图片提示无法创建目录 | 宿主机目录 owner 不是 33 | ls -ln data/html/wp-content |
| 后台保存设置只生效一半 | max_input_vars太小 | 改 php.ini 后重启容器 |
| 页面样式全丢 | 站点地址还是旧 IP | 查wp_options的 siteurl |
| 数据库容器反复重启 | 数据目录权限或磁盘满 | docker-compose logs db |
| 装 Docker 报 container-selinux 缺失 | 源里没这个包 | 单独yum install container-selinux |
docker-compose命令找不到 | PATH 或安装失败 | which docker-compose |
| 磁盘占满 | 日志没限制 / 镜像堆积 | docker system df |
| HTTPS 证书申请失败 | 系统时间不对 | timedatectl/chronyc sources |
6.2 几个排查思路的展开
容器启动即退出。这是最常见的情况,九成是数据库连接问题。docker-compose logs wordpress里会看到MySQL Connection Error之类的字样。先确认 db 容器是不是 healthy,再确认WORDPRESS_DB_HOST写的是db:3306而不是localhost:3306。容器里的 localhost 指的是容器自己,不是宿主机,这是新手最容易踩的一个点。
磁盘突然满了。用docker system df看三块占用:Images、Containers、Local Volumes。Images 大是镜像层堆积,docker image prune -a能清掉没被引用的;Containers 大是日志,说明 daemon.json 里的 log-opts 没生效,docker inspect <容器名> | grep -A5 LogConfig能确认;Local Volumes 大说明有孤儿卷,docker volume prune。清理前一定确认没有需要保留的数据。
站点迁移后所有链接都指向旧地址。除了改wp_options里的两项,还有wp_posts表里的正文内容、wp_postmeta里的配置数据、以及各种插件自己的表。这些都是序列化数据,直接UPDATE ... REPLACE()会破坏长度前缀,导致反序列化失败。正确做法是用 WP-CLI:
docker-compose exec wordpress wp search-replace 'http://old-domain.com' 'https://new-domain.com' --all-tables --precise --allow-root--precise参数会正确处理序列化数据,--all-tables覆盖插件建的表。执行前先备份,这个命令改错了回滚很麻烦。
性能慢但 CPU 和内存都不高。去看 Nginx 的access_log,重点看$request_time。如果某个请求特别慢,可能是某个插件在跑定时任务,或者数据库缺索引。docker-compose exec db mysql -e "SHOW FULL PROCESSLIST"能看到当前在执行的 SQL,卡住的语句一目了然。
6.3 两个不够寻常但很有用的技巧
第一个,用docker-compose config验证编排文件。改完 yml 不确定有没有语法问题,跑这条命令,它会展开所有变量、合并继承配置、输出最终结果,有问题直接报错位置。比up报错之后再去猜快得多。
第二个,容器里的时区。默认镜像是 UTC,WordPress 后台显示的发布时间、定时任务的执行时间都会差 8 小时。在 compose 里加一行TZ=Asia/Shanghai环境变量,或者在 php.ini 里设date.timezone = Asia/Shanghai,两边都设最保险。
7. 几条实战心得
版本号这事,我现在的习惯是所有镜像都写完整 tag,wordpress:6.4-php8.2-apache而不是wordpress:latest。latest意味着某天你重启一下容器,PHP 版本悄悄变了,某个扩展没了,站点直接白屏。写死版本,升级变成一个主动动作,可控。
宿主机保持干净这一点,说起来简单做起来难。很多人的做法是先装个宝塔或者 LNMP 一键包,再在上面套 Docker,结果端口冲突、PHP 版本混乱、日志分散在三个地方。我的建议是宿主机上除了 Docker 和 Nginx(如果你要在宿主机做反代),别的都别装。数据库在容器里,PHP 在容器里,出了问题docker-compose down一删,环境就是干净的。
数据目录放在/data而不是/var/lib/docker下面,这个习惯是从一次磁盘满的事故里养成的。当时根分区 40G 满了,Docker 起不来,容器全停,数据在/var/lib/docker/volumes里,想挪都挪不动,因为没空间做临时拷贝。后来所有项目的数据统一放独立挂载的/data,根分区只放系统,容量焦虑少了一大半。
最后一点,先跑通最小可用版本,再考虑优化。我见过不少人一上来就研究 Redis 对象缓存、CDN、页面静态化、数据库读写分离,结果基础站点还没装完。正确的顺序是:容器能起来,站点能访问,后台能登录,数据能备份恢复。这四件事做到了,再往上叠加才是有意义的。反过来的话,每一步都会变成新的问题来源。
WordPress 的插件生态很丰富,但也意味着质量参差。装插件之前先看更新时间、兼容版本、活跃安装量这三个指标,两年没更新的插件基本可以跳过。后台那个“自动更新”开关,我一般关掉,尤其是核心大版本更新,先在自己的测试环境跑一遍,确认主题和插件都没问题再动生产环境。一次大版本更新导致站点挂掉,恢复起来要花的时间,远比提前测试多得多。
这个环境后续还能往几个方向扩展,比如加一个独立的 Redis 容器做对象缓存,把wp-content/cache从磁盘挪到内存;或者用 Nginx 的fastcgi_cache做整页缓存,对流量型站点效果比插件级缓存明显得多。不过这些都要在基础环境稳定运行一段时间之后再考虑,否则问题排查时很难判断是新加组件的问题还是原有配置的问题。