☰
CentOS 7 下 Docker 容器化部署 WordPress 实战排障
2026/10/27 11:26:25 网站建设 项目流程

1. 动手之前先摸清这台 CentOS 7 的底细

CentOS 7 上加 Docker 再跑一套 WordPress,这套组合在过去几年里被无数人用过,看着像是"照着命令敲一遍就完事"的活。真正上手你会发现,大多数翻车点不在 Docker 本身,而在这台机器的内核状态、磁盘布局、软件源可达性这些"地基"上。我遇到过不止一次这样的情况:命令一路回车下去,docker ps能出结果,docker compose up也提示成功,浏览器打开却只有一片空白或者 502,最后排查半天发现是内核小版本太老、overlay2 起不来,或者是/var/lib/docker所在分区根本撑不住一个 MySQL 的数据目录。

这篇内容面对的是手头确实还有一台 CentOS 7 机器、需要在上面跑 WordPress 的人,可能是把老博客从裸机 PHP 迁进容器,也可能是给内部团队搭一个内容站。默认你有一点 Linux 命令行基础,知道yum、systemctl、vim是干什么的,不要求你懂容器原理。整套流程我会从机器体检讲到上线后的故障排查,中间每一个选择都会说明为什么要这么选,方便你判断自己那台机器上能不能照着抄。

1.1 内核小版本与文件系统格式的两项硬性前置

CentOS 7 出厂内核是 3.10 系列,这个版本号看着离 Docker 要求的 3.10 很近,实际上坑就藏在小版本里。overlay2 存储驱动依赖内核中的 overlayfs 实现,CentOS 7.2 之后的内核做过相关补丁回移植,早期 7.0、7.1 的机器跑起来会报overlay2 not supported,然后 Docker 悄悄退回 devicemapper 的 loop 模式。这个模式用一个稀疏文件模拟块设备,写入性能差,而且容量会被那个文件锁死,跑 WordPress 这种持续写上传文件、写数据库的场景,很容易在某天突然报磁盘满,而df -h显示还有大把空间。

所以第一件事是确认自己的内核小版本:

uname -r # 期望看到 3.10.0-957 或更高,例如 3.10.0-1160.el7.x86_64 cat /etc/centos-release

如果是3.10.0-327这类偏早的版本,先做一次系统更新把内核升上去,然后重启。别嫌这一步麻烦,因为它决定了后面 Docker 的运行方式,跳过去只会把问题推迟到更难查的位置。

第二项是根文件系统的类型。容器层和镜像层要靠联合文件系统叠起来,ext4 和 xfs 都可以,但 xfs 必须带ftype=1这个特性才能支持 overlay2。很多人在装机时用了默认参数格式化的 xfs,ftype是 0,这种情况下 Docker 会明确告诉你 xfs 不支持 d_type,然后干脆拒绝启动。检查方法:

df -Th /var/lib/docker xfs_info /var/lib/docker | grep ftype

看到ftype=0就意味着要么重新格式化分区(生产机上这基本不现实),要么把/var/lib/docker单独挂到一个 ext4 分区上去。我自己的做法是给 Docker 单独划一块盘或者挂一个数据盘,格式化成 ext4 挂到/var/lib/docker,这样既绕开 ftype 问题,也避免了容器镜像把根分区撑爆。

提示:如果暂时没有独立分区,至少先执行du -sh /var看一眼现有占用,再规划 Docker 数据目录的位置。一个 WordPress 站点加上 MySQL 数据、镜像层、日志,正常占用在 5 到 15 GB 之间,还要给备份留出同等空间。

1.2 SELinux 与 firewalld 对容器的两处干扰

CentOS 7 默认开着 SELinux,模式是 enforcing。它不是不能和 Docker 共存,而是共存时需要额外的卷标签处理。当你用绑定挂载把宿主机目录映射进 WordPress 容器时,宿主机上的文件带着unconfined_u:object_r:usr_t之类的上下文,容器里的进程(以 www-data 身份运行)基于策略访问会被拒绝,表现就是 WordPress 提示"无法写入 wp-config.php"或者插件安装时卡在上传失败。加:z或:Z后缀能给卷重新打标签,这是官方推荐做法,我在下面写 compose 文件时会带上。

