CentOS 7 上安装 Docker再用容器把 WordPress 跑起来——这套组合我前后折腾过十几遍从 1 核 1G 的入门机型到 8 核 16G 的内网测试机都试过。它解决的核心问题其实就一个把 PHP、MySQL、Nginx 这几套互相牵扯的环境从“装错一个版本就全盘重来”的泥潭里拽出来变成几条命令就能重建的标准化流程。标题里“docker 的安装”和“wordpress 的部署”看着是两件事实际上前者决定了后者能不能跑得住仓库源配错、存储驱动回退、日志不限制大小这些安装阶段埋下的雷最后都会在 WordPress 访问变慢、磁盘写满、容器反复重启的时候炸出来。这篇内容适合三类人看手上还有 CentOS 7 存量机器要用的运维、想给自己搭个独立博客但被 LNMP 编译劝退的新手、以及已经用上容器但总觉得“能跑就行”想再抠一抠细节的开发者。下面我按真实操作顺序拆开讲中间穿插我踩过的坑和对应的判断依据命令和配置都可以直接抄。1. 这套组合到底适合什么场景1.1 存量机器续命是最常见的出发点CentOS 7 在 2024 年 6 月 30 日已经走完了官方维护周期官方仓库的软件包不再更新默认的 yum 源地址在很多网络环境下也拉不动了。但现实是大量内网机器、测试环境、老业务服务器还跑着它重装系统意味着业务中断和一堆兼容性验证。这种情况下用 Docker 把应用层“封装”起来让宿主机只承担一个内核 容器的角色是性价比最高的选择宿主机的老没关系容器里跑的永远是你要的那个版本。我在一个内部知识库项目里就是这种情形——机器是 2018 年采购的系统装好就没动过PHP 版本还停在 5.4。硬升 PHP 会牵连一堆老扩展最后是用容器把整站迁到 PHP 8.2宿主机一个包都没装连 PHP 都不需要存在。WordPress 这种“一坨 PHP 文件 一个数据库”的应用天然适合这种拆法。另一个典型场景是个人博客和小团队官网。这类站点的特征很一致流量不大、预算有限、没有专职运维、但要求“挂了能自己修”。容器化的好处不是性能而是可复现——你把 docker-compose.yml 存进 Git换台机器 clone 下来两条命令就能还原出一模一样的环境。我见过太多人把 WordPress 装在某台机器上两年后想迁移发现连当初装的 PHP 是什么版本都记不清了。1.2 三种部署路线的取舍动手之前先明确一点WordPress 的部署方式不止一种选错了后面全是返工。我把常见的三条路线列出来做个对照。路线上手难度环境隔离迁移成本适合谁手工编译 LNMP高无高需逐项记录需要极致调优、能接受维护成本面板类工具一键部署低无中依赖工具导出完全不想碰命令行的人Docker Compose 编排中强低配置文件即环境想长期维护、需要多环境一致手工编译的问题不在于难而在于“不可复制”。同一份教程在不同机器上编译出来的 PHP 扩展列表可能就不一样出了问题只能靠记忆回溯。面板类工具把复杂度藏起来了短期很爽但一旦面板本身出问题或者你想做点它没覆盖的定制就会卡死在黑盒里。Compose 编排的好处是把“环境”变成了一个文本文件。数据库用什么版本、PHP 开哪些扩展、Nginx 的配置长什么样全部白纸黑字写着。代价是你得理解容器之间怎么通信、数据往哪儿存——这也正是下面要重点讲的。1.3 一个必须提前说清的预期管理用 Docker 跑 WordPress很多人第一反应是“性能会不会打折”。实测下来在同样的 2 核 4G 配置上容器化的 WordPress 和宿主机直装的差距在 5% 以内主要开销来自网络栈的一次转发对内容型站点完全可以忽略。真正影响性能的是三件事MySQL 的缓冲池给够没有、PHP 的 OPcache 开没开、静态资源有没有让 Nginx 直接吐出去。这三件事在后面第 5 节会具体给参数。反过来容器化带来的一个真实成本是磁盘占用。镜像层、容器日志、数据库文件、上传的媒体文件四个地方都在涨。默认配置下 Docker 的 json-file 日志是没有大小上限的一个被爬虫反复打的站日志能把磁盘写满——这是我在真实环境里遇到过的第一号事故后面第 3 节会讲怎么在 daemon 层面直接掐死。2. 动手之前的环境准备与检查2.1 硬件规格与内存分配的计算WordPress 官方给的推荐配置是 PHP 256M 内存、MySQL 5.6但这个数字放到容器环境里不够用因为你要同时跑三个容器。我按实际跑下来的经验给个对照表配置能不能跑实际体感1 核 1G勉强MySQL 8 默认配置容易 OOM必须调小缓冲池后台操作明显卡1 核 2G能加 1G swap 后可用适合个人博客日 PV 千级没问题2 核 4G推荐后台流畅能开缓存插件日 PV 万级以内够用4 核 8G 以上宽裕可以跑多站点或加 Redis 缓存层关键点在 MySQL。MySQL 8 的默认 innodb_buffer_pool_size 是 128M但它的其他开销连接线程、临时表、performance_schema加起来一个空实例就能吃掉 400M 以上。如果是 1G 内存的机器容器一启动就接近临界点稍微来点并发就被内核 OOM killer 干掉表现是数据库容器莫名其妙重启WordPress 报“建立数据库连接时出错”。所以内存小于 2G 的机器要么换成 MySQL 5.7要么在启动参数里把 buffer pool 压到 128M 甚至 64M同时把 performance_schema 关掉。磁盘方面镜像本身大概占 1.5Gmysql 8 约 600Mwordpress-fpm 约 700Mnginx-alpine 约 50M加上数据库和上传文件建议预留 20G 以上。如果你的服务器是系统盘小、数据盘大的结构把 Docker 的数据目录挪到数据盘上这个在第 3 节配置。2.2 内核与文件系统检查一个必须做的动作CentOS 7 默认内核是 3.10Docker 要求 3.10 以上这条是过的。但真正会坑人的是文件系统。Docker 的首选存储驱动是 overlay2它对底层文件系统有一个硬要求xfs 必须开启 ftype1。查看命令很简单uname -r df -T / xfs_info / | grep ftype如果输出里是ftype0那 overlay2 就用不了Docker 会自动回退到 vfs 驱动。vfs 是什么概念它不做任何分层复用每写一层就把整个目录完整拷贝一次。一个 WordPress 站点拉起来镜像解压的体积可能是原来的十倍磁盘瞬间见底而且每次构建都慢得离谱。CentOS 7 从 7.4 之后的默认安装基本都是 ftype1 了但老机器大概率是 0。遇到 ftype0 有三个选择重装系统时加--ftype1参数格式化、把 /var/lib/docker 挂到一个新格式化的 ext4 分区上ext4 没这个限制、或者干脆接受 vfs 但把磁盘留足。我一般选第二个因为不用动系统盘数据。顺便检查一下 SELinux 状态getenforce返回 Enforcing 的话容器挂载宿主机目录时会遇到“Permission denied”哪怕目录是 777。原因不是权限位的问题是 SELinux 的安全上下文不认。处理方式有两种给挂载目录打上container_file_t标签chcon -Rt container_file_t /opt/wordpress/html或者在挂载参数后面加:z。我不建议直接关 SELinux那是把整台机器的防护拆了但如果这台机器只跑这一个应用、且在内网关闭也确实是最省事的选择看你自己的风险承受度。2.3 清理可能存在的旧版本如果这台机器以前装过 Docker哪怕是 yum 装的旧版 docker-common必须先清干净否则会出现两个 docker 二进制打架、或者存储目录格式不兼容的情况。yum remove -y docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine注意这条命令不会删/var/lib/docker目录。里面是旧版本的镜像和容器数据如果你确定不要了手动rm -rf /var/lib/docker如果里面还有能用的镜像可以先留着但旧版本比如 1.x 时代的存储格式新版本读不了还是要清。2.4 防火墙与系统源的处理顺序这两件事有先后顺序搞反了会多折腾半小时。先说系统源CentOS 7 官方源失效是最常见的拦路虎表现是yum install报 404 或者一直超时。解决办法是把 repo 指向归档源cd /etc/yum.repos.d/ sed -i s/mirrorlist/#mirrorlist/g /etc/yum.repos.d/CentOS-*.repo sed -i s|#baseurlhttp://mirror.centos.org|baseurlhttp://vault.centos.org|g /etc/yum.repos.d/CentOS-*.repo yum clean all yum makecacheCentOS-Base.repo里有个$releasever变量CentOS 7 解析出来是 7vault 路径下确实有 7.9.2009 这个目录能正常拉到包。这一步不通过后面装 yum-utils 都装不上。再说防火墙。firewalld 和 Docker 的关系比较微妙Docker 启动时会自己往 iptables 里插规则而且它的规则优先级在 firewalld 之前结果就是你在 firewalld 里关了 8080 端口容器映射出来后外部照样能访问。这个行为经常把人吓一跳。我的处理原则是宿主机如果有 firewalld 在管其他服务就不要让 Docker 去碰 iptables在 daemon.json 里加iptables: false然后端口一律在 firewalld 里显式放行如果这台机器只跑容器直接把 firewalld 停掉、由云厂商的安全组统一管控逻辑最简单也不容易出“明明关了端口却能访问”的诡异现象。systemctl stop firewalld systemctl disable firewalld注意停 firewalld 之前必须确认外层有安全组或者硬件防火墙兜底。宿主机上不设防、又没有网络层防护等于把数据库端口暴露在公网上——MySQL 被扫描爆破是很常见的事。3. Docker 安装从仓库到验证的完整链路3.1 仓库配置与安装命令社区版仓库的配置很直接yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo加完之后先看一眼能不能解析到包yum list docker-ce --showduplicates | sort -r | head -20这里有个细节值得说docker-ce.repo里用的也是$releasever变量CentOS 7 上解析成 7而仓库里确实保留了 7 的目录所以即使系统 EOL 了这个源通常还是能用的。如果解析不出来把 repo 文件里的$releasever手动改成7即可。安装时我建议锁定版本而不是无脑 latestyum install -y docker-ce-24.0.9 docker-ce-cli-24.0.9 containerd.io docker-compose-plugin为什么锁 24.0.x因为 CentOS 7 的 glibc 是 2.17Docker 25 之后的某些版本对 glibc 版本要求提高装上去可能起不来。24.0 是我实测在 CentOS 7 上最省心的一个系列功能也完全够用。锁版本还有一个好处半年后你重装环境装出来的东西和现在一模一样不会因为上游更新引入行为变化。关于 composedocker-compose-plugin装的是 v2 版本命令是docker compose中间是空格。如果这个包在你的环境下装不上主要是依赖解析的问题退而求其次装 Python 版的 v1yum install -y python3 python3-pip pip3 install docker-compose1.29.2v1 的命令是docker-compose带横杠。两套命令的语法基本兼容但 v1 已经停止维护能用 v2 就别用 v1。判断装没装成功docker compose version3.2 启动服务与开机自启systemctl enable --now docker systemctl status dockerenable --now是同时做两件事设置开机自启、立刻启动。CentOS 7 的 systemd 版本支持这个写法。启动后跑一下官方验证镜像docker run --rm hello-world看到 “Hello from Docker!” 就说明客户端、守护进程、镜像拉取、容器运行这条链路全通了。这个镜像很小但它验证的东西很全面比docker version更有说服力——后者只能证明守护进程活着。3.3 拉不动镜像时该看哪里国内网络环境下拉 Docker Hub 的镜像经常超时。表现是docker pull卡在 “Pulling from library/xxx” 几十秒然后报net/http: TLS handshake timeout或者i/o timeout。处理方法是在/etc/docker/daemon.json里配置镜像加速地址{ registry-mirrors: [https://你的加速地址] }改完systemctl daemon-reload systemctl restart docker再用docker info看最后一行有没有列出来。加速地址需要用你自己网络环境下能访问的那个随便找的公开地址随时可能失效。如果配了加速还是不行先确认是 DNS 的问题cat /etc/resolv.conf docker run --rm busybox nslookup registry-1.docker.ioCentOS 7 上偶尔会出现容器内 DNS 解析不了、宿主机却正常的情况原因是 Docker 默认把宿主机的 resolv.conf 拷进容器如果里面有127.0.0.1这种回环地址容器里就解析不了。在 daemon.json 里指定dns: [8.8.8.8, 1.1.1.1]可以绕开。3.4 daemon.json 里那些不该省略的参数很多人装完 Docker 就完事了daemon.json 空着。下面这份是我每次都会写的{ registry-mirrors: [https://你的加速地址], data-root: /data/docker, log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 }, live-restore: true, default-ulimits: { nofile: { Name: nofile, Hard: 65535, Soft: 65535 } } }逐条说清楚为什么要加>systemctl daemon-reload systemctl restart docker docker info | grep -A5 Logging Driver注意>mkdir -p /opt/wordpress/{html,mysql,nginx/conf.d,nginx/certs,nginx/acme,php} cd /opt/wordpress结构说明html/放 WordPress 全站文件包括 wp-content 下的主题、插件、上传的图片。这是必须要能直接看到的目录备份、改代码、传主题都靠它。mysql/放数据库文件。直接挂载这个目录而不是用命名卷好处是备份时可以直接停容器打包坏处是不能跨大版本降级MySQL 8 的数据目录用 5.7 打不开。nginx/conf.d/放站点配置改完docker compose restart nginx就生效不用重建镜像。php/放自定义的 php.ini 片段主要用来调上传大小和执行时间。为什么不用 Docker 的命名卷命名卷确实更“容器原生”但它的数据藏在/var/lib/docker/volumes/下面名字是一串哈希备份脚本要额外用docker run --rm -v挂进去打包多一层心智负担。对于 WordPress 这种“文件即资产”的应用bind mount 到固定目录直观得多出问题了用ls就能看用rsync就能备。唯一的代价是权限。容器里的 php-fpm 以www-datauid 33运行宿主机上的目录如果归 root 所有写文件就会失败。处理方式chown -R 33:33 /opt/wordpress/html这条命令要在 WordPress 首次启动生成文件之后再执行因为容器初始化时也会尝试 chown顺序反了会被覆盖。如果宿主机上没有 uid 33 这个用户chown 33:33一样有效Linux 只认数字。4.2 编排文件逐段拆解/opt/wordpress/docker-compose.ymlname: wp services: db: image: mysql:8.0.36 container_name: wp-db restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} TZ: ${TZ} command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --default-authentication-pluginmysql_native_password - --innodb-buffer-pool-size256M - --max-connections200 - --performance-schemaOFF volumes: - ./mysql:/var/lib/mysql healthcheck: test: [CMD-SHELL, mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent] interval: 10s timeout: 5s retries: 12 start_period: 60s networks: - wpnet wordpress: image: wordpress:6.4-php8.2-fpm container_name: wp-app restart: unless-stopped depends_on: db: condition: service_healthy environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_NAME: ${MYSQL_DATABASE} WORDPRESS_DB_USER: ${MYSQL_USER} WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD} WORDPRESS_TABLE_PREFIX: ${WP_TABLE_PREFIX} WORDPRESS_CONFIG_EXTRA: | define(WP_MEMORY_LIMIT, 256M); define(DISALLOW_FILE_EDIT, true); TZ: ${TZ} volumes: - ./html:/var/www/html - ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro networks: - wpnet nginx: image: nginx:1.25-alpine container_name: wp-web restart: unless-stopped depends_on: - wordpress ports: - 80:80 - 443:443 volumes: - ./html:/var/www/html:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/certs:/etc/nginx/certs:ro - ./nginx/acme:/var/www/acme:ro networks: - wpnet networks: wpnet: driver: bridge几个关键设计点值得单独说为什么用 php-fpm 镜像而不是 apache 版。官方的wordpress镜像默认是 Apache 版一个容器就把 Web 服务器和 PHP 打包了对新手最友好ports: - 8080:80一映射就能访问。但它的静态文件处理和 HTTPS 配置比较别扭。用-fpm变体 独立 nginx 容器职责更清晰nginx 管 TLS、静态资源、缓存头php-fpm 只管执行 PHP。代价是多一个容器换来的是配置的完全可控。depends_on为什么要配condition: service_healthy。普通的depends_on只保证启动顺序不保证数据库“已经能接受连接”。MySQL 容器从启动到 ready 通常要 20 到 40 秒如果 WordPress 先起来了初始化脚本连不上数据库会写一份错误的 wp-config.php后面一直报连接失败。加上健康检查之后Compose 会等到mysqladmin ping通过才启动 wordpress这个坑就没了。healthcheck 里的$$是什么。Compose 会对$做变量替换$$是转义成字面量$。所以-p$$MYSQL_ROOT_PASSWORD传到容器里是-p$MYSQL_ROOT_PASSWORD由容器的 shell 展开成实际密码。写成单个$的话Compose 会在宿主机上就把它替换成空值健康检查永远失败。--default-authentication-pluginmysql_native_password这个参数要注意版本。它在 MySQL 8.0 里用来兼容老客户端但到了 8.4 已经被移除写上去容器直接起不来。这也是我为什么把镜像锁成mysql:8.0.36而不是mysql:latest——latest现在指向 8.4 甚至 9.x参数一变整套配置全废。4.3 环境变量文件与权限控制.env文件放在 compose 同目录Compose 会自动读取它做变量替换cat /opt/wordpress/.env EOF MYSQL_ROOT_PASSWORD换成你自己的强密码 MYSQL_DATABASEwordpress MYSQL_USERwpuser MYSQL_PASSWORD另一个强密码 WP_TABLE_PREFIXwp_ TZAsia/Shanghai EOF chmod 600 /opt/wordpress/.env密码不要用纯字母数字的短串也不用在 compose 文件里硬编码。.env一旦提交进 Git等于把数据库密码公开了所以项目里一定要有.gitignore只提交.env.example。表前缀wp_建议改成别的不是因为它能防住什么纯粹是减少被自动化脚本盲扫的概率。WP_TABLE_PREFIX只有首次安装时生效。已经装好的站改这个值等于让 WordPress 去找一张不存在的表表现是“欢迎使用 WordPress 安装向导”看着像数据丢了其实只是前缀对不上。4.4 Nginx 站点配置与 PHP 参数/opt/wordpress/nginx/conf.d/site.confserver { listen 80; server_name example.com; root /var/www/html; index index.php index.html; client_max_body_size 64m; location /.well-known/acme-challenge/ { root /var/www/acme; } location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { include fastcgi_params; fastcgi_pass wordpress:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_read_timeout 300; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff2?)$ { expires 30d; access_log off; add_header Cache-Control public; } location ~ /\.(ht|git) { deny all; } }fastcgi_pass wordpress:9000里的wordpress是 compose 里的服务名。Compose 会给每个服务在自定义网络里注册一个 DNS 记录容器之间直接用服务名互访不需要知道 IP。这也是为什么数据库连接地址写db:3306而不是127.0.0.1——后者在容器里指的是容器自己。try_files $uri $uri/ /index.php?$args这一行是 WordPress 固定链接正常工作的前提。少了它所有非首页的文章页都会 404。$args不能省否则分页、搜索、筛选参数全会丢。client_max_body_size 64m和 PHP 侧的上传限制要配套。只改一边的话表现是“上传到 100% 然后失败”或者“明明设了 64M 却只能传 8M”。PHP 侧配置放在php/uploads.iniupload_max_filesize 64M post_max_size 64M memory_limit 256M max_execution_time 300 max_input_vars 5000max_input_vars这个容易被忽略。默认值是 1000WordPress 装了几个插件之后后台保存菜单或者主题选项时表单字段数量很容易超过这个限制表现是“保存后部分设置莫名其妙丢了”没有任何报错。调到 5000 是个安全的做法。4.5 启动、验证与首次安装cd /opt/wordpress docker compose up -d docker compose ps docker compose logs -f wordpressdocker compose ps里 STATE 那一列正常情况下 db 会显示Up (healthy)另外两个是Up。如果 db 一直是Up (health: starting)超过两分钟去看docker compose logs db大概率是初始化数据目录时权限不对宿主机目录属主不是 999。浏览器打开http://你的IP应该看到语言选择页。数据库信息填字段填什么数据库名.env里的 MYSQL_DATABASE用户名.env里的 MYSQL_USER密码.env里的 MYSQL_PASSWORD数据库主机db:3306表前缀.env里的 WP_TABLE_PREFIX主机这一栏填db:3306而不是localhost是新手最容易卡住的地方。localhost在容器里指向容器自身那里没有数据库。安装完成后ls -l /opt/wordpress/html应该能看到wp-config.php、wp-admin、wp-content这些。如果目录是空的但网站能访问说明文件写进了容器内部而不是挂载目录——检查 compose 里的 volumes 路径有没有拼错。5. 上线前必须补的几件配置5.1 HTTPS 的落地步骤现在浏览器对 HTTP 站点的提示越来越不友好博客挂个证书是基本要求。我用的方式是用 acme 客户端做域名验证把验证文件放到 nginx 能访问的目录里拿到证书后 nginx reload。先把site.conf里的server_name改成真实域名并且让/.well-known/acme-challenge/指向一个挂载出来的目录上面配置里是/var/www/acme宿主机对应./nginx/acme。这个 location 必须在location /之前否则会被 try_files 吞掉。签发流程大致是客户端生成验证文件写入nginx/acme目录CA 服务端回访http://域名/.well-known/acme-challenge/xxx校验通过后签发证书再把证书文件写到nginx/certs。整个流程的关键是 80 端口必须能从公网访问到且路径映射正确。拿到证书后把 server 块改成 443 监听80 只做跳转server { listen 80; server_name example.com; location /.well-known/acme-challenge/ { root /var/www/acme; } location / { return 301 https://$host$request_uri; } } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; # 其余配置同上 }ssl_session_cache这一行对小站提升明显握手复用的开销能省掉一大半。注意证书续期必须要做。acme 客户端一般支持--cron参数加到 crontab 里每天跑一次到期前 30 天会自动续。续期成功后要执行docker compose exec nginx nginx -s reload否则 nginx 还拿着旧证书在内存里。5.2 备份策略只备数据库是不够的WordPress 的资产分两块数据库里的文章、设置、评论和wp-content里的主题、插件、上传的图片。只备数据库恢复出来是个没有图的空站只备文件文章全丢。两个都要。数据库备份用 mysqldump 在容器里执行导出到宿主机的挂载目录docker compose exec -T db sh -c \ exec mysqldump --single-transaction --quick --default-character-setutf8mb4 \ -u root -p$MYSQL_ROOT_PASSWORD $MYSQL_DATABASE \ | gzip /opt/backup/db-$(date %F).sql.gz--single-transaction是必须的它让 InnoDB 在一致性快照里导出不锁表。不加这个参数导出期间站点会卡住流量大的时候用户直接看到超时。--default-character-setutf8mb4是为了避免中文乱码——不带这个参数导出的数据恢复到另一个字符集不同的实例里表情符号和部分中文会变成问号。文件备份用 rsync 更合适rsync -a --delete /opt/wordpress/html/wp-content/ /opt/backup/wp-content/只备wp-content就够了WordPress 核心文件可以从官方包重新解压没必要占空间。备份脚本放进 crontab0 3 * * * /opt/scripts/backup.sh /var/log/wp-backup.log 21我一般保留最近 7 天的数据库备份和一份最新的文件快照脚本里加一行find /opt/backup -name db-*.sql.gz -mtime 7 -delete自动清理。备份完必须验证——我见过太多“备份文件是 0 字节”的情况尤其是数据库还没 ready 的时候执行 dump。脚本里加一句[ -s $file ] || echo 备份异常就能发现。5.3 资源限制与性能参数不给容器设资源上限一个容器跑飞了会把整台机器拖死。compose 里可以加deploy: resources: limits: memory: 512M注意deploy.resources.limits在非 swarm 模式下docker compose v2 是支持的v1 需要改成mem_limit: 512M。设了内存上限之后容器超过阈值会被 OOM killer 干掉表现是 WordPress 突然 502 然后自己恢复。所以memory_limit和容器的内存上限要留出余量PHP 进程数 × 单进程内存 系统开销 容器上限。PHP-FPM 的进程数在wordpress镜像里可以通过环境变量或者挂载配置调整。默认的pm dynamic在 2G 内存的机器上会开 5 个进程起步每个进程 50M 左右加上 MySQL 的 400M接近临界点。稳妥的做法是挂一个www.conf进去把pm.max_children定成 5 到 8pm.max_requests设 500 让进程定期回收避免内存泄露累积。静态资源那块nginx 配置里已经加了 30 天的 expires。再往前一步可以开 gzipgzip on; gzip_types text/css application/javascript application/json image/svgxml; gzip_min_length 1k;gzip_types里不要加图片格式jpeg 和 png 本来就是压缩过的再压一遍只浪费 CPU。6. 那些年踩过的报错与排查路径6.1 数据库连接失败的三层排查“Error establishing a database connection” 是出现频率最高的报错但它至少有四种原因得按层次排除。第一层数据库容器起来没有。docker compose ps看 db 的状态如果是Restarting或者Exited直接看日志docker compose logs --tail100 db日志里出现[ERROR] InnoDB: Operating system error number 13就是权限问题./mysql目录的属主不对需要chown -R 999:999 ./mysql。第二层网络能不能通。进 wordpress 容器测一下docker compose exec wordpress sh -c php -r var_dump(fsockopen(\db\, 3306));返回 resource 就说明网络通问题在认证返回 false 说明网络不通检查两个服务是不是在同一个 network 里。第三层认证信息对不对。wp-config.php里的用户名密码和.env里的是否一致。这里有个隐蔽的坑.env改了密码但./mysql目录里存的还是旧密码因为 MySQL 只在初始化数据目录时读取MYSQL_PASSWORD。改密码要么进数据库执行ALTER USER要么删掉./mysql重来数据会丢。6.2 上传失败与 502 的区分上传文件失败和 502 是两种完全不同的故障但新手经常混为一谈。上传失败通常表现为进度条走完然后提示“无法上传”或者图片上传成功但显示破图。前者查两个地方nginx 的client_max_body_size和 PHP 的upload_max_filesize两个值必须一致且都不小于目标文件。后者查权限wp-content/uploads目录必须归 uid 33 所有。502 Bad Gateway 是 nginx 连不上 php-fpm。进 nginx 容器测试docker compose exec nginx wget -qO- http://wordpress:9000正常应该立刻返回连接被拒的错误信息如果是一直挂起然后超时说明 DNS 解析或者网络有问题。如果 php-fpm 容器本身在重启看日志找原因——常见的是memory_limit太小导致 PHP 进程崩溃。还有一个隐蔽的 502fastcgi_read_timeout太短。导入大数据库或者后台批量操作时PHP 执行时间超过默认的 60 秒nginx 主动断开用户看到 502但服务端其实还在跑。上面配置里设成 300 秒就是为这个。6.3 常见问题速查表现象最可能的原因处理方式容器启动后立刻退出环境变量缺失或参数冲突docker compose logs 服务名看首屏报错后台保存设置丢字段max_input_vars太小调到 5000 并重启 php-fpm图片上传失败但无报错uploads 目录权限不对chown -R 33:33 html/wp-content站点变慢磁盘占用飙升容器日志无限增长daemon.json 加 log-opts 上限数据库容器反复重启内存不足被 OOM调小 buffer pool 或加 swap迁移后所有链接 404固定链接规则失效nginx 加 try_files 或重设固定链接时间显示差 8 小时容器时区为 UTC加TZAsia/Shanghai环境变量端口被占用起不来宿主机已有 80 在监听ss -lntp拉镜像一直超时加速地址没配或已失效docker info检查换可用地址宿主机 iptables 规则被清firewalld 与 docker 冲突二者选一或关掉 docker 的 iptables 管理7. 长期维护的几点实操心得7.1 升级要按“小步快跑”的节奏WordPress 的版本升级在容器里做风险主要在数据库结构的演进。跨大版本比如 5.x 直接跳到 6.6时我一般会先做完整备份然后在一个临时目录里用一份数据库副本跑一遍升级流程确认没问题再动生产。镜像升级同理。compose 文件里的wordpress:6.4-php8.2-fpm写成具体版本号升级时改 tag 再docker compose up -d这样回滚就是改回原来的 tag两分钟的事。写latest的后果是某天重启之后站突然白屏你还不知道是哪个版本变的。升级 PHP 主版本8.2 到 8.3要额外注意扩展兼容性。WordPress 核心一般没问题出问题的是第三方插件里那些老的、已经没人维护的扩展。升级前在测试环境跑一遍所有插件比在生产上撞大运强。7.2 整站迁移的正确打包方式迁移的坑不在打包在下游的域名和路径替换。步骤我一般是这样的先在源机停容器保证数据文件是静止的cd /opt/wordpress docker compose down tar czf wp-site.tar.gz .env docker-compose.yml html mysql nginx php注意docker compose down而不是stop。down会移除容器和网络stop只停容器迁移时把容器定义一起带过去没意义。数据都在挂载目录里容器本身是无状态的删掉随时重建。到目标机解压后先改.env和 nginx 的server_name然后docker compose up -d。如果换了域名必须进数据库把旧的 URL 换掉否则邮件通知里的链接、后台跳转、图片地址全是旧的。用 wp-cli 做是最稳的docker compose exec wordpress wp search-replace https://old.com https://new.com --all-tables --precise--all-tables是关键默认只搜 wp_posts 等主表插件自己建的配置表不会被处理。--precise让 wp-cli 做序列化安全替换直接跑 SQL 的 REPLACE 会把序列化数据里的长度字段改错导致主题设置全部失效——这个坑我踩过一次恢复起来极其麻烦。7.3 该盯哪些指标小站不用上完整的监控栈但下面三个数值得每天扫一眼df -h看磁盘。低于 20% 剩余空间就该清理了Docker 的镜像清理用docker image prune -a --filter until720h能删掉一个月没用过的镜像。docker stats --no-stream看容器内存和 CPU。如果某个容器内存一直在涨不回落基本是内存泄露配合pm.max_requests定期回收进程能缓解。docker compose logs --since 24h | grep -i error扫错误。WordPress 的 PHP 错误默认不写到日志文件需要在 wp-config.php 里打开WP_DEBUG_LOG写到wp-content/debug.log。这个文件会越滚越大记得定期清。提示debug.log里如果出现大量“PHP Fatal error”别急着删文件先看是哪个插件报的。很多插件在 PHP 8 环境下用了已废弃的函数错误堆着但功能还能用也可能某个功能直接失效了。一张我自己常用的巡检命令清单可以打包成脚本每天跑#!/bin/bash cd /opt/wordpress echo 磁盘 ; df -h / | tail -1 echo 容器状态 ; docker compose ps --format table {{.Name}}\t{{.Status}} echo 资源占用 ; docker stats --no-stream --format table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}} echo 近24小时错误 ; docker compose logs --since 24h 21 | grep -ci error echo 站点探活 ; curl -sS -o /dev/null -w %{http_code}\n -m 10 http://127.0.0.1/最后一条探活返回值 200 或 301 都算正常500 说明 PHP 挂了502 说明 php-fpm 连不上这些数值比看日志快得多。一个我自己的体会这套东西真正的价值不在于“装得快”而在于“恢复得快”。我经历过一次因为磁盘写满导致数据库容器反复重启整个站挂了半小时——那次之后我把日志上限、磁盘告警、备份验证三件事全部补齐了。容器化把环境的复杂度收进了一个文件但运维的复杂度并没有消失只是换了个地方。把 compose 文件当成代码来管、把备份当成功能来测这台机器才能真正撑得久。