「Docker生产最佳实践」系列第②篇,聚焦存储驱动深度配置与网络架构规划。
很多人在安装完 Docker 后就直接投入使用,默认的存储驱动和网络配置完全没动。运行一段时间后,各种问题开始浮现:容器 IO 偶尔会异常飙升、磁盘空间莫名其妙被占满、不同服务的容器之间居然能互相 ping 通……其实,大多数这类问题都可以在最初部署时,通过合理的配置提前规避。
存储:overlay2 不是装上就完了
磁盘配额——防止单个容器把磁盘吃光
我曾遇到过一个案例:某个容器因故障疯狂写入日志,短短几小时就把宿主一块 500GB 的磁盘空间全部写满。仅仅依赖日志轮转工具限制单个日志文件的大小有时还不够,更稳妥的做法是在存储驱动层直接为每个容器设置磁盘配额。
前提条件:Docker 数据目录在 xfs 文件系统上
# 检查文件系统类型
df -Th /var/lib/docker
# 如果是 xfs,可以继续配置配额
启用 quota 的挂载方式:
需要在系统启动时自动挂载的配置文件中,为 Docker 的数据目录添加配额选项。
# /etc/fstab 中添加 pquota 挂载选项
/dev/sdb1 /var/lib/docker xfs defaults,pquota 0 0
daemon.json 中配置每个容器的磁盘上限:
这是全局配置,对所有新建的容器生效。
{
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true",
"overlay2.size=10G"
]
}
配置后,每个容器的可写层最多只能使用 10GB 空间。这样一来,即使某个容器失控,其影响范围也被严格限制在自身,不会波及宿主或其他容器。
也可以针对单个容器设置:
在启动特定容器时,可以覆盖全局配置,为其指定独立的磁盘限额。
docker run --storage-opt size=5G nginx
定期清理——自动化,别靠记忆
Docker 的磁盘空间问题通常有两个来源:一是运行中容器不断产生的日志和应用数据;二是随着时间积累下来的“垃圾”,比如已停止的容器、悬空的镜像、未被使用的数据卷。
依赖手动清理不仅效率低下,而且容易遗忘。最佳实践是编写一个自动化清理脚本,并交给 crontab 定时执行。
#!/bin/bash
# /usr/local/bin/docker-cleanup.sh
# 清理3天前停止的容器
docker container prune -f --filter "until=72h"
# 清理7天前未使用的镜像
docker image prune -a -f --filter "until=168h"
# 清理7天前未使用的卷(小心,确认数据无用再启用此条)
docker volume prune -f --filter "until=168h"
# 清理构建缓存
docker builder prune -a -f --filter "until=168h"
echo "[$(date)] Docker cleanup done" >> /var/log/docker-cleanup.log
# 将脚本添加到 crontab,设定每天凌晨3点执行
0 3 * * * /usr/local/bin/docker-cleanup.sh
快速查看 Docker 当前占用多少磁盘:
docker system df
# 输出中的 RECLAIMABLE 列,就是可以安全回收的空间大小
网络:别全扔在默认 bridge 上
为什么默认网络不适合生产?
Docker 自带的 bridge 网络(名为 bridge 的默认网络)在生产环境存在两个明显的硬伤:
- 容器间通信只能使用 IP 地址,而 IP 是动态分配的,容器一旦重启就可能发生变化。
- 没有内置 DNS 服务,容器之间无法通过容器名称进行解析。
因此,生产环境强烈推荐使用 自定义 bridge 网络,它不仅能解决上述问题,还能实现容器组间的网络隔离,这是做好 运维 和安全的基础。
我的网络分层方案
一个清晰的生产架构通常会按功能进行网络分层。这里分享一个典型的三层网络划分方案:
# 前端网络(用于 Nginx 等接入层服务)
docker network create \
--driver bridge \
--subnet 172.80.1.0/24 \
--gateway 172.80.1.1 \
--opt com.docker.network.bridge.name=docker-frontend \
frontend-net
# 后端网络(用于应用服务内部通信)
docker network create \
--driver bridge \
--subnet 172.80.2.0/24 \
--gateway 172.80.2.1 \
--opt com.docker.network.bridge.name=docker-backend \
backend-net
# 数据库网络(内部隔离,禁止访问外网)
docker network create \
--driver bridge \
--subnet 172.80.3.0/24 \
--gateway 172.80.3.1 \
--internal \
database-net
其网络隔离和数据流向可以这样理解:
[外网流量]
↓
Nginx (连接至 frontend-net)
↓
应用服务 (同时连接 frontend-net 和 backend-net)
↓
MySQL (连接至 database-net, 且拥有 --internal 属性)
关键在于,标记为 --internal 的 database-net 完全禁止容器访问外部网络。这意味着即使数据库容器因漏洞被攻破,攻击者也无法直接从该容器发起对外网络请求,天然形成了一道安全屏障。这种精细化的 网络 规划是 云原生 应用安全的重要一环。
MTU 问题——大数据包丢失排查
典型现象: 容器内进行小请求测试一切正常,但传输大文件时就会出现问题,表现为连接超时或间歇性丢包。
这通常是MTU(最大传输单元)不匹配导致的。在使用了 VPN、或云服务商提供的 overlay 隧道网络的宿主机环境中尤其常见。
# 首先查看宿主机物理网卡的 MTU 值
ip link show eth0
# 输出中会显示类似 `mtu 1500` 的信息
# 如果宿主机存在VPN或隧道,有效MTU可能需要减去封装开销,例如变为1450
# 创建 Docker 网络时,明确指定适配的 MTU
docker network create \
--driver bridge \
--opt com.docker.network.driver.mtu=1450 \
my-net
MTU 的选取原则通常是:比宿主机实际有效 MTU(考虑隧道开销后)稍小一些,比如小 50 字节,为各种协议封装预留空间。
端口规划——别让端口成为运维噩梦
当多个项目或服务运行在同一台宿主机上时,端口冲突会成为令运维人员头疼的问题。提前做好端口范围规划能有效避免混乱。
| 端口范围 |
用途 |
示例 |
| 8000-8099 |
Web应用 |
8080: Nginx, 8081: 前端服务 |
| 8100-8199 |
API服务 |
8100: API Gateway |
| 8200-8299 |
中间件 |
8200: Redis, 8201: 消息队列 |
| 8300-8399 |
数据库 |
8300: MySQL, 8301: PostgreSQL |
| 8400-8499 |
监控工具 |
8400: Prometheus, 8401: Grafana |
更优的方案是:在生产环境中,容器一律不向宿主机暴露端口,统一通过 Nginx 或 API 网关进行反向代理。
# 容器启动时不使用 -p 参数暴露端口
docker run -d --name app --network backend-net myapp
# 由宿主机上的 Nginx 统一做反向代理和 SSL 终止
# 好处:端口管理集中,SSL证书只需配置一次,容器可以随时迁移或重启而不影响外部访问
本篇小结
- 存储:采用 overlay2 驱动,在 XFS 文件系统上启用配额以限制单容器磁盘用量,并通过定时任务自动清理无用资源。
- 网络:弃用默认 bridge 网络,按照前端、后端、数据库等逻辑层次创建自定义网络,在 VPN/隧道环境下务必注意 MTU 配置。
- 端口:要么提前做好全局端口规划,更推荐的做法是让容器不直接暴露端口,统一经由反向代理对外提供服务。
这些配置在部署初期花费一些时间完成,却能帮你避开后续无数次的故障排查和“救火”工作。希望这篇来自实践的经验总结能对你有所帮助。
本文是「Docker生产最佳实践」系列第②篇。下一篇我们将探讨资源限制——CPU、内存、磁盘IO 该如何配置,才能避免一个失控的容器拖垮整台服务器。
如果你在配置过程中有其他心得或疑问,欢迎到技术社区交流探讨。