至于 firewalld,它和 Docker 的端口管理是两套体系。Docker 启动容器做端口映射时,会直接把规则插到 iptables 的DOCKER链里,绕过 firewalld 的管理。结果就是你在 firewalld 里明确关了 8080,容器映射之后外部依然能访问。这个行为曾经让不少人误以为防火墙失效。正确的做法是:对外只暴露入口层(比如 Nginx)需要的端口,容器之间的通信全部走 Docker 自建网络的内部解析,不映射到宿主机;需要限制来源时,在 Docker 的daemon.json里设置"iptables": false并自己维护规则,或者干脆在入口层做 IP 白名单。

getenforce # 查看 SELinux 当前模式 firewall-cmd --state # 确认 firewalld 是否在跑 firewall-cmd --list-ports # 看已放行的端口

1.3 CentOS 7 停止维护后的软件源处理

这一点必须单独说,因为它会让后面的安装步骤直接卡在第一步。CentOS 7 已经在 2024 年 6 月停止维护,官方把原有的镜像列表地址下线了,yum makecache会报一堆 404 或者Cannot find a valid baseurl。此时需要把 base 仓库指向归档地址:

cd /etc/yum.repos.d/ cp CentOS-Base.repo CentOS-Base.repo.bak sed -i 's/^mirrorlist=/#mirrorlist=/' CentOS-Base.repo sed -i 's|^#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|' CentOS-Base.repo yum clean all && yum makecache

执行完再yum repolist,能看到几个仓库和包数量就说明通了。如果你的机器在内网、有单位自建的 yum 镜像站,直接把 baseurl 指过去更省事,速度也快。这一步看着和 Docker 没关系,但后面装yum-utils、device-mapper-persistent-data、lvm2这几个依赖包时全靠它,源不通就是一连串的失败。

2. Docker Engine 的安装路径与 daemon 配置

装 Docker 有三条路:系统自带源里的docker包、官方仓库的docker-ce、以及各种一键脚本。CentOS 7 自带源里的版本太老(1.13 时代),很多语法和 Compose 特性都不支持,直接排除。一键脚本看着省事,但它会顺手改一堆系统配置,出了问题很难回滚。我一般用官方仓库的方式,版本可控,卸载干净。

2.1 清理残留与安装依赖的正确顺序

如果这台机器以前装过 Docker(哪怕是失败的尝试),先彻底清一遍,避免新旧包冲突导致 daemon 起不来:

yum remove -y docker docker-client docker-client-latest docker-common \ docker-latest docker-latest-logrotate docker-logrotate docker-engine \ docker-selinux docker-ce docker-ce-cli containerd.io rm -rf /var/lib/docker

注意rm -rf /var/lib/docker会删掉所有镜像和容器数据,机器上有存量数据的话先备份再执行。清完之后装依赖:

yum install -y yum-utils device-mapper-persistent-data lvm2

yum-utils里带了yum-config-manager,后面加仓库要用;device-mapper-persistent-data和lvm2是 devicemapper 驱动的依赖包,就算你最终用 overlay2,Docker 的安装脚本也会检查这两个包,缺了会报依赖错误,索性一次装齐。

2.2 添加官方仓库并锁定一个合适的版本

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 版本。这里有个现实问题:CentOS 7 停止维护之后,新版 Docker 对它的支持是逐步收缩的,越新的版本装上去越可能遇到依赖或者运行时的奇怪问题。我的建议是不要盲目装 latest,从列表里挑一个 24.x 或 25.x 的版本,把它钉死:

yum install -y docker-ce-24.0.9 docker-ce-cli-24.0.9 containerd.io \ docker-compose-plugin

钉版本的理由很实际:容器这东西一旦跑起来,你未必会经常动它,某天因为别的原因重装或者换机器,如果没记版本,yum install docker-ce装上的可能是又一个新大版本,行为差异足够让你重新排查一遍。把版本号记在部署文档里,比什么都有用。

