# 保哥笔记 — Docker与容器 > 本分片含 2 篇文章,按发布日期倒序。全部分片索引见 https://zhangwenbao.com/llms-full.md **站点**:https://zhangwenbao.com/ **分类**:Docker与容器 **生成**:2026-09-10 10:30:06 CST --- ## Docker Compose发布端口后,你写的那条ufw规则一次都没被执行过 - URL:https://zhangwenbao.com/docker-compose-published-ports-ufw-bypass-defaults.html - 分类:Docker与容器 - 发布:2026-07-14 | 更新:2026-07-31 - 摘要:ufw规则拦不住Docker已发布的端口,因为包在nat表就被转走了。讲清机制、Docker 28修与没修的边界、四种收口做法,以及compose五个默认值的坑。 - 关键词:Nginx,索引,服务器加固 > **TLDR**:摘要:你在VPS上敲的ufw deny从来没有拦住过任何一个访问容器的请求,因为那些包根本不走ufw所在的那条链。这不是配错了,是Docker官方文档里白纸黑字写着的“两者互不兼容”。Docker Engine 28确实收紧了默认值,但它收的是另一个洞——已发布端口照旧穿墙而过。下面把包的实际走向、28版修与没修的边界、四种收口做法各自能活多久,以及compose.yaml里另外五个替你做过决定的默认值,一次讲清楚。 > 摘要:你在VPS上敲的ufw deny从来没有拦住过任何一个访问容器的请求,因为那些包根本不走ufw所在的那条链。这不是配错了,是Docker官方文档里白纸黑字写着的“两者互不兼容”。Docker Engine 28确实收紧了默认值,但它收的是另一个洞——已发布端口照旧穿墙而过。下面把包的实际走向、28版修与没修的边界、四种收口做法各自能活多久,以及compose.yaml里另外五个替你做过决定的默认值,一次讲清楚。 先说一个让人不太舒服的事实:如果你在一台VPS上同时装了ufw和Docker,然后用docker compose up -d起了一堆服务,那么你在ufw里写的所有deny规则,对这些服务的已发布端口是完全无效的。不是部分无效,是压根没有被执行到。 更让人不舒服的是,这件事没有任何报错。ufw的status输出漂漂亮亮,规则一条不少,容器跑得好好的,日志干干净净。唯一的异常,是全世界都能连上那个你以为只对本机开放的端口。 保哥去年帮一个做汽车改装件的DTC客户做服务器体检,就是在这里翻的车。他们把一套自建的图片处理服务跟主站放在同一台VPS上,compose文件里写着ports: - "9000:9000",ufw里只放行了22、80、443三个端口。结果扫一遍公网,9000端口对全球开放,里面躺着三万多张还没打水印的产品原图。 ## 防火墙规则条条生效,为什么端口还是全世界可连? 这个问题的答案不在ufw,也不在compose,而在Linux内核处理数据包的顺序上。 ## ufw的规则挂在哪条链上 ufw本质上是iptables的一层封装。你写的ufw allow 443,最终变成filter表INPUT链上的一条规则。INPUT链管的是目的地是本机的包——包走到网络栈,内核发现收件人就是这台机器自己,才交给INPUT链去审。 关键就在这句“目的地是本机”。 ## 发往容器的包不是发给本机的 当你用-p 9000:9000发布一个端口,Docker会在nat表的PREROUTING链上写一条DNAT规则。包一进网卡,还没轮到任何过滤逻辑,目的地址就已经被改写成容器在bridge网络里的内网地址了。 改写之后,内核再去做路由判断:这个包的收件人不是本机,是172.17.0.x。于是它走的不是INPUT链,而是FORWARD链。而ufw默认压根不在FORWARD链上写业务规则。 Docker官方那篇《Packet filtering and firewalls》 (https://docs.docker.com/engine/network/packet-filtering-firewalls/)把这件事说得毫不含糊,原话是:Docker和ufw使用防火墙规则的方式让两者互不兼容;当你用Docker发布容器端口时,流量在到达ufw的设置之前就已经被转走了。 > 官方文档的措辞值得琢磨——它用的是incompatible(互不兼容),不是bug。也就是说这不会被“修”,它就是设计。 ## 为什么这个坑格外阴 常见的配错,你多少能从某个地方看出端倪:服务起不来、日志报错、端口连不上。而这个坑的所有症状都是反的——一切正常,只是安全边界不存在。 更麻烦的是它的验证方式有陷阱。很多人在服务器上curl 127.0.0.1:9000测一下,通了,觉得正常;再curl一下公网IP,也通,觉得“大概是我放行过吧”。两次测试都从本机发起,两次都绕开了真正的判据。唯一有效的验证是从另一台机器打过来,或者干脆用在线端口扫描服务从外网扫一遍。 如果你还没做过服务器层面的端口盘点,可以先照着ufw防火墙的端口放行与云安全组配置 (https://zhangwenbao.com/linux-ufw-firewall-server-port-rules-ssh-cloud-security-group.html)那套流程把基线摸清楚,再回来处理Docker这一层。 ## 三条命令,自己验一遍 这件事不需要相信任何人的转述,十分钟能自己跑完。找一台测试VPS,装好ufw和Docker,然后: # 第一步:把默认策略关死,只放行SSH ufw default deny incoming ufw allow 22/tcp ufw enable # 第二步:起一个发布了端口的容器 docker run -d --name t -p 8080:80 nginx:alpine # 第三步:从另一台机器打过来 curl -m 5 -I http://你的公网IP:8080/ 正常情况下你会拿到一行HTTP/1.1 200 OK。ufw的默认策略是deny,8080没有被放行,然而它通了。 想看清包被谁截走了,再加一条: iptables -t nat -L DOCKER -n --line-numbers 输出里会有一条形如DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80的规则。这条规则在nat表的PREROUTING阶段执行,而ufw的规则在filter表的INPUT阶段——前者比后者早,且改完目的地址之后包就不再属于INPUT的管辖范围了。 顺手记一句:验证完记得docker rm -f t,别把测试容器留在那儿。这类“测完忘了删”的容器,在保哥做过的服务器体检里出现频率高得离谱。 ## 这个洞是新出现的,还是一直都在? 一直都在,而且有据可查。 ## 一张开了十年的单 moby仓库那张标题为local network container access vulnerability的问题单 (https://github.com/moby/moby/issues/14041),开单时间是2015年6月19日。它描述的是一个相邻但不同的场景:局域网里的其他机器,可以绕过端口发布限制直接访问容器。 这张单子活得比很多创业公司都久。它一直挂到2025年,Docker Engine 28发布时才被正式收口。 ## 十年里社区的应对方式 在官方默认值改动之前,社区拿出的方案基本是三类:一类是关掉Docker的iptables自动管理("iptables": false),代价是容器的出站网络也一起废掉;一类是往DOCKER-USER链里手写规则;还有一类是专门的适配脚本。 chaifeng/ufw-docker这个专门解决该冲突的项目 (https://github.com/chaifeng/ufw-docker)属于第三类,它的做法是往ufw的after.rules里插一段自定义链,把Docker的转发流量重新拉回ufw的判据下。这个项目的README第一句话就是“修复Docker和UFW的安全缺陷而不必禁用iptables”,标题本身已经说明这个问题存在了多久、有多普遍。 ## Docker Engine 28修好了吗? 修了,但修的不是你以为的那个。这一节值得慢慢读,因为2025年之后写的大量文章在这里给出了错误的安心感。 ## 28版实际改的默认值 Docker Engine v28默认收紧容器网络的官方公告 (https://www.docker.com/blog/docker-engine-28-hardening-container-networking-by-default/)发布于2025年2月28日。它改的是这样一件事:以前如果宿主机的filter-FORWARD链策略是ACCEPT、并且开了net.ipv4.ip_forward,那么没有发布的容器端口,也能被同一个局域网里的其他机器通过容器IP直接访问到。28版给这类流量加上了显式的DROP。 换句话说,28版堵的是“我压根没发布,你凭什么连得上”这个洞。这确实是2015年那张单描述的场景。 ## 28版明确没改的部分 同一篇公告里有一句话,是整件事的分水岭:已发布的端口将继续照常工作(比如-p 8080:80)。 “照常工作”的意思,就包括照常绕开ufw。28版把未发布端口的意外暴露堵上了,但对已发布端口与主机防火墙的冲突,一个字没动,也不打算动——因为在Docker的设计里,你写-p就是在明确表达“我要把这个端口开出去”的意图。 这就构成了一条很实用的判据: 场景 | Docker 27及以前 | Docker 28及以后 | 未发布端口,局域网直连容器IP | 可能连得上 | 默认被DROP | 已发布端口(-p),ufw拒绝该端口 | 依然可连 | 依然可连 | 已发布端口,绑定到127.0.0.1 | 只有本机可连 | 只有本机可连 | 宿主机自身端口(如SSH) | ufw正常生效 | ufw正常生效 | 表里第二行是这篇文章存在的全部理由。升级到28并不会让你的compose文件变安全一点点。 ## 28版的两个回退开关 如果你的架构确实依赖“从局域网直连容器IP”这个老行为——比如内网监控直接刮容器的指标端口——28版给了两个显式的回退路径。一个是给某个网络单独开口: docker network create -d bridge \ -o com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected \ my_unprotected_net 另一个是在/etc/docker/daemon.json里全局关掉这个DROP策略: { "ip-forward-no-drop": true } 需要提醒的是,第二个开关是全局的,一开就等于回到27版的行为。真要用,也应该优先用第一个,把影响范围锁在单个网络里。 ## 端口该怎么发布才不越过防火墙? 四种做法,按“需要多少纪律才能长期成立”从低到高排。 ## 把发布地址钉死在回环 这是成本最低、也最该成为默认习惯的一条。Compose规范的services章节 (https://github.com/compose-spec/compose-spec/blob/main/05-services.md)对短语法的定义是:HOST部分是可选的[IP:](port | range),如果不设置,它会绑定到所有网络接口,也就是0.0.0.0。 于是这两行的安全含义天差地别: services: imgproxy: ports: - "9000:9000" # 绑0.0.0.0,全世界可连 - "127.0.0.1:9000:9000" # 只有本机可连 绝大多数内部服务——图片处理、队列面板、搜索引擎、数据库——都应该走第二种,然后由Nginx做反向代理对外暴露。这样安全边界重新回到了你熟悉的位置:Nginx配置和ufw,两个你本来就在维护的东西。 ## 往DOCKER-USER链里写规则 Docker专门留了一条名为DOCKER-USER的链给你,它在Docker自己那堆规则之前被求值,是官方认可的插手位置。典型写法是只允许某几个来源访问转发流量: iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP 这条规则的意思是:从eth0进来、源地址不在内网段的转发包,一律丢弃。 它的问题是持久化。DOCKER-USER里的规则能扛住容器重启,扛不住宿主机重启,除非你用iptables-persistent之类的机制把它存下来。保哥见过不止一次的事故形态是:规则写对了,验证也通过了,三个月后机房断电重启,端口重新裸奔,而没有任何人会收到通知。 ## 用ufw-docker把两套规则并轨 前面提到的那个脚本,本质上是把DOCKER-USER的手写规则封装成了ufw风格的命令。装好之后你可以这样写: ufw-docker allow imgproxy 9000 好处是心智模型统一了,团队里其他人不需要知道iptables链的存在。代价是多了一个第三方依赖,Docker升级后偶尔需要重新跑一次同步。 ## 干脆不发布端口 最彻底的一种:内部服务之间通过compose自建的网络用服务名互访,一个端口都不往宿主机发布。Nginx如果也在compose里,它直接用http://imgproxy:9000访问,宿主机上看不到9000这个监听。 这条路的前提是你的反向代理也进了容器。如果Nginx装在宿主机上,那就退回到回环绑定那一档。想理解容器网络这层隔离到底是怎么做出来的,可以看看Linux网络命名空间与veth虚拟网络隔离 (https://zhangwenbao.com/linux-network-namespace-netns-veth-virtual-network-isolation.html)那篇的底层拆解。 ## 四种做法怎么选 做法 | 改动成本 | 宿主机重启后 | 需要的纪律 | 适合谁 | 回环绑定 | 改一行compose | 自动成立 | 记得每次都写 | 所有人的默认选项 | DOCKER-USER手写 | 写iptables规则 | 要显式持久化 | 持久化+文档+交接 | 有专职运维的团队 | ufw-docker脚本 | 装一个第三方工具 | 随ufw规则保留 | Docker升级后复查 | 多人协作、口径要统一 | 完全不发布端口 | 反向代理也要进容器 | 自动成立 | 整套架构一致 | 新项目、能重来一次的 | 表里最值得注意的是第三列。四种做法里只有两种是“配好就不会自己失效”的,另外两种都需要一个人记得某件事——而运维事故的绝大多数,都发生在那个人换岗之后。如果非要挑一条,回环绑定加反向代理是唯一一条既便宜、又不依赖记忆力的路。 ## 顺手把网络也拆开 还有一层常被跳过的收口:让不该互相说话的服务压根不在同一张网里。compose默认给整个项目建一张网,所有服务都在里面,也就是任何一个容器被攻破,其余全部可达。 services: nginx: networks: [frontend] app: networks: [frontend, backend] db: networks: [backend] networks: frontend: backend: internal: true 这样一来nginx碰不到db,而internal: true让backend这张网彻底没有出网能力——数据库容器连不上外网,也就没法被用来往外传数据。这一行的防守价值,比大多数人给它的注意力要高得多。 ## compose.yaml没写的字段,Docker替你填了什么值? 端口只是最扎眼的一个。compose文件的设计哲学是“不写就用合理默认”,问题在于“合理”是对开发环境说的,不是对生产说的。 ## expose写了等于没写 很多教程把expose说成“只对内网开放端口”,听起来像是ports的安全版本。规范里的实际说法要冷淡得多:如果镜像的Dockerfile已经暴露了端口,即便compose文件里不设expose,它对同网络的其他容器也是可见的。 同一个网络里的容器之间,端口本来就是互通的,expose不增加也不减少任何东西,它更接近一份文档注释。真要限制容器之间的访问,得靠拆分网络,让不该说话的两个服务压根不在同一张网里。 ## restart的四个档位差在重启之后 规范里四个值的定义是这样的: 值 | 规范定义 | 宿主机重启后 | no | 默认策略,任何情况下都不重启容器 | 不起来 | on-failure[:n] | 退出码表示出错时重启 | 不起来 | always | 一直重启直到容器被删除 | 起来 | unless-stopped | 不看退出码都重启,但服务被手动停止或删除后不再重启 | 起来,除非你之前手动停过 | 差别集中在最后一列。always的always是字面意义上的always——包括你上周手动停掉它、准备第二天排查问题,结果半夜机房重启,它又自己爬起来了。做灰度或者留了坏容器待查的场景,unless-stopped才是你要的那个。 ## 默认不写restart等于不重启 这条单独拎出来说,是因为它太容易漏。规范写得很清楚:no是默认策略。一个compose文件如果没写restart,服务器一重启,你的服务就是不起来的,而且没有任何东西会提醒你。 把开机自启这件事纳入巡检,比记住写这一行更靠谱。可以顺手接进用cron把独立站服务器运维自动化 (https://zhangwenbao.com/linux-cron-shell-independent-site-automation-ops-backup-sitemap-ssl.html)那套定时任务里,让机器自己每天确认一遍该活的都活着。 ## 与其背默认值,不如让工具打印出来 上面这些默认值还会继续变——Compose规范本身在演进,Docker Engine的版本也在往前走。与其记住某个版本的某个默认值,不如养成一个更省心的习惯: docker compose config 这条命令会把你的compose文件连同所有变量插值、所有继承关系一起展开,打印出Docker实际会用的那份完整配置。你没写的字段,凡是有默认值的都会在这份输出里现形。 更实用的用法是把它接进上线流程:改完compose文件先docker compose config看一眼展开结果,再决定要不要up。.env没读到、变量拼错、覆盖文件顺序反了这几类问题,在这一步全都会暴露出来,而在up之后它们只表现为“服务行为跟预期不一样”。 再补一条相关的:docker compose config --services只列服务名,--images只列镜像。上线前用--images核对一遍镜像标签,能挡住相当一部分“本地跑的是新版、服务器上还是latest缓存”的事故。 ## 容器都起来了,Nginx为什么还在回502? 这是compose第二大坑,也是最容易被误判成“网络问题”的一个。 ## depends_on的“ready”不是你理解的ready 规范里对depends_on的描述用了一个很有迷惑性的词:Compose保证依赖服务先于依赖方启动,并会等待依赖服务“就绪”后再启动依赖方。 问题是这个“就绪”的默认判据是service_started——容器进程起来了就算就绪,跟里面那个服务能不能接请求毫无关系。MySQL容器起来到真正能接受连接,中间通常有十几秒;这十几秒里,你的应用容器已经按照compose的承诺开始跑了,连数据库连不上,直接退出或者进入重试。 规范用ready这个词,而它的默认含义是started,这大概是compose文件里最贵的一处措辞歧义。 ## healthcheck加条件依赖才是真等待 要拿到真正的就绪语义,得同时写两件事——被依赖方的健康检查,和依赖方的条件依赖: services: db: image: mysql:8.4 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 start_period: 40s app: depends_on: db: condition: service_healthy start_period这个参数经常被漏掉,它的作用是给容器一段宽限期:在这段时间内检查失败不计入retries。MySQL首次初始化数据目录可能要三四十秒,没有start_period的话,健康检查会在初始化完成之前就把重试次数耗光,容器被判定为unhealthy。 ## 健康检查要检到业务层 写健康检查还有个常见的偷懒做法:检查端口通不通。端口通只说明进程在监听,说明不了它能不能处理请求。 对Web服务,检查点应该落到一个真正会走完业务逻辑的轻量路径上,比如一个会读一次数据库的/healthz。对独立站来说这件事还有额外的意义——502不只是用户看不到页面,它同时会被搜索引擎记在账上,这层账怎么算,TTFB与多层缓存对Core Web Vitals和抓取预算的双重影响 (https://zhangwenbao.com/ttfb-multi-layer-cache-core-web-vitals-crawl-budget-seo.html)那篇讲得比较细。 ## 磁盘是怎么被容器日志悄悄写满的? 这一条排在“端口暴露”之后,是保哥见过的第二高频容器事故。 ## json-file驱动出厂没有上限 Docker默认的日志驱动是json-file,它把容器stdout和stderr的每一行都写进宿主机的一个JSON文件里。json-file日志驱动的官方参数说明 (https://docs.docker.com/engine/logging/drivers/json-file/)里,max-size的默认值是-1,意思是不限制。 不限制的后果,在一个日志话痨型的应用上会来得很快。一个每秒打十行日志、每行200字节的服务,一天写掉约1.6GB。跑一个月,50GB。而很多入门级VPS的系统盘就是50GB。 ## 磁盘满了之后先坏的不是容器 磁盘写满时,最先出事的往往是MySQL和Nginx,而不是那个话痨容器。MySQL无法写binlog直接拒绝写入,Nginx无法写access log开始返回错误。你在告警里看到的是“数据库挂了”,真凶在另一个目录。 排查入口很固定,一条命令能看出量级: du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail -5 ## 在daemon层设上限,而不是逐个服务设 compose里可以给单个服务配logging,但更省事的是在/etc/docker/daemon.json里一次设全局默认: { "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "3" } } 要注意这个设置只对之后新建的容器生效,已经在跑的容器需要重建才会应用。改完记得docker compose up -d --force-recreate过一遍,否则你只是给未来买了保险,当下的炸弹还在。系统层面的日志治理思路可以参考用logrotate与journald管好服务器日志 (https://zhangwenbao.com/linux-server-log-management-logrotate-journald-analysis-alerting.html),两套机制的边界正好互补。 ## 数据卷的属主为什么改不动? 这个坑不常见,但一旦踩上,排查时间通常以小时计。 ## 命名卷的初始化只发生一次 当你把一个命名卷第一次挂到容器的某个目录上,而这个卷是空的,Docker会把镜像里该目录的内容连同属主和权限一起复制进卷里。这是一次性行为——卷一旦非空,后续挂载不再复制任何东西。 于是这个场景就成立了:你把镜像从node:18升到node:22,新镜像里应用目录的属主UID从1000变成了1001,但卷里保存的还是1000。容器起来之后写文件报权限拒绝,你去镜像里查,UID明明是对的。 ## 三条可用的出路 第一条是显式指定运行用户,user: "1000:1000",让容器去迁就卷而不是反过来。第二条是在宿主机上直接chown卷目录,命名卷的实际路径在/var/lib/docker/volumes/卷名/_data。第三条最干脆——把卷删了重建,前提是里面的数据可以重新生成或者你已经备份过。 备份这件事在容器场景下比在传统部署下更容易被忽略,因为“数据在卷里”这个说法听起来就像已经安全了。实际的备份策略可以照着rsync增量备份与快照的服务器实操 (https://zhangwenbao.com/linux-server-rsync-incremental-backup-snapshot-link-dest-offsite-restore.html)那套来做,把卷目录纳进去就行。 ## 这些默认值最后是怎么变成SEO事故的? 把上面几条串起来看,它们通向的不只是运维故障,还有一条更安静的损失路径。 ## 自建服务被抓进索引 绑在0.0.0.0上的服务,只要有一个页面返回HTML,就存在被抓取和收录的可能。图片处理服务的调试页、队列监控面板、搜索引擎的管理界面——这些页面的共同点是没有noindex,没有robots规则,也没人想过要给它们加。 它们进索引的后果分两层:一层是暴露基础设施细节,另一层更麻烦——这些页面会稀释站点在搜索引擎眼里的主题聚焦度,还会占用本来该给商品页的抓取额度。 回到开头那个汽车改装件的客户。当时的处理顺序是这样的:先把9000端口从0.0.0.0改到回环、重建容器,再用站长工具查有没有被收录过的IP:9000形式的地址——查出来七条,全是图片处理服务的调试页。然后对这七条做移除请求,同时在Nginx层对来自IP直连的请求统一返回444。三周后复查,收录归零。 值得记下来的是这件事的时间成本分布:改配置花了十分钟,清理已经进索引的痕迹花了三周。这个比例在这类事故里相当典型,也是为什么“上线前扫一遍端口”这种听起来很无聊的动作值得被写进流程。 ## 健康检查缺失导致的502会被记账 依赖顺序没配好导致的间歇性502,用户可能刷新一下就过去了,爬虫不会。连续的5xx会让抓取速率下调,而恢复是缓慢的、不对称的——掉下去很快,涨回来要按周算。 ## 日志爆盘的连锁反应最贵 磁盘写满导致的全站不可用,在监控里往往表现为一段完整的空白。这种故障的特点是同时命中可用性、抓取、和用户体验三项。真正让人肉疼的是,起因只是某个compose文件少写了四行logging配置。 如果你的站点跑在容器里且用WordPress,环境一致性和数据持久化那部分可以对照WordPress独立站的Docker容器化部署 (https://zhangwenbao.com/wordpress-docker-containerized-deployment-environment-consistency.html)一起看,那篇讲的是怎么搭起来,这篇讲的是搭起来之后哪几个默认值会咬人。 ## 上线前照着这份清单过一遍 把前面所有判据压成一份可以直接执行的自查表。每一条都能在两分钟内验证完。 ## 网络与暴露面 - 用另一台机器扫一遍公网IP的全端口,而不是在本机curl。 - compose里每一条ports都确认过是否真的需要对外,需要对外的走Nginx。 - 所有内部服务的ports加上回环地址前缀,或者干脆删掉ports只留网络内互访。 - 如果用了DOCKER-USER规则,验证过宿主机重启之后规则还在。 ## 启动与依赖 - 每个服务都写了restart,且区分清楚always和unless-stopped。 - 有状态依赖的服务写了condition: service_healthy,不是光写服务名。 - 数据库类容器的健康检查带start_period,值不小于首次初始化时间。 - 健康检查探到业务层,不是只探端口。 ## 日志与数据 - daemon.json里配了max-size和max-file,且已经force-recreate过一次。 - 命名卷的实际磁盘占用有人在看,不是等告警。 - 卷目录进了备份范围,且做过一次真实的恢复演练。 - 镜像升级前确认过运行用户UID有没有变。 最后补一条不属于任何分类、但优先级最高的:这份清单本身要有人定期跑。容器的问题很少在部署当天暴露,它们更喜欢在某次不相干的重启之后,安安静静地出现。宿主机层面的加固基线,可以对照SSH密钥登录与fail2ban的服务器加固实操 (https://zhangwenbao.com/linux-server-ssh-login-hardening-key-auth-sudo-fail2ban-brute-force-protection.html)一并做掉,两层放在一起才算完整。 ## 常见问题解答 ## 把Docker升级到28以上,是不是就不用管ufw的事了? 不是。28版堵的是未发布端口被局域网直连的洞,官方公告里明确写了已发布端口继续照常工作。只要你还在compose里写ports并绑到0.0.0.0,ufw对那个端口依然没有发言权。升级值得做,但它解决不了这篇文章讨论的主要问题。 ## 直接在daemon.json里设置iptables为false,能不能一劳永逸? 能让ufw重新说了算,但代价大到通常不划算。关掉之后Docker不再维护NAT规则,容器的出站访问、容器之间的通信、端口发布全部需要你自己写iptables来实现。除非你有很强的网络自定义需求并且团队里有人长期维护这套规则,否则更推荐回环绑定加反向代理这条路。 ## 只在compose里写expose不写ports,端口会暴露到公网吗? 不会。expose不做任何端口映射,宿主机上不会出现监听。但也别把它当成安全措施——同一个Docker网络里的容器之间本来就能互相访问端口,写不写expose都一样,它的实际作用接近一行注释。真要隔离,得靠拆网络。 ## depends_on写了服务名,为什么应用还是连不上数据库? 因为短语法的默认条件是service_started,容器进程起来就算满足,跟数据库能不能接受连接是两件事。要真正等待,需要给数据库服务写healthcheck,并在应用服务里用长语法写condition: service_healthy。另外记得配start_period,否则首次初始化期间的失败会把重试次数提前耗光。 ## 容器日志已经把磁盘写满了,现在怎么救? 先用du定位到最大的那个*-json.log,然后用truncate -s 0把它清零——不要直接rm,文件句柄还被进程持有,删了空间不会释放。清完立刻去daemon.json里配上max-size和max-file,再docker compose up -d --force-recreate让新配置对现有容器生效。这两步顺序反了的话,磁盘会很快再满一次。 ## compose文件里的version字段还需要写吗? 不需要了。Compose规范已经把version标记为废弃字段,新版docker compose遇到它会打印一条警告。顺带一提,带连字符的旧版docker-compose是Python写的独立工具,已经在2023年7月停止维护,现在应该用作为Docker CLI插件的docker compose。 ## 权威参考资料 ## WordPress独立站怎么用Docker部署?环境一致、数据持久化和零差异迁移讲透 - URL:https://zhangwenbao.com/wordpress-docker-containerized-deployment-environment-consistency.html - 分类:Docker与容器 - 发布:2026-03-18 | 更新:2026-03-18 - 摘要:本地能跑、线上就崩,换主机又装不上扩展——独立站的环境噩梦,Docker能根治。这篇用大白话讲清容器化部署WordPress:从镜像、数据卷,到本地与生产怎么做到完全一致、怎么零差异搬家,并附5个常见坑。 - 关键词:WordPress,独立站,独立站运营 > **TLDR**:摘要:保哥这几年帮人看独立站,遇到最多的一类崩法,几乎都长一个样——“我本地明明好好的,一传到服务器就白屏”,再不就是换个主机,一堆扩展装不上、版本对不齐,折腾一整天站还起不来。这些病的根子是同一个:你的开发环境和生产环境根本不是一套东西,全靠手工对齐,对到哪算哪。这篇不堆术语,用大白话把Docker这件事讲给做独立站的人听:容器和镜像到底是什么、一份docker-compose怎么把WordPress和数据库一条命令拉起来、容器删了数据怎么才不丢、本地和线上怎么做到一模一样、搬家怎么做到零差异,最后容器化对SEO和性能到底有没有影响。一句话:Docker不是让你的站变快的魔法,而是把“环境”这件最容易翻车、又最说不清的事,变成一份能复制、能版本管理、在哪台机器跑都一样的标准件。 > 摘要:保哥这几年帮人看独立站,遇到最多的一类崩法,几乎都长一个样——“我本地明明好好的,一传到服务器就白屏”,再不就是换个主机,一堆扩展装不上、版本对不齐,折腾一整天站还起不来。这些病的根子是同一个:你的开发环境和生产环境根本不是一套东西,全靠手工对齐,对到哪算哪。 这篇不堆术语,用大白话把Docker这件事讲给做独立站的人听:容器和镜像到底是什么、一份docker-compose怎么把WordPress和数据库一条命令拉起来、容器删了数据怎么才不丢、本地和线上怎么做到一模一样、搬家怎么做到零差异,最后容器化对SEO和性能到底有没有影响。一句话:Docker不是让你的站变快的魔法,而是把“环境”这件最容易翻车、又最说不清的事,变成一份能复制、能版本管理、在哪台机器跑都一样的标准件。 ## 为什么独立站搞来搞去,总栽在“环境不一致”上? 先说个保哥几乎每周都会碰到的求助。一个独立站主在本地电脑上把站调得好好的,主题改完、插件装齐、页面预览完美,兴冲冲传到服务器,刷新一看——白屏,或者满屏报错。他第一反应是“我代码没问题啊,本地明明好好的”。这句话,保哥听过不下几百遍。 问题恰恰就出在这句“本地好好的”上。你的本地电脑和你的服务器,根本不是同一套环境。本地可能是PHP 8.2,服务器还是7.4;本地装了某个图像处理扩展,服务器没有;本地的数据库版本、内存配置、文件路径、时区设置,跟线上处处不一样。代码是同一份代码,但脚下的地基完全是两块,地基对不齐,房子当然塌。 更糟的是换主机。独立站主常因为速度、价格、合规原因要搬家,一搬就是一场灾难:新服务器要重新装PHP、装扩展、配Nginx、调权限、导数据库、改一堆路径和配置,任何一步漏了或版本对不上,站就起不来。保哥在网站迁移为什么总掉流量 (https://zhangwenbao.com/site-migration-seo-no-traffic-loss-complete-guide.html)那篇里讲过迁移的SEO风险,而这些风险里有一大半,其实是“环境没搬齐”引发的连锁故障。 还有一类更隐蔽的折磨——多个站挤在一台服务器上互相打架。这个站要PHP 8.1,那个站老主题只能跑7.4;这个站的插件要装某个系统库,结果跟另一个站的依赖冲突。保哥在一台主机绑多个独立站点 (https://zhangwenbao.com/using-htaccess-to-bind-a-virtual-host-to-multiple-independent-websites.html)那篇里讲过用目录和配置去隔离,但在传统裸机模式下,运行时层面的隔离始终别扭,一动牵全身。 把这些病摆到一起看,会发现它们是同一个根:环境是手工攒出来的,散落在系统各处,没法整体复制、没法版本管理、没法保证两台机器完全一样。你每搭一次环境,都是在凭记忆和文档手工对齐,对到哪算哪,漏一项就埋一颗雷。Docker要解决的,正是这个最底层、又最容易被忽视的麻烦。 ## Docker到底解决了独立站的什么问题? 把术语都丢一边,保哥用一个比方讲Docker是什么。传统部署,像你搬家到一间毛坯房,得自己一样样置办——通水电、装家具、布网线,每搬一次重来一次,还总有几样忘了买。Docker则是把你的整个房间,连家具带水电带装修,整体打包成一个标准集装箱,吊到哪块地上一放,里面一切原样,立刻能住。 这个“集装箱”就是容器。它把你的应用,连同它运行需要的一切——特定版本的PHP、需要的扩展、各种系统库、配置文件——全都封装在一个隔离的盒子里。这个盒子在你的笔记本上跑,和在云服务器上跑,里面的环境分毫不差,因为它装的根本就是同一套东西。Docker官方在Docker官方文档 — What is Docker?(讲清容器与镜像的概念,以及容器如何把应用和它依赖的整套环境隔离打包) (https://docs.docker.com/get-started/docker-overview/)里把这件事讲得很直白:容器通过把应用与底层基础设施隔离,让你能像管理代码一样去管理运行环境。 那容器从哪来?来自镜像。镜像是容器的“模板”或者说“出厂设置”——一个只读的打包文件,写明了这个盒子里该装什么、怎么装。你拿一个镜像,就能启动出一个或一百个一模一样的容器。镜像可以版本化、可以分发,你和你的同事、你的本地和线上,拉的是同一个镜像,得到的就是同一个环境。“同一个镜像”这五个字,就是环境一致的全部秘密。 它和传统在服务器上一个个装环境的区别,关键在三点。一是可复现:环境怎么搭的,全写在镜像构建文件里,是代码、能进版本库、能一键重建,而不是某个人脑子里的隐性知识。二是隔离:每个站、每个服务待在自己的容器里,PHP版本、扩展、依赖互不干扰,这个站要8.2那个站要7.4,各跑各的井水不犯河水。 三是即弃:容器轻量、起停极快,搞砸了删掉重建几秒钟的事,不像传统环境装乱了得重装系统。也正因为它和动辄占好几个G、启动要等半天的虚拟机比起来轻得多、快得多,你才能在一台机器上痛快地跑很多个容器。保哥在PHP版本选型那篇22周账本 (https://zhangwenbao.com/php-version-decision-22-weeks-wordpress-magento-cross-cms.html)里头疼的多版本共存问题,在容器世界里几乎是免费的——每个站锁死自己那版PHP,装进各自镜像,谁也碰不到谁。 ## 一个WordPress独立站用Docker,到底怎么跑起来? 概念讲透了,看看实际怎么落地。一个WordPress站最少需要两样东西:WordPress本体(PHP程序)和一个数据库(通常是MySQL或MariaDB)。在Docker世界里,这就是两个容器:一个跑WordPress,一个跑数据库,它们俩组队干活。 怎么把这俩容器组织到一起、还让它们能互相通信?答案是 Docker Compose——用一个叫docker-compose.yml的文本文件,把你这个站要用到的所有容器、它们的配置、它们之间的关系,一次性写清楚。写好之后,一条命令就能把整组容器拉起来。 Docker官方在Docker官方文档 — Why use Compose?(用一个YAML文件定义多容器应用,官方着重强调它的跨环境一致与可移植) (https://docs.docker.com/compose/intro/features-uses/)里专门讲了Compose的价值:把多容器应用的定义沉淀成一个文件,跨环境搬运时一致又省心。 一份最小可用的WordPress compose文件,骨架大概长这样(保哥精简过,方便理解结构): services: db: image: mysql:8.0 volumes: - db_data:/var/lib/mysql # 数据库数据,挂到命名卷 environment: MYSQL_DATABASE: wp MYSQL_USER: wpuser MYSQL_PASSWORD: 改成强密码 MYSQL_ROOT_PASSWORD: 也改成强密码 wordpress: image: wordpress:php8.2-apache depends_on: - db ports: - "8080:80" # 本地用 8080 访问,生产改由反代接管 volumes: - wp_content:/var/www/html/wp-content # 主题/插件/上传,挂出来 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: wpuser WORDPRESS_DB_PASSWORD: 改成强密码 WORDPRESS_DB_NAME: wp volumes: db_data: wp_content: 不用怕看不懂,保哥把要点拆开说。services下面定义了两个容器:db用官方MySQL镜像,wordpress用官方WordPress镜像(这里顺手把PHP版本锁成了8.2)。两个容器自动在同一个内部网络里,所以WordPress能直接用db这个名字找到数据库,不用记IP。 environment是环境变量,数据库名、用户、密码这些都从这里注入,不写死进镜像——这是后面讲“本地线上一致”的关键伏笔。ports把容器的端口映射到宿主机,本地开发时浏览器访问8080就能看到站。 volumes把要长期保存的数据挂出来,这是下一节的重头戏。Docker Hub上的 Docker Hub — WordPress官方镜像(页面给出了用docker compose同时跑WordPress与MySQL两个容器的完整示例) (https://hub.docker.com/_/wordpress) 给的就是这套结构的官方完整版,可以对照着学。 写好这个文件,在它所在目录敲一条docker compose up,Docker会自动去拉镜像、按你写的配置把两个容器创建好、连上网络、启动起来。几十秒后,一个完整的WordPress站就在本地跑起来了。整个过程没有手工装PHP、没有手工配数据库、没有改一堆系统文件——所有该做的,都浓缩进了这一个文件里。这就是“环境即代码”的第一次直观体验。 ## 容器一删数据就没了,WordPress的数据库和图片怎么保住? 这一节是整篇里最不能含糊的地方,因为它是新手翻车最惨烈的一关。前面说容器“即弃”是优点,但它有个吓人的另一面:容器内部产生的数据,容器一删就跟着灰飞烟灭。你想象一下,辛辛苦苦发了几百篇文章、传了一堆图,结果某天重建了一下容器,全没了——这种事真发生过,根子就是数据没挂出来。 解决办法叫数据持久化,机制就是前面compose文件里出现过的volumes(数据卷)。它的作用,是把容器里那些需要长期保存的目录,绑定到容器之外——要么是Docker管理的命名卷,要么是宿主机上的某个真实目录。这样数据就活在容器的生命周期之外,容器删了重建,数据原封不动。Docker官方在Docker官方文档 — Volumes(官方明确:数据卷是容器在生成和使用数据时做持久化的首选机制) (https://docs.docker.com/engine/storage/volumes/)里写得很清楚:数据卷是容器做数据持久化的首选机制。 对一个WordPress站,有两块数据是命根子,必须挂出来。第一块是数据库的数据目录。你的所有文章、页面、评论、用户、站点设置,全都存在数据库里,对应MySQL容器里的 /var/lib/mysql。这个目录不挂卷,数据库容器一重建,你的全部内容瞬间归零。 第二块是 WordPress的wp-content目录,尤其是里面的uploads——你上传的每一张产品图、每一个媒体文件都在这。主题和插件也住在wp-content里。这块不挂出来,重建容器后图全裂、主题没了。把这两块挂对,你的站才算真正“站稳了”,之后随便折腾容器都不慌。 这里保哥要敲黑板提醒一个最常见的误解:挂了卷,不等于做了备份。数据卷保证的是“数据不随容器消失”,但它还是躺在同一块硬盘上——硬盘坏了、机房出事、你手滑误删了卷,数据照样没。所以容器化之后,该有的备份一样不能少:定期导出数据库、打包wp-content、最好再往异地存一份。保哥在迁移保稳那篇 (https://zhangwenbao.com/site-migration-seo-no-traffic-loss-complete-guide.html)里强调过数据完整性,放到容器场景下,规矩没变——卷负责“在线不丢”,备份负责“出事能救”,两件事,缺一不可。 ## 本地开发和线上生产,怎么做到一模一样不再“我这能跑”? 绕了一圈,回到最初那个病——“本地好好的,线上就崩”。现在有了镜像和compose,保哥告诉你怎么彻底根治它。核心原则就一句话:本地和生产,跑同一个镜像,用同一份compose结构,差异只通过环境变量来表达。 什么意思?你本地开发用的WordPress镜像,锁的是php8.2-apache;那生产服务器上,拉的也必须是同一个php8.2-apache,一个字都不差。这样一来,本地能跑的,线上没有任何理由跑不了,因为脚底下踩的是同一块地基。过去那种“本地8.2线上7.4”的版本错位,从机制上就被掐死了。镜像锁版本,是消灭环境漂移的第一道闸。 那本地和生产总有不一样的地方吧?当然有——数据库密码不同、域名不同、调试开关不同、要不要开缓存不同。这些差异全部收口到环境变量和配置文件里,绝不写死进镜像。本地用一份 .env,生产用另一份 .env,镜像和compose主体完全共用。这正是业界推崇的做法:把会变的配置从代码里剥出来,交给环境去注入,代码(镜像)本身保持各处一致。 这套打法带来的,是开发流程的一次质变。新人入职,不再是照着一份过时的文档手工搭半天环境,而是拉下代码、一条命令起站,五分钟进入开发。几个人的本地环境天然一致,再不会有“你那能跑我这报错”的扯皮。改动在本地这个跟线上同构的环境里验证过,推上去翻车的概率断崖式下降。 更进一步,你可以用同一套镜像,搭一个跟生产几乎一模一样的预发布环境。保哥在预发布环境怎么压测才能防SEO翻车 (https://zhangwenbao.com/staging-environment-seo-stress-test-pre-launch.html)那篇里反复强调,测试环境越接近生产,测出来的结论才越可信。在传统模式下“接近生产”是句空话,环境永远对不齐;而在容器模式下,“跟生产一致的staging”第一次成了开箱即得的能力——同一个镜像换一份环境变量而已。改版前在这种环境里把性能、兼容、SEO该测的都测一遍,上线才真正有底。 ## 用Docker迁移和备份独立站,真能做到零差异搬家吗? 独立站主迟早要面对搬家:换更快的主机、换更便宜的云、为了合规换地区。传统搬家有多痛前面说过了——重装环境、重配服务、对一堆版本,处处是雷。容器化之后,这件事的难度被砍掉一大截,接近“零差异”。 原理很简单:你的站现在由两部分组成——一是描述环境的镜像与compose文件,二是装着数据的数据卷。前者是代码、本来就在版本库里;后者打包导出(数据库做一份dump、wp-content打个包)即可。搬家就变成:在新服务器装好Docker,把compose文件拷过去,把数据导入新卷,一条docker compose up——站就在新家原样起来了。 为什么能做到环境零差异?因为你搬过去的镜像,和老服务器上跑的是同一个,PHP版本、扩展、配置完全一致,新机器上压根不存在“装漏了、版本对不上”的空间。传统迁移里最折磨人、最容易出错的那一整块——环境重建——直接被跳过了。你只需要保证数据迁移完整、域名和反代配置接好,剩下的环境一致是镜像白送的。 保哥在网站迁移掉流量那篇 (https://zhangwenbao.com/site-migration-seo-no-traffic-loss-complete-guide.html)里拆过迁移的六个SEO风险维度,里头有相当一部分故障,本质是环境差异导致的页面行为变化(比如某个扩展没装,导致图片处理或伪静态规则失灵,URL或页面悄悄变了)。容器化把环境这个变量锁死之后,你迁移时要盯的,就从“千头万绪的环境加数据加配置”,收敛成了主要看数据和DNS/反代,出错面小了一大圈,掉流量的概率自然跟着降。 备份的逻辑也顺势变清爽。要恢复一个容器化的站,你需要的就是那份compose、那份 .env、以及数据卷的备份,三样齐了就能在任何一台装了Docker的机器上把站完整还原出来。比起传统那种“备份了文件和库,但环境得重搭、还未必搭得回原样”的尴尬,容器化的备份恢复,确定性强了不止一个量级。当然,前一节那句话得再念一遍——卷不等于备份,异地多存一份的纪律,到哪都不能丢。 ## 容器化对独立站的SEO和性能,到底有没有影响? 这是独立站主最关心、也最容易被忽悠的一块,保哥把话说透。先泼冷水:Docker不会直接让你的网站变快。容器是有那么一丁点额外开销的,指望装上容器速度就起飞,纯属误会。任何说“容器化提升SEO”的话术,要么是话没说全,要么是想卖你东西。 但容器化对SEO有实实在在的间接价值,核心是两个词:可控、可复现。性能优化最怕的是什么?是你今天调好了,明天换个环境又打回原形;是staging上测得飞快,上了生产却慢,因为两边环境不是一回事。容器化把环境钉死之后,你的每一项性能优化都能稳定复现,压测数据可信,改版不会因为环境漂移把好不容易调好的速度搞丢——而页面体验本就是排名因素,守住它就是守住SEO的地基。 具体到落地,容器化的生产架构通常会在WordPress容器前面,再架一层反向代理(比如Nginx或Traefik容器)。这一层统一负责HTTPS证书、gzip/Brotli压缩、静态资源缓存、把流量分发给后面的站。它和你做不做容器其实是两件事,但容器化让这层代理也变成一个标准、可复用、可版本管理的部件,配一次到处用,HTTPS和缓存这些直接影响SEO的设置不再每台机器手工抠。 还有一个常被独立站主担心的点,保哥直接给你吃定心丸:容器化对搜索引擎是完全透明的。Googlebot来抓你的站,看到的就是反向代理正常吐出来的那份HTML,跟任何一个普通站没有半点区别。爬虫不知道、也根本不在乎你后端是裸机还是容器、是单机还是集群。容器是部署方式,SEO看的是最终吐给用户和爬虫的页面,两者井水不犯河水,不存在“用了Docker被降权”这种无稽之谈。 最后一个实在的好处是资源隔离。一台服务器上跑好几个站时,传统模式下一个站被刷流量或插件失控吃满CPU,很容易把整机拖垮、连累其它站一起慢、一起影响SEO。容器可以给每个站设资源上限,一个站出问题被限在自己的盒子里,不至于殃及池鱼。对靠多个站吃饭的独立站主来说,这种“故障不串门”的隔离,本身就是一种稳定性保险。 ## 独立站上Docker,最容易踩的5个坑是什么? 方向对了,执行时还有几个坑特别容易栽。保哥把帮人擦屁股擦得最多的5个列出来,对照自查,能帮你少熬好几个通宵。 坑一:数据不挂卷,一删丢库。这是头号惨案,前面专门讲过还是要再列一次,因为太多人栽。数据库目录和wp-content没挂数据卷,容器一重建数据全没。上线前务必确认这两块卷挂对了、还真的能在容器删除重建后数据犹在——亲手测一遍,别想当然。 坑二:进容器里手动改东西,重建就没了。有人习惯了传统模式,直接docker exec进容器里改配置、装东西、打补丁,改完是当下生效了,可这些改动没写进Dockerfile或compose,纯属“在即弃的盒子里裸改”。下次镜像更新、容器重建,你的改动一笔勾销,还查不出为啥又变回去了。规矩是:任何环境改动,都要落到构建文件里,让它能被复现,这才不辜负容器化的初衷。 坑三:拿开发的compose直接上生产。本地图方便的配置——数据库端口暴露在外、弱密码、调试模式开着、没有反代和HTTPS——原样推上线,等于把大门敞开请人进来。生产必须另做加固:数据库不对外暴露端口,强密码走环境变量,前面架反代管HTTPS,关掉调试。开发和生产用两套compose(或同主体加不同 .env),别混。 坑四:镜像用latest,版本悄悄漂移。图省事不写具体版本号,镜像标签写成latest,听着是“永远最新”,实则是个定时炸弹——今天重建拉到的和上个月拉到的可能是不同版本,你以为锁死了环境,其实它在背后偷偷变。正确做法是把镜像版本号写死(像前面例子里的mysql:8.0、wordpress:php8.2-apache),要升级是你主动、可控地改版本号,而不是被动地被latest牵着走。环境一致的承诺,全靠这一点维系。 坑五:以为上了Docker就一劳永逸,疏于维护。容器化让部署变省心,但不等于扔着不管。基础镜像会有安全更新、WordPress和插件要打补丁、数据卷要定期备份、资源用量要盯着。把容器当“装好就不用碰”的黑盒,迟早在安全或容量上吃大亏。容器化是让维护更有章法,不是免除维护——定期更新镜像、验证备份能恢复、复查配置,这些功课一样都不能少。 ## 常见问题解答 ## 用Docker部署独立站,是不是必须很懂命令行和服务器? 不需要精通,但完全不碰命令行确实不行,得有个基础门槛。做独立站用Docker,你真正要会的就那么几样:登录服务器的基本操作、几条Docker命令(起停容器、看日志、进容器)、能看懂并改一份docker-compose文件。网上现成的WordPress镜像和compose模板一大把,照着改改数据库密码、域名、路径就能跑起来。真正的难点不在敲命令,而在理解几个概念——什么是镜像、什么是容器、数据为什么要单独挂出来。这几个想通了,命令只是手指头的事。保哥建议的学法是:先在自己电脑上用Docker Desktop把一个WordPress跑起来,反复删了重建,亲手体会一遍“容器即弃、数据靠卷保住”,比看十篇教程都管用。 ## 我的站已经在普通主机上跑得好好的,值得折腾成Docker吗? 看你的处境,别为了容器化而容器化。如果你就一个站、流量平稳、也很少换环境、自己又不太懂技术,那它在哪跑得好好的就别动,Docker的好处你大半享受不到,还平添一层维护负担,这种情况保哥是劝退的。但有几种处境,容器化的价值会立刻显现:一是你手上有好几个站挤在一台服务器上,互相抢资源、PHP版本还各有各的要求;二是你经常要搬家、换主机、或者在本地开发再传上线,每次都被环境差异折磨;三是有团队协作,几个人的开发环境老对不齐;四是你需要一个跟线上一模一样的测试环境来验证改动。只要中了其中一条,把环境标准化带来的省心,就远远盖过学习和迁移的成本了。判断标准很简单:你被“环境”这件事坑的次数越多,越该上Docker。 ## 容器化之后网站会变快吗,对SEO有没有直接好处? 要老实说:Docker本身不会让你的网站变快,甚至还有一点点微乎其微的额外开销。指望装上Docker速度就起飞,方向就错了。它对SEO的好处是间接的、但很实在——核心是“可控”和“可复现”。环境标准化之后,你能搭一个跟生产一模一样的预发布环境去做性能压测和改版验证,测出来的速度数字才算数;你做的任何性能优化,结果都能稳定复现,不会今天好了明天又莫名其妙打回原形;迁移、扩容时环境零差异,不会因为搬家把好不容易调好的速度和配置搞丢,间接守住了页面体验这个排名因素。所以别指望Docker直接提分,但它能帮你把性能这件事管得住、守得稳。 ## 那种cPanel的共享虚拟主机,能用Docker吗? 基本不能,这是先要泼的一盆冷水。共享虚拟主机(就是几十上百个站挤在一台机器、通过cPanel这类面板管理的那种)通常不给你root权限,也不允许你自己装Docker,因为Docker需要相当底层的系统能力,主机商出于隔离和安全考虑不会开放。所以如果你现在用的是这类便宜共享主机,想上Docker,得先升级到能给你完整控制权的环境——最常见的就是VPS或者云服务器(比如各家云厂商的轻量应用服务器、云主机),花几十块钱一个月起,你拿到root权限,自己装Docker才谈得上后面这一切。换句话说,Docker这条路适合的是“自己当服务器管理员”的独立站主,不适合还停留在共享主机阶段、连SSH都没开的场景。再决定要不要打这套打法。 ## 容器删了、服务器重启,我的文章和图片会不会丢? 只要你把数据正确地挂了出来,就不会丢;绝大多数“数据没了”的惨案,根子都是没挂数据卷。容器本身是即弃的,你在容器内部产生的一切,容器一删就跟着没了。所以两块东西必须单独挂到宿主机或命名数据卷上——一是数据库的数据目录(你的文章、评论、设置全在数据库里),二是WordPress的wp-content/uploads(你上传的所有图片和媒体文件)。这两块挂对了,你尽管删容器、重启服务器、升级镜像,数据稳稳地待在卷里,重建容器一挂回去,站还是原来那个站。还要补一句:挂了卷不等于做了备份——卷只是“不随容器消失”,硬盘坏了、误删了照样没。该有的定期备份(导出数据库、打包uploads、最好异地存一份)一样不能省,容器化不替代备份。 ## 用Docker跑生产站,安全吗,会不会更容易被黑? 容器化用对了反而是安全上的加分项,用错了才危险,关键在于别把开发用的那套直接搬上生产。先说加分的一面:容器之间有一定的隔离,一个站被攻破,不那么容易直接波及同机的其它容器和宿主机。但危险也很真实,最大的坑是把本地开发图方便的配置原样推上线——数据库端口直接暴露在公网、用着example之类的弱密码、调试模式还开着、管理后台没任何防护。生产环境必须另做加固:数据库容器绝不对外暴露端口,只在内部网络里让WordPress访问;所有密码走环境变量且足够强;前面架反向代理统一管HTTPS;及时更新基础镜像打安全补丁;再叠上你本来就该做的WordPress层防护。把这几条落实了,Docker跑生产站不仅安全,还比一锅乱炖在裸机上更清爽可控。 ## 权威参考资料