Docker Compose发布端口后,你写的那条ufw规则一次都没被执行过

Docker Compose发布端口后,你写的那条ufw规则一次都没被执行过
张文保 更新 30 分钟阅读 3,734 阅读
本文目录
  1. 防火墙规则条条生效,为什么端口还是全世界可连?
  2. ufw的规则挂在哪条链上
  3. 发往容器的包不是发给本机的
  4. 为什么这个坑格外阴
  5. 三条命令,自己验一遍
  6. 这个洞是新出现的,还是一直都在?
  7. 一张开了十年的单
  8. 十年里社区的应对方式
  9. Docker Engine 28修好了吗?
  10. 28版实际改的默认值
  11. 28版明确没改的部分
  12. 28版的两个回退开关
  13. 端口该怎么发布才不越过防火墙?
  14. 把发布地址钉死在回环
  15. 往DOCKER-USER链里写规则
  16. 用ufw-docker把两套规则并轨
  17. 干脆不发布端口
  18. 四种做法怎么选
  19. 顺手把网络也拆开
  20. compose.yaml没写的字段,Docker替你填了什么值?
  21. expose写了等于没写
  22. restart的四个档位差在重启之后
  23. 默认不写restart等于不重启
  24. 与其背默认值,不如让工具打印出来
  25. 容器都起来了,Nginx为什么还在回502?
  26. depends_on的“ready”不是你理解的ready
  27. healthcheck加条件依赖才是真等待
  28. 健康检查要检到业务层
  29. 磁盘是怎么被容器日志悄悄写满的?
  30. json-file驱动出厂没有上限
  31. 磁盘满了之后先坏的不是容器
  32. 在daemon层设上限,而不是逐个服务设
  33. 数据卷的属主为什么改不动?
  34. 命名卷的初始化只发生一次
  35. 三条可用的出路
  36. 这些默认值最后是怎么变成SEO事故的?
  37. 自建服务被抓进索引
  38. 健康检查缺失导致的502会被记账
  39. 日志爆盘的连锁反应最贵
  40. 上线前照着这份清单过一遍
  41. 网络与暴露面
  42. 启动与依赖
  43. 日志与数据
  44. 常见问题解答
  45. 把Docker升级到28以上,是不是就不用管ufw的事了?
  46. 直接在daemon.json里设置iptables为false,能不能一劳永逸?
  47. 只在compose里写expose不写ports,端口会暴露到公网吗?
  48. depends_on写了服务名,为什么应用还是连不上数据库?
  49. 容器日志已经把磁盘写满了,现在怎么救?
  50. compose文件里的version字段还需要写吗?
  51. 权威参考资料
摘要:你在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》把这件事说得毫不含糊,原话是:Docker和ufw使用防火墙规则的方式让两者互不兼容;当你用Docker发布容器端口时,流量在到达ufw的设置之前就已经被转走了。

官方文档的措辞值得琢磨——它用的是incompatible(互不兼容),不是bug。也就是说这不会被“修”,它就是设计。

为什么这个坑格外阴

常见的配错,你多少能从某个地方看出端倪:服务起不来、日志报错、端口连不上。而这个坑的所有症状都是反的——一切正常,只是安全边界不存在。

更麻烦的是它的验证方式有陷阱。很多人在服务器上curl 127.0.0.1:9000测一下,通了,觉得正常;再curl一下公网IP,也通,觉得“大概是我放行过吧”。两次测试都从本机发起,两次都绕开了真正的判据。唯一有效的验证是从另一台机器打过来,或者干脆用在线端口扫描服务从外网扫一遍。

如果你还没做过服务器层面的端口盘点,可以先照着ufw防火墙的端口放行与云安全组配置那套流程把基线摸清楚,再回来处理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的问题单,开单时间是2015年6月19日。它描述的是一个相邻但不同的场景:局域网里的其他机器,可以绕过端口发布限制直接访问容器。

这张单子活得比很多创业公司都久。它一直挂到2025年,Docker Engine 28发布时才被正式收口。

十年里社区的应对方式

在官方默认值改动之前,社区拿出的方案基本是三类:一类是关掉Docker的iptables自动管理("iptables": false),代价是容器的出站网络也一起废掉;一类是往DOCKER-USER链里手写规则;还有一类是专门的适配脚本。

chaifeng/ufw-docker这个专门解决该冲突的项目属于第三类,它的做法是往ufw的after.rules里插一段自定义链,把Docker的转发流量重新拉回ufw的判据下。这个项目的README第一句话就是“修复Docker和UFW的安全缺陷而不必禁用iptables”,标题本身已经说明这个问题存在了多久、有多普遍。