提示:docker-compose-plugin提供的是docker compose(v2,作为子命令调用),不是老式的docker-compose(v1,独立 Python 程序)。v1 早已停止维护,新项目别再用。如果 plugin 装不上或者运行时报错,可以去下载 Compose 的独立二进制文件,放到/usr/local/lib/docker/cli-plugins/docker-compose并chmod +x,效果一样。

2.3 daemon.json:几行配置决定长期运行质量

安装完成不要急着systemctl start docker,先把/etc/docker/daemon.json写好。这个文件不存在就新建,内容是:

{ "storage-driver": "overlay2", "data-root": "/var/lib/docker", "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" }, "live-restore": true, "default-address-pools": [ { "base": "172.31.0.0/16", "size": 24 } ] }

逐条解释一下为什么加:

storage-driver显式指定 overlay2,避免 Docker 自动探测到 devicemapper 上去;>systemctl daemon-reload systemctl enable docker systemctl start docker docker info | grep -E "Storage Driver|Docker Root Dir|Server Version"

Storage Driver显示 overlay2、Docker Root Dir是你预期的路径,这两项对了就说明地基打好了。

2.4 普通用户权限与常用别名

日常操作一直挂 root 不安全,也不方便。把用户加进 docker 组:

usermod -aG docker yourname # 退出重新登录后生效 docker ps

docker组的权限等价于 root,这点要有心理准备,只在可信的开发机上这么做。顺手在~/.bashrc里加点别名,长期用下来能省不少敲键:

alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"' alias dlog='docker logs --tail 100 -f' alias dcu='docker compose up -d' alias dcd='docker compose down'

3. WordPress 容器化的方案取舍

WordPress 跑在容器里,有两种常见形态:一种是把 Nginx、PHP-FPM、MySQL 全拆成独立容器,用 Compose 编排;另一种是用官方wordpress镜像,它内部已经打包好了 Apache 和 PHP,只差一个数据库。两种都行,但适用场景差别很大,选错了后面维护会很别扭。

3.1 单容器与多容器编排的真实差异

维度官方 wordpress 镜像(Apache 内置)拆分的 Nginx + PHP-FPM
上手成本低,一个 compose 文件两个服务中,需要自己写 Nginx 配置和 fastcgi 参数
静态文件性能一般,Apache 处理好,Nginx 直接返回
缓存方案扩展需要额外挂插件或加容器方便接 fastcgi_cache
配置调试难度低,日志集中在两个容器高,PHP 和 Nginx 两侧都要看
适合场景个人博客、中小内容站、内部系统有明确并发压力、需要精细调优的站点

我给绝大多数人的建议是第一种。WordPress 这类应用的瓶颈几乎从来不在 Web 服务器是 Apache 还是 Nginx 上,90% 的性能问题来自数据库查询、插件质量、没有页面缓存。先把它跑起来、跑对,比一开始就追求架构优雅有意义得多。等你确实遇到了静态资源压力,再把入口层换成 Nginx 做转发和缓存,那时改动量也不大。

3.2 compose 文件逐字段拆解

在/opt/wordpress下建docker-compose.yml:

services: db: image: mysql:8.0 container_name: wp_db restart: unless-stopped command: - --default-authentication-plugin=mysql_native_password - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --innodb-buffer-pool-size=256M environment: MYSQL_DATABASE: wordpress MYSQL_USER: wpuser MYSQL_PASSWORD: ChangeThisPassword MYSQL_ROOT_PASSWORD: ChangeThisRootPassword volumes: - db_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pChangeThisRootPassword"] interval: 10s timeout: 5s retries: 10 wordpress: image: wordpress:6.5-php8.2-apache container_name: wp_app restart: unless-stopped 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: ChangeThisPassword WORDPRESS_CONFIG_EXTRA: | define('WP_MEMORY_LIMIT', '256M'); define('DISALLOW_FILE_EDIT', true); volumes: - wp_data:/var/www/html healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1/wp-login.php"] interval: 30s timeout: 5s retries: 3 volumes: db_data: wp_data:

