服务器入坑指北:docker 配置优化
docker 装好之后,多数人的第一反应是把容器跑起来——能跑就行。真正让人半夜爬起来的是另外几件事:重启一次 docker,没写重启策略的容器一个都没回来;日志不设上限把系统盘写满,docker 自己都起不来;compose 里几个资源键摆在一起,改哪个、改成多少心里没底;容器端口明明没在防火墙放行,外部却能连上。
麻烦不在单个参数上,而在动手的次序:重启策略、日志上限、内存边界这些底线先钉死,运行时与内核参数这类进阶项放到后面。次序一旦颠倒,出问题时你就得在同一晚里既救火、又回头补配置。
本篇按配置的层次走一遍:先跑起来的最小配置 → 日志与磁盘 → 升级与重启 → 网络与防火墙 → 资源限制 → 验证清单。每条给出可以直接抄的片段和验证方式;文中数值都是随手举的示例,按自己机器的内存与磁盘调整。
先跑起来:一组最小可用的配置
这五项属于"不改会出事、改了几乎没有副作用"的类型,先落到 /etc/docker/daemon.json 里就能解决大部分常见问题:
{
"log-driver": "json-file",
"log-opts": {"compress": "true", "max-file": "4", "max-size": "20m"},
"live-restore": true,
"registry-mirrors": ["https://docker.1ms.run"],
"firewall-backend": "iptables"
}
改完让守护进程重新加载,重启会中断服务,放在维护窗口做,并且先备份:
[ -f /etc/docker/daemon.json ] && cp /etc/docker/daemon.json /etc/docker/daemon.json.bak
systemctl restart docker
剩下那些会改变运行行为的开关(运行时、cgroup 驱动、代理转发、镜像存储、实验特性、拉取并发、关闭超时)都放到后面的「可选增强项」里逐项讲——分两次改,出问题时也更容易定位是哪一项。
日志怎么别把盘写满?
把 docker 主机搞挂的高频原因就是这个:默认驱动 json-file 不设上限就一直涨,直到把 /var/lib/docker 所在的盘写满,之后 docker 连新容器都起不来。三个键一起理解:max-size 管单个日志文件多大,max-file 管保留几个归档,compress 决定归档压不压缩。示例里 4 × 20m:单容器日志上限在 80MB 量级,压缩让归档在磁盘上更小;这两件事都发生在日志轮转阶段,通过"写满一个文件就归档、超出份数就丢弃"的机制生效。
要紧的是这条:log-opts 只对新建容器生效。已经在跑的容器不会自动套用,需要重建才应用;临时想放宽某个容器,可以在 docker run 时用 --log-opt 单独指定。
docker info | grep -i 'logging driver'
docker inspect <容器名> --format '{{.HostConfig.LogConfig}}' # 看得到 MaxSize/MaxFile 才算生效
du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail -5 # 谁在涨,一眼看到
取舍:上限越小越省盘,排障线索也丢得越快;压缩省空间,代价是排查时多一步解压。按"值得保留多久的排障窗口"定,别照抄。
docker 升级时服务会不会断?
不打开 live-restore 就会断。它的机制是:守护进程重启或升级时,正在运行的容器不被停止,新守护进程重新接管它们,所以升级阶段业务不中断。官方说明见 Docker 文档的 live-restore 页面。
边界比想象中多,三条都要记住:
- 与 Swarm 互斥:启用后执行
docker swarm init会直接报错--live-restore daemon configuration is incompatible with swarm mode。要用 Swarm 就先关掉它。 - 只保证同一
YY.MM.x补丁线内的跨守护进程升级可用。跨大版本或跳版本升级后,守护进程可能无法重新接管仍在运行的容器,需要手动停掉再拉起——升级窗口里要预留这一步。 - 只对 standalone 容器有效,对 swarm service 无效;把它当成编排层的可用性方案会落空。
怎么确认它真的起作用:看重启后容器是否持续在跑,而不是重启完立刻取一次结果(那时容器可能还没就绪)。
docker info | grep -i 'live restore'
systemctl restart docker # 维护窗口执行;未受 live-restore 保护的容器会短暂中断
sleep 60 && docker ps
docker inspect <容器名> --format '{{.State.StartedAt}}' # 起始时间早于本次重启,说明容器没被重建
镜像拉不动、拉得慢怎么办?
配镜像源。机制很直接:拉取发生在下载阶段,docker 会优先从 registry-mirrors 里的地址下载层,距离更近、链路更短,速度就更稳;写多个源时它会自动挑可用者。
docker info | grep -A3 'Registry Mirrors' # 确认已经生效
docker pull <镜像> # 再拉一次,确认走的是源
当前常见的是毫秒镜像,域名 docker.1ms.run。要留一句:镜像源地址与免费层策略都会变(例如晚高峰可能限速),这不是写死就不管的配置,用之前自行核对当前可用性。拉取失败时按这个顺序排查:源是否可达 → 镜像名与标签是否正确 → 是否需要登录私有仓库 → 最后再看守护进程日志 journalctl -u docker -n 50。取舍:它解决拉取速度,代价是引入一个外部依赖;关键节点的镜像同时保留原始 registry 兜底。
docker 端口绕开了防火墙,这是怎么回事?
真实存在的高频坑。机制是:docker 发布端口时会往宿主机写入自己的转发与地址转换规则,这些规则与主机防火墙工具(ufw / firewalld)管理的规则集各管各的,于是出现"防火墙里没放行、外部却能连上"的现象。绝大多数"服务不该暴露却暴露了"的事故都出在这里。
处理方式,从优先级高的往下:
- 只发布到本机:需要公网访问时,让容器端口绑到
127.0.0.1,再由宿主上的反向代理对外提供服务;不需要公网访问的服务,压根不要发布端口。 - 在来源侧收敛:管理端口与内部服务的来源,限定为固定出口 IP 或运维网段,不做任意来源放通。
- 在
DOCKER-USER链里加自定义放行或拦截:这条链在 docker 自己的规则之前被处理,是留给自己加规则的地方;不要直接去改 docker 管理的链,它们会被重建。
firewall-backend这项保持默认的"iptables"。nftables后端是 Docker 29.0.0 才引入的实验特性,官方明确警告其配置项、行为与实现都可能变化;此外 overlay 网络的规则尚未从 iptables 迁移、Swarm 模式下不能启用、从 iptables 切过去会删除大部分 iptables 链(DOCKER-USER的 jump 残留会让规则行为与预期不一致),而且 nftables 后端下 docker 不再自行开启 IP 转发,缺少转发时守护进程会报错。firewall-backend的默认值就是iptables,取值为iptables或nftables。所以现阶段继续用iptables,把 nftables 记成观察项。
验证"端口到底有没有对外暴露",要到另一台机器上试,不要在本机自测:
docker info # 全量输出,配合下面这条人工比对后端
cat /etc/docker/daemon.json # 确认 firewall-backend 现值
iptables -S DOCKER-USER | head # 自定义规则的落点在这条链
nft list ruleset | head # 只读观察;宿主未安装 nftables 时该命令不可用
可选增强项:想再稳一点、再省一点再加
下面每项都能独立加,示例值各不相同、随手举例,按自己的机器改。加之前想清楚它改变了哪个阶段的行为,加之后按对应的方法验证。
换 crun:影响的是启动阶段
crun 是 C 实现的容器启动器,runc 是 Go 实现的。两者都是启动器:创建容器、配置好 cgroup 与 namespace 之后就退出,容器运行期间并没有它们的进程。所以这个选择的差别只在启动阶段——二进制更小、拉起更快、启动瞬间的内存开销更低。
也就是说,它的意义集中在频繁创建容器的场景:CI 流水线、按需拉起、对冷启动敏感的服务。反过来要明确一句:容器运行起来之后的内存占用与运行期开销,不由这个选择决定。
crun 不是 Docker 自带的,必须先用发行版的包管理器装上。顺序不能颠倒:先装包 → 用 command -v crun 确认真实路径 → 最后才改配置;配置里的路径填实际输出,不要照抄示例。
# Debian / Ubuntu
apt install -y crun
# RHEL / Fedora
dnf install -y crun
command -v crun # 输出必须与 runtimes.crun.path 一致
docker info | grep -i 'default runtime'
{
"default-runtime": "crun",
"runtimes": {"crun": {"path": "<command -v crun 的输出>"}}
}
两处必须同时成立,缺一处守护进程就起不来;最常见的翻车方式是只改了 default-runtime 没装包,systemctl restart docker 直接失败、docker 命令整体不可用,同机其它依赖 docker 的服务一起受影响。回滚:删掉这两项,或把 default-runtime 改回 "runc",恢复备份后重启守护进程。
统一 cgroup 驱动:影响的是资源记账
让 docker 用它自带的 cgroupfs 驱动,会让 docker 与 systemd 各自操作 cgroup 层级,形成双重管理:容器资源不被 systemd 记账,systemctl status、systemd-cgtop、systemd 的资源核算与 OOM 处置都看不到容器;内存紧张时两套管理还会互相打架,cgroup v2 的单一层级下更明显。改成 systemd 驱动后,容器成为 systemd scope,记账、限制、排障的口径才一致——这也是它真正优化的地方。
{"exec-opts": ["native.cgroupdriver=systemd"]}
先判断本机是哪一代 cgroup,再决定怎么改:
stat -fc %T /sys/fs/cgroup # 输出 cgroup2fs 即 v2;输出 tmpfs 即 v1
docker info | grep -i cgroup # 驱动应为 systemd,版本与上面结论一致
如果是 cgroup v1,留意 systemd 是否启用了统一层级;两代上的行为差异要在本机确认后再上线。判断标准:systemd-cgtop 里能看到你的容器。
关掉 docker-proxy:影响的是连接转发路径
"userland-proxy": false 之后,已发布端口的转发交给内核规则处理,不再由每个端口背后的 docker-proxy 进程接住再转——路径上少了一个代理环节,也少了一批监听进程。附带的好处是外部访问容器时,服务里看到的来源地址更接近真实客户端。
{"userland-proxy": false}
代价是这条回环路径不是所有人都用不到:部分应用(或容器内访问宿主地址的场景)依赖它,关掉之后会失败。所以验证要求比别的键高——必须用真实访问验证,不要只看 docker ps 显示端口已映射:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker exec <容器名> curl -sS -o /dev/null -w '%{http_code}\n' http://<宿主地址>:<已发布端口>/
对了才算成:容器内访问宿主已发布端口能通、外部访问正常、服务日志里看到的来源地址符合预期。
镜像存储开关:磁盘层面差别最大的一项
{"features": {"containerd-snapshotter": false}} 关掉的是 containerd 镜像存储。它的机制值得说清楚,因为磁盘账正好和直觉相反:
- 开启后,压缩过的 blob 存在 containerd 的 content store 里,解压后的层数据又由 snapshotter 另存一份——同一个镜像层在磁盘上大约占两份;
- 传统的 overlay2 图形驱动只保留解压后的那一份。
所以开启的收益在拉取侧:压缩 blob 能在多个镜像、多个平台之间共享去重,拉取更快、更省流量——这对 k8s 节点、镜像密集、频繁拉取的环境很值;对个人服务器是净亏:付出接近翻倍的层占用,换来用不上的拉取优化。
结论:个人服务器上关掉它,镜像层只存一份,磁盘占用接近减半。该重新评估的场景是:要做多平台构建、有多个镜像共享大量基础层、或者这台机器将来要接进 k8s。边界也要记住:日后启用时,旧数据不会自动迁移或清理,得自己处理。
docker system df # 对比镜像与层占用
docker system df -v # 看单个镜像的分层占用
其余几个开关:按需打开
"experimental": false:实验特性总开关,默认关着;只有当你要跟踪某个实验项(例如上面讨论的网络后端)时才打开。机制是"开了才有对应选项",所以用完记得关回去,别让它长期停在不稳定分支上。"max-concurrent-downloads": 4:限制并行拉取的层数,作用在拉取阶段——并发越高,磁盘 IO 与带宽的瞬时冲击越大。调低更稳但首次部署慢,调高更快但抖动大,先小后大。"shutdown-timeout": 20:守护进程停止时等待容器优雅退出的秒数,作用在停止/重启阶段——调短能缩短重启窗口,但太短会直接硬杀容器,业务丢正在进行中的请求。改之前确认应用能在几秒内响应停止信号。"max-concurrent-downloads"与"shutdown-timeout"的现值都可以用docker info | grep -iE 'downloads|shutdown'复核。
compose 片段:一份能直接用的服务定义
下面是一份可用的服务骨架,除资源限制外,把几个"不写就容易出事"的项也带上了:
services:
app:
image: <你的镜像:标签>
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./data:/app/data
environment:
TZ: Asia/Shanghai
init: true
mem_limit: <按容器峰值定,例如 768m>
memswap_limit: <等于 mem_limit 即为该容器禁用 swap,例如 768m>
shm_size: <例如 512m;默认 64m>
logging:
driver: json-file
options:
max-size: "15m"
max-file: "5"
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
blkio_config:
weight: <10–1000 之间,例如 300>
device_read_iops:
- path: <宿主块设备,例如 /dev/nvme1n1>
rate: <例如 800>
device_write_iops:
- path: <宿主块设备,例如 /dev/nvme1n1>
rate: <例如 500>
device_read_bps:
- path: <宿主块设备,例如 /dev/nvme1n1>
rate: '<例如 15mb>'
device_write_bps:
- path: <宿主块设备,例如 /dev/nvme1n1>
rate: '<例如 10mb>'
几点说明:
healthcheck的test要换成镜像里真实存在的命令(curl、wget、nc都可能没装,缺了会让容器一直显示 unhealthy);它让"服务进程还在、接口已经不通"这种状态能被docker ps看出来。logging是给这个服务单独钉住日志上限,与daemon.json里的默认值不冲突。ports写成127.0.0.1:8080:8080,公网入口交给宿主上的反向代理,这样"端口对外暴露"就不会悄悄发生。restart: unless-stopped让容器在崩溃或宿主重启后自己回来,同时保留"手动停掉的不自动拉起"的语义;volumes把数据放在容器之外,重建容器不丢数据;environment里的TZ让日志时间与本地一致(容器默认常是 UTC,排查时容易看错 8 小时)。
compose 里那几个资源键到底管什么?
mem_limit / memswap_limit:防的是整机被拖死。 机制是 cgroup 的内存限制:不给上限时,一个内存泄漏的容器能吃掉整机内存,轻则整机疯狂换页、所有服务卡顿,重则触发 OOM 把别的进程一起杀掉。mem_limit 按容器实际峰值留余量,别按宿主总内存的一半去拍。memswap_limit 是常见的写错点——它是**“内存 + swap"的总量**,不是"swap 的大小”;当它等于 mem_limit 时,等价于给这个容器禁用 swap,避免它在内存压力下被换页拖慢。设置前先确认宿主有 swap:
swapon --show
另外 mem_limit 属于旧版 Compose 的顶层键,docker compose 仍然支持;Compose Specification 的现行写法是 deploy.resources.limits.memory。两者不要同时写,否则容易互相顶掉;docker stack deploy(Swarm)不认 mem_limit,要用 --compatibility 或直接写 deploy 段。
init: true:防的是服务假死。 容器里的 PID 1 是你的应用时,回收孤儿进程、转发信号这两件事都得它自己做,很多应用写不好——结果是僵尸进程越堆越多、docker stop 发出去的信号没人接,表现就是"容器还在跑、服务不响应"。开启后注入一个轻量 init(docker-init)当 PID 1,把这两件事接管过去。
shm_size:防的是共享内存不够直接崩。 它决定 /dev/shm 这个内存文件系统的大小,默认只有 64MB;数据库、浏览器内核、部分 JVM 与机器学习负载都要用共享内存,超过默认值就报错,而且报错信息往往不直接指向 /dev/shm,特别容易查偏。示例给了 512m,按该负载文档里的要求定。
blkio_config:防的是一个容器把磁盘 IO 抢光。 weight 是相对权重,取值 10–1000,默认 500;device_read_bps / device_write_bps 用 path(宿主块设备)+ rate(字符串,如 15mb);device_read_iops / device_write_iops 用 path + 整数 rate。字段定义见 Docker 文档的 Compose 服务参考。不做会怎样:一个容器做备份或构建镜像,就能把共享小盘占满,其它容器一起卡住,表现是"机器没满载但所有服务都很慢"。
必须实测的边界:它依赖宿主块设备路径真实存在、且内核 blkcg 支持。在 cgroup v2 与部分云盘 / 虚拟化环境下,按设备限速可能不生效或不被支持——配置写得对,不代表限制真的生效。测不出效果时,用 weight 做相对分配比按设备限速更实际。验证要在两个阶段各测一次:先在容器内跑读写基线,套用限制后再跑一遍,比较差值;容器内没有 fio 时不用为测速去改镜像,在宿主上、或临时起一个带 fio 的容器挂载同一数据目录,同样能做对比。
常用运维命令
docker system df # 镜像 / 容器 / 卷 / 构建缓存各占多少,先看谁在吃盘
docker system prune -a # 清理所有未使用镜像与构建缓存
docker stats --no-stream # 每个容器实际 CPU / 内存 / IO,一眼看出谁异常
docker inspect <容器名> --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}} {{.HostConfig.ShmSize}}'
docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}'
判断要点:docker system prune -a 清的是所有未被容器使用的镜像,不只是悬空镜像,代价是下次构建要重新拉取全部基础镜像,网络慢的机器上要算好时间;不要顺手执行 docker volume prune,它会删掉已停止容器的数据卷,属于不可逆操作。docker stats 是采样值,看趋势比看单次数字有用。
常见故障怎么处置
镜像拉不动:先 docker info 看镜像源是否生效,再确认镜像名与标签、是否需要登录,最后看守护进程日志——认证失败与网络失败在日志里是两种不同的报错。
日志把盘写满:docker system df 加上 du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail 找出最大的那个;先改 log-opts 并重建容器,应急时可以对单个日志文件执行 truncate -s 0(容器继续写、文件立即归零;代价是这段排障线索没了)。
容器被 OOM 杀掉:确认状态并找原因,然后给它写 mem_limit,或调低应用自身的内存上限——两者留一个明确的边界,别都空着。
docker inspect <容器名> --format '{{.State.OOMKilled}}' # true 即被内存限制杀掉
dmesg | tail # 看内核侧的击杀记录
systemctl status docker; journalctl -u docker -n 50 # 守护进程启动失败的现场
磁盘 IO 被抢:docker stats --no-stream 先定位是谁;给重 IO 的容器加 blkio_config 的 weight 或按设备限速,然后用读写前后对比确认真的生效,而不是只看配置文件。
改了 daemon.json 起不来:用上面的 systemctl status 与 journalctl 看启动错误,再把备份还原回去重启。所以改之前那份备份不是可选项。
端口意外对公网开放:回到前面那节的顺序——先改成只绑 127.0.0.1,再从另一台机器复测。
改完怎么验证:一张清单
| 改了什么 | 判据 |
|---|---|
log-opts |
新建容器 docker inspect 里有 MaxSize/MaxFile;日志目录按份数轮转 |
live-restore |
重启守护进程后等一分钟,容器仍在跑,且 State.StartedAt 早于本次重启 |
registry-mirrors |
docker info 的 Registry Mirrors 列出你的源,拉取速度有变化 |
firewall-backend: iptables |
docker info 与配置现值人工比对一致;另一台机器复测端口暴露面 |
default-runtime: crun |
command -v crun 与配置里的 path 一致;docker info 显示默认运行时为 crun,且启动耗时可比 |
native.cgroupdriver=systemd |
docker info 驱动为 systemd;systemd-cgtop 里能看到容器 |
userland-proxy: false |
容器内能访问宿主已发布端口、外部访问正常、服务看到真实来源地址 |
containerd-snapshotter: false |
docker system df 的镜像占用与 docker system df -v 的分层数据没有出现"两份" |
experimental / 拉取并发 / 关闭超时 |
docker info 显示现值;重启窗口内观察容器退出耗时 |
mem_limit / memswap_limit |
docker inspect 的 HostConfig.Memory 与 MemorySwap 等于设定值 |
shm_size |
docker inspect 的 HostConfig.ShmSize 等于设定值;负载不再报共享内存错误 |
init: true |
容器内 PID 1 是 docker-init;不再堆积僵尸进程 |
blkio_config |
限速前后各做一次读写对比,数值有可测差异 |
healthcheck |
docker ps 的 Status 列出现 (healthy);把服务弄挂后能变成 unhealthy |
先改哪几处
如果只想改最少的地方:给 log-opts 加上限并重建容器(挡住磁盘写满)、给每个容器写 mem_limit 与 shm_size(挡住整机 OOM 和共享内存崩溃)、打开 init: true(挡住服务假死)、把不该公网的端口改成只绑 127.0.0.1(挡住意外暴露)。这四处都不需要停机很久,改完用上面的清单逐条验一遍,机器会明显更可控。增强项等遇到对应问题时再加。
避坑清单
- 改了
default-runtime却没装 crun:守护进程起不来、docker命令整体不可用。先command -v crun再改配置。 - 装了 crun 但
runtimes.crun.path与真实路径不一致:同样起不来。路径用command -v crun的输出。 - 以为换 crun 能省运行期内存:它是启动器,容器创建完就退出,容器运行期间的进程里没有它;差别只在启动阶段。
- 让 docker 用自带的 cgroupfs 驱动、systemd 也在管 cgroup:容器资源不被 systemd 记账,
systemctl status、systemd-cgtop看不到,内存紧张时两套管理互相打架。统一用native.cgroupdriver=systemd。 - 开了
live-restore又执行docker swarm init:两者互斥,会报--live-restore daemon configuration is incompatible with swarm mode。 - 以为跨大版本升级容器一定活着:只有同一
YY.MM.x补丁线内才有保证,跨版本升级前先停容器。 - 把
live-restore当成编排层的保险:它只对 standalone 容器有效,对 swarm service 无效。 - 把
log-opts当成对老容器也生效:它只作用于新建容器,存量容器要重建才应用。 - 现在就切
firewall-backend: nftables:实验特性、overlay 规则未迁移、Swarm 下不可用、不再自动开启 IP 转发,切换还会删掉大部分 iptables 链。 - 只靠主机防火墙拦端口,忘了 docker 会自己写转发规则:会出现"防火墙没放行、外部却能连上"。不公网的端口就别发布,要发布就绑
127.0.0.1。 - 不写
mem_limit:一个容器就能吃掉整机内存,轻则疯狂换页、重则 OOM 杀掉别的进程。 - 把
memswap_limit当成 swap 大小:它是内存 + swap 的总量,等于mem_limit时才是禁用 swap。 - 宿主根本没有 swap 却配 swap 相关键:先
swapon --show,没有 swap 这些键没有实际意义。 shm_size留在默认 64MB:数据库、浏览器内核、部分 JVM / ML 负载会报共享内存错误,而报错不指向/dev/shm。- 应用不是合格的 PID 1 却没开
init:僵尸进程堆积、停止信号没人接,表现成服务假死。 blkio限速没验证就当生效:cgroup v2 与部分云盘 / 虚拟化环境可能不支持,要用读写前后对比确认。userland-proxy: false改完没验证访问:可能影响容器访问宿主已发布端口,不要只看docker ps。- 顺手执行
docker volume prune:会删掉已停止容器的数据卷,属于不可逆操作。 - 资源限制类改动只改文件不重建容器:多数限制需要重建才生效,改了没效果先想这一点。
- 改
daemon.json不做备份与回滚准备:先判断文件存在再cp备份,重启失败就把备份还原回去再systemctl restart docker。 - 把示例值当成推荐值直接上线:文中所有数字都是示例,按自己机器的内存与磁盘算,不要照抄。
- 点赞
- 收藏
- 关注作者
评论(0)