Docker Engine 28修好了吗?

修了,但修的不是你以为的那个。这一节值得慢慢读,因为2025年之后写的大量文章在这里给出了错误的安心感。

28版实际改的默认值

Docker Engine v28默认收紧容器网络的官方公告发布于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章节对短语法的定义是: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虚拟网络隔离那篇的底层拆解。

四种做法怎么选

做法改动成本宿主机重启后需要的纪律适合谁
回环绑定改一行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把独立站服务器运维自动化那套定时任务里,让机器自己每天确认一遍该活的都活着。

与其背默认值,不如让工具打印出来

上面这些默认值还会继续变——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和抓取预算的双重影响那篇讲得比较细。

磁盘是怎么被容器日志悄悄写满的?

这一条排在“端口暴露”之后,是保哥见过的第二高频容器事故。

json-file驱动出厂没有上限

Docker默认的日志驱动是json-file,它把容器stdout和stderr的每一行都写进宿主机的一个JSON文件里。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管好服务器日志,两套机制的边界正好互补。

数据卷的属主为什么改不动?

这个坑不常见,但一旦踩上,排查时间通常以小时计。

命名卷的初始化只发生一次

当你把一个命名卷第一次挂到容器的某个目录上,而这个卷是空的,Docker会把镜像里该目录的内容连同属主和权限一起复制进卷里。这是一次性行为——卷一旦非空,后续挂载不再复制任何东西

于是这个场景就成立了:你把镜像从node:18升到node:22,新镜像里应用目录的属主UID从1000变成了1001,但卷里保存的还是1000。容器起来之后写文件报权限拒绝,你去镜像里查,UID明明是对的。

三条可用的出路

第一条是显式指定运行用户,user: "1000:1000",让容器去迁就卷而不是反过来。第二条是在宿主机上直接chown卷目录,命名卷的实际路径在/var/lib/docker/volumes/卷名/_data。第三条最干脆——把卷删了重建,前提是里面的数据可以重新生成或者你已经备份过。

备份这件事在容器场景下比在传统部署下更容易被忽略,因为“数据在卷里”这个说法听起来就像已经安全了。实际的备份策略可以照着rsync增量备份与快照的服务器实操那套来做,把卷目录纳进去就行。

这些默认值最后是怎么变成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容器化部署一起看,那篇讲的是怎么搭起来,这篇讲的是搭起来之后哪几个默认值会咬人。

上线前照着这份清单过一遍

把前面所有判据压成一份可以直接执行的自查表。每一条都能在两分钟内验证完。

网络与暴露面

  • 用另一台机器扫一遍公网IP的全端口,而不是在本机curl
  • compose里每一条ports都确认过是否真的需要对外,需要对外的走Nginx。
  • 所有内部服务的ports加上回环地址前缀,或者干脆删掉ports只留网络内互访。
  • 如果用了DOCKER-USER规则,验证过宿主机重启之后规则还在。

启动与依赖

  • 每个服务都写了restart,且区分清楚alwaysunless-stopped
  • 有状态依赖的服务写了condition: service_healthy,不是光写服务名。
  • 数据库类容器的健康检查带start_period,值不小于首次初始化时间。
  • 健康检查探到业务层,不是只探端口。

日志与数据

  • daemon.json里配了max-sizemax-file,且已经force-recreate过一次。
  • 命名卷的实际磁盘占用有人在看,不是等告警。
  • 卷目录进了备份范围,且做过一次真实的恢复演练。
  • 镜像升级前确认过运行用户UID有没有变。

最后补一条不属于任何分类、但优先级最高的:这份清单本身要有人定期跑。容器的问题很少在部署当天暴露,它们更喜欢在某次不相干的重启之后,安安静静地出现。宿主机层面的加固基线,可以对照SSH密钥登录与fail2ban的服务器加固实操一并做掉,两层放在一起才算完整。

常见问题解答

把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-sizemax-file,再docker compose up -d --force-recreate让新配置对现有容器生效。这两步顺序反了的话,磁盘会很快再满一次。

compose文件里的version字段还需要写吗?

不需要了。Compose规范已经把version标记为废弃字段,新版docker compose遇到它会打印一条警告。顺带一提,带连字符的旧版docker-compose是Python写的独立工具,已经在2023年7月停止维护,现在应该用作为Docker CLI插件的docker compose

权威参考资料

分享到
标签
版权声明

本文标题:《Docker Compose发布端口后,你写的那条ufw规则一次都没被执行过》

本文链接:https://zhangwenbao.com/docker-compose-published-ports-ufw-bypass-defaults.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交