几个字段值得单独说:

ports写的是127.0.0.1:8080:80,前面带了回环地址。这样容器只在宿主机本地监听 8080,外网访问不到,必须经过入口层转发。很多人图省事写8080:80,等于把 WordPress 直接暴露在公网上,后面配 HTTPS 和访问控制都会很被动。

depends_on配了condition: service_healthy,配合 db 的 healthcheck,能保证 MySQL 真正可以接受连接之后才启动 WordPress。这个细节看似多余,实际上能避免大量"首次启动白屏、重启就好"的诡异现象,原因我在第 4 节展开。

WORDPRESS_CONFIG_EXTRA是官方镜像支持的环境变量,写进去的内容会被追加到生成的wp-config.php里。用它可以免去进容器改文件的麻烦。DISALLOW_FILE_EDIT关掉后台在线编辑主题文件的功能,是个低成本的加固手段。

数据库密码这种敏感值不该硬编码在 compose 文件里,正式环境用.env文件配合${MYSQL_PASSWORD}引用,并把.env的权限设成 600。这是习惯问题,早养成比事后补好。

3.3 命名卷和绑定挂载怎么选

上面的文件用的是命名卷(db_data、wp_data)。命名卷由 Docker 管理,位置在/var/lib/docker/volumes/下,好处是跨平台一致、权限由 Docker 初始化、备份时用docker run --rm -v打包就行。坏处是你不能直接用ls看里面的文件,想改个主题文件得进容器或者用临时容器挂载。

绑定挂载(./html:/var/www/html)则把宿主机目录直接映射进去,改文件方便,但会引入 SELinux 上下文和文件所有者两个问题。我的实践是分开处理:

  • 数据库数据用命名卷,不需要人肉干预,也不该被随手改动。
  • WordPress 的wp-content用绑定挂载,方便直接传主题、放插件、备份上传文件。
volumes: - wp_data:/var/www/html - ./wp-content:/var/www/html/wp-content:z

:z后缀告诉 Docker 给这个目录重新打 SELinux 标签,允许容器进程访问。宿主机上的./wp-content目录要先建好,并把所有者设成 33(容器内 www-data 的 UID):

mkdir -p /opt/wordpress/wp-content chown -R 33:33 /opt/wordpress/wp-content chmod -R 755 /opt/wordpress/wp-content

跳过chown这一步,最常见的结果是后台上传图片时报"无法建立目录",或者装插件时提示需要 FTP 凭据。这两个报错我见过太多次,几乎都是宿主机目录所有者不对导致的。

4. 数据库联调:让 WordPress 真正连上 MySQL

容器都起来了,docker compose ps显示两个服务都是 Up,不代表站点能用。这一节讲的是从"起来了"到"能打开"之间最容易出问题的几个点。

4.1 MySQL 8 的认证插件为什么会导致连接被拒

MySQL 8.0 默认的认证插件从mysql_native_password换成了caching_sha2_password。PHP 的 mysqli 扩展要 7.4 以上的版本才支持它,而且还得底层客户端库配合。如果你用的 WordPress 镜像是php7.4或更早,或者数据库不是通过官方镜像启动的、没带那个参数,就会看到这样的报错:

MySQL server has gone away 或者 The server requested authentication method unknown to the client

解决办法在 compose 的command里加--default-authentication-plugin=mysql_native_password,也就是上面那份文件里写的那样。这是 MySQL 8.0 系列的参数,到了 8.4 这个参数被移除了,改成用--mysql-native-password=ON启动,或者建库之后单独执行:

ALTER USER 'wpuser'@'%' IDENTIFIED WITH mysql_native_password BY 'ChangeThisPassword'; FLUSH PRIVILEGES;

我在实际项目里的做法是直接用mysql:8.0这个 tag,不追最新的 8.4,因为 WordPress 生态对 8.4 的适配还没完全跟上,为了省这点升级收益去踩认证坑不划算。真要升,先在测试环境把整条链路跑通再动生产。

4.2 环境变量、wp-config.php 与首次生成的时机

官方 wordpress 镜像的启动逻辑是这样的:容器启动时检查/var/www/html/wp-config.php是否存在,不存在就用环境变量里的WORDPRESS_DB_*生成一份。这意味着两件事。

第一,WORDPRESS_DB_HOST的值必须是db:3306这种服务名形式,不能写localhost或127.0.0.1。在容器里,localhost指的是容器自己,那里没有 MySQL,连接必然失败。这一点听起来很基础,但因为它和传统裸机部署的习惯直接冲突,是新手最容易写错的地方。

第二,如果你先启动过一次容器,wp-config.php已经生成并落在卷里了,之后你改环境变量里的数据库密码,容器不会重新生成配置文件,站点依旧用旧密码连库,然后报连接失败。这时候要么进容器手动改wp-config.php,要么删掉文件让它重新生成。知道这个机制,排查时就不会在环境变量上反复折腾。

docker compose exec wordpress cat wp-config.php | grep DB_

这条命令能直接看到容器里实际生效的数据库配置,比对着 compose 文件猜要快得多。

4.3 启动顺序与健康检查的联动

前面提到的depends_on+healthcheck组合,解决的是这样一个问题:MySQL 容器进程启动只要一两秒,但真正初始化数据库、建表、建用户需要十几秒甚至更久。WordPress 容器如果在这期间启动并尝试连接,会拿不到数据库然后退出。restart: unless-stopped会让它反复重启,运气好几次之后连上就正常了,运气不好就一直卡在循环里,日志刷屏。

健康检查的写法有讲究。mysqladmin ping在数据库还在初始化时也可能返回成功,因为它只检查进程是否响应,不检查数据库是否可用。更严格的方式是直接查一次:

healthcheck: test: ["CMD-SHELL", "mysql -uroot -p$$MYSQL_ROOT_PASSWORD -e 'SELECT 1' >/dev/null 2>&1"] interval: 10s timeout: 5s retries: 12 start_period: 30s

start_period给容器一个初始化宽限期,这段时间内的失败不计入重试次数,能避免初始化慢的机器上被误判为不健康。

排查连接问题时的顺序我一般是这样:先docker compose logs db看数据库有没有正常完成初始化,再docker compose exec db mysql -uwpuser -p -e 'show databases;'确认用户和库都在,最后docker compose exec wordpress php -r "new mysqli('db','wpuser','密码','wordpress');"直接测 PHP 层的连通性。三层都通了,站点打不开就一定是 Web 层的问题,与数据库无关,排查范围立刻缩小一半。

5. 入口层:用 Nginx 做转发、HTTPS 与体积控制

容器跑在127.0.0.1:8080上,外面的用户需要一个入口。宿主机上装的 Nginx(或者你也可以再套一个 Nginx 容器)承担三件事:把 80/443 的请求转发给容器、处理 TLS、控制上传体积和超时。

5.1 宿主机 Nginx 与 Nginx 容器的选择

两种都行。宿主机直接装 Nginx 的好处是配置调试直观、证书续期方便,坏处是依赖宿主机环境。用容器跑 Nginx 的好处是整个栈都在 Compose 里,迁移时一个目录打包带走,坏处是证书挂载和网络配置要理清楚。

我倾向宿主机装 Nginx,因为它是最外层,跟系统其他服务(比如证书自动续期的定时任务)耦合更自然,出问题也更好查。如果这台机器上还跑着别的站点,用宿主机 Nginx 可以在一个配置文件体系里统一管理。当时如果这台机器专用于 WordPress,且团队习惯"全部容器化",用 Nginx 容器也完全没问题,记得把它和 wordpress 放进同一个 Docker 网络,用服务名转发。

5.2 转发配置里容易被忽略的几行

server { listen 80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; client_max_body_size 64m; location / { proxy_pass http://127.0.0.1:8080; 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_http_version 1.1; proxy_read_timeout 120s; } }

X-Forwarded-Proto这一行是必须的。WordPress 靠它判断当前请求是不是 HTTPS,缺了这行,后台会出现"重定向次数过多"的循环,或者混合内容告警,页面加载时一堆 http 资源被浏览器拦截。同理Host头不传,WordPress 生成的链接会变成127.0.0.1:8080,站点一打开就是错链。

如果确定长期这么用,可以在wp-config.php里再加一道保险:

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; }

client_max_body_size和 WordPress 自己的上传限制是两回事,两边都要调,只改一边的结果是上传大文件时要么被 Nginx 挡回 413,要么被 PHP 挡回 500。

5.3 上传体积、PHP 参数与超时的联动调整

WordPress 容器里的 PHP 默认upload_max_filesize是 2M,post_max_size是 8M,这个限制在今天基本没法用。调整方式有两种:挂一个自定义 ini 文件进去,或者在 Compose 里用卷覆盖。

推荐建一个php-custom.ini:

upload_max_filesize = 64M post_max_size = 80M memory_limit = 256M max_execution_time = 300 max_input_vars = 5000

然后挂载:

volumes: - ./php-custom.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro

文件名以zz-开头能保证它在默认配置之后加载,覆盖生效。post_max_size要略大于upload_max_filesize,因为请求体里还有表单字段和其他数据。max_input_vars调到 5000 是为了应对大型菜单或者插件配置页提交字段过多的情况——默认 1000 个,超了之后表单提交只会保存一部分,而且不报错,这种"数据莫名丢失"的问题排查起来非常费劲。

max_execution_time调大主要给插件安装、主题更新、导入内容这些长任务用。注意 Nginx 的proxy_read_timeout也要同步放开到 120 秒以上,否则 PHP 那边还在跑,Nginx 这边已经超时返回 504 了。

6. 上线后最常撞上的几类故障与处理路径

站点跑起来只是开始,接下来几个月里你会遇到的基本都是同一批问题。把这几个排查路径记住,能省下大量时间。

6.1 页面 502 的三层排查链路

502 的含义是入口层连不上后端。不要一上来就重启所有容器,按层往下查效率更高。

第一层,确认容器还在跑:

docker compose ps docker compose logs --tail 50 wordpress

如果容器状态是Restarting,说明它在反复崩溃,日志里通常能看到具体原因,比如数据库连不上、卷权限不对。

第二层,确认端口在监听:

ss -lntp | grep 8080 curl -I http://127.0.0.1:8080

ss看不到 8080 说明端口映射没生效,检查 compose 文件里的ports是否写错、有没有被其他进程占用。curl能通但浏览器 502,问题就在 Nginx 配置上。

第三层,确认 Nginx 侧的转发地址。如果 compose 里 wordpress 服务的端口映射被改过,或者 Nginx 的proxy_pass指向了容器 IP(而不是宿主机回环地址),容器重建后 IP 变化就会导致连接失败。用容器 IP 做上游几乎必然会在某次重建后出问题,这也是我坚持用宿主机端口映射的原因。

6.2 权限错配引发的连锁反应

"无法建立目录"、"上传失败"、"插件需要 FTP 凭据",这三个报错背后是同一个原因:容器内的 www-data(UID 33)对wp-content目录没有写权限。

先用容器内的身份验证一下:

docker compose exec wordpress id docker compose exec wordpress ls -ln /var/www/html/wp-content docker compose exec wordpress touch /var/www/html/wp-content/test.txt

最后一条命令如果报 Permission denied,问题就确认了。修复方式在 3.3 节已经说过,宿主机chown -R 33:33。这里补一个新装插件之后权限又乱掉的情况:某些插件会自己创建目录,Docker 在 bind mount 上创建目录时默认用 root,之后 WordPress 就写不进去了。解决办法是给wp-content目录加 group 粘滞位:

chmod -R g+w /opt/wordpress/wp-content find /opt/wordpress/wp-content -type d -exec chmod g+s {} \;

g+s让新建的子目录继承父目录的属组,新文件自动归属于 33 组,权限就不会一路烂掉。

注意:不要图省事直接chmod -R 777。这在共享主机或者多站点环境下会带来安全隐患,而且并不是所有场景下都能生效(SELinux 拦截时改权限也没用,得改上下文)。

6.3 磁盘被镜像层和日志撑满

容器化部署最常见的一次"事故"就是磁盘满。表现很杂:数据库报写入失败、docker compose up卡住不动、SSH 登录之后命令都没法执行。

先看 Docker 占了多少:

docker system df -v du -sh /var/lib/docker/* | sort -hr | head

docker system df -v会列出每个镜像、容器、卷、构建缓存的大小。常见的两个大户:一是构建缓存,如果这台机器上还在做镜像构建,Build Cache会不断累积;二是容器日志,如果没有按 2.3 节配置log-opts,单个日志文件可以到几十 GB。

清理动作按风险从低到高:

docker image prune -f # 清理悬空镜像 docker builder prune -f # 清理构建缓存 docker system prune -a -f # 慎用,会删掉所有未使用的镜像

docker system prune -a之前务必docker compose ps确认没有容器在用那些镜像,虽然有保护机制,但重建镜像需要时间和网络。

对 WordPress 站点来说,还有一个隐藏的磁盘消耗点在数据库的wp_options表。某些插件会往autoload字段写大量数据,随着时间推移这张表膨胀到几百 MB,不但占空间,每次请求都要加载一遍,直接拖慢整站。定期检查:

SELECT SUM(LENGTH(option_value)) AS total FROM wp_options WHERE autoload='yes';

超过 1 MB 就该看看是哪个插件在乱写了,用SELECT option_name, LENGTH(option_value) FROM wp_options WHERE autoload='yes' ORDER BY 2 DESC LIMIT 20;排序找出罪魁祸首。

6.4 备份与迁移的可复现做法

容器化之后,备份这件事反而清楚了:需要备份的只有三样东西——数据库、wp-content目录、以及 compose 文件和 Nginx 配置。

数据库备份:

docker compose exec -T db sh -c \ 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --default-character-set=utf8mb4 wordpress' \ > /backup/wp-$(date +%F).sql

--single-transaction保证导出的一致性,不用锁表,线上站点备份时不会短暂不可用。-T参数关掉伪终端分配,否则重定向出来的 SQL 文件开头会混入奇怪的字符,恢复时报语法错误。

文件备份和迁移:

docker run --rm -v wordpress_wp_data:/data -v /backup:/backup alpine \ tar czf /backup/wp-data-$(date +%F).tar.gz -C /data .

注意卷名前面的项目前缀。Compose 默认给卷加项目名前缀,实际名字是目录名_卷名,用docker volume ls确认一下再执行。

迁移到新机器的完整流程是:新机器装好 Docker、准备/opt/wordpress目录、把 compose 文件和.env拷过去、docker compose up -d让容器初始化、docker compose down、删除新生成的卷、把旧机器的卷解压进去、修改数据库里的站点地址(如果域名变了),最后docker compose up -d。听起来绕,但比重新走一遍安装向导要可靠得多,尤其是站点里有几十个插件和几千篇文章的时候。

域名变更那一步用一条 SQL 就能搞定:

UPDATE wp_options SET option_value = REPLACE(option_value, 'http://old.example.com', 'https://new.example.com') WHERE option_name IN ('siteurl','home'); UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://old.example.com', 'https://new.example.com');

只改wp_options里那两条是不够的,历史文章正文里硬编码的旧域名图片地址会全部失效,所以还要扫一遍wp_posts。改完记得清理缓存,否则前台看起来没变化。

我在几台不同配置的机器上跑过这套流程,体会最深的一点是:容器化解决的是环境一致性问题,它并不能自动帮你避开 CentOS 7 这台老机器本身的限制。内核、SELinux、磁盘、系统源这些东西该处理还是得处理,只不过处理完之后,后面无论换机器还是在测试环境重现,都只需要一份 compose 文件和两个备份包。真正花时间的从来不是敲命令,而是搞清楚为什么第一次跑不出预期结果——把前面那几节的检查项当成固定动作做一遍,后续的麻烦会少很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询