故障排查与最佳实践
Docker 排障这件事,套路比经验重要。绝大多数故障逃不出「现象 → 可能原因 → 排查命令 → 解决方法」这四步,这篇笔记把常见故障场景按这个套路整理一遍,配上真实的报错格式和排查命令,遇到问题照着走一遍基本都能定位。
后半部分是性能优化、Dockerfile / Compose 最佳实践和企业部署规范,属于「少踩坑」的部分,和前面的「踩坑后怎么办」配套看。
Docker 服务启动失败
典型报错
$ sudo systemctl start docker
Job for docker.service failed because the control process exited with error code.
See "systemctl status docker.service" and "journalctl -xeu docker.service" for details.
systemctl status 里只能看到失败结论,看不到原因:
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since Thu 2024-01-18 10:23:45 CST; 12s ago
Process: 24501 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock (code=exited, status=1/FAILURE)
Main PID: 24501 (code=exited, status=1/FAILURE)
可能原因
按出现频率排:
/etc/docker/daemon.json写错——最常见,多一个逗号、少一个引号、字段名拼错,Docker 都会直接拒绝启动- 存储驱动配置和现有数据不兼容(比如原来用 overlay2,改成了别的驱动)
- iptables / 内核模块缺失,常见于精简过的系统或内核升级后
- 磁盘满了,Docker 起不来
- 升级后老配置项被废弃
排查命令
# 看服务状态和最近几行日志
sudo systemctl status docker
# 看完整日志,-n 100 取最近 100 行,--no-pager 防止进入翻页模式
sudo journalctl -u docker -n 100 --no-pager
# 还不清楚就前台手动启动,报错直接打在屏幕上
sudo dockerd --debug
daemon.json 出问题时,日志里会有非常明确的提示。JSON 语法错误:
unable to configure the Docker daemon with file /etc/docker/daemon.json:
invalid character '}' looking for beginning of object key string
字段名拼错(不认识的配置项):
unable to configure the Docker daemon with file /etc/docker/daemon.json:
the following directives don't match any configuration option: registry-mirro
解决方法
JSON 语法问题:先校验再修,工具会指出出错位置。
# 用 python 校验 JSON 是否合法
python3 -m json.tool /etc/docker/daemon.json
字段名写错:对照 dockerd --help 或官方文档核对,改完 sudo systemctl restart docker。
实在找不到是哪一项的问题:把 daemon.json 改名备份,让 Docker 用默认配置启动,确认能起来后再把配置逐项加回来,二分定位。
sudo mv /etc/docker/daemon.json /etc/docker/daemon.json.bak
sudo systemctl restart docker
每次改 daemon.json 之前先 cp 备份一份,改完先 python3 -m json.tool 校验,再重启。这个文件写错是 Docker 起不来的头号原因,而它本身没有任何语法检查机制帮你兜底。
容器无法启动
典型报错
docker run 直接失败:
docker: Error response from daemon: driver failed programming external connectivity on endpoint web (a1b2...):
Bind for 0.0.0.0:8080 failed: port is already allocated.
另一种更隐蔽:命令返回了容器 ID,看起来启动成功,但 docker ps 里根本没有它。
排查命令
容器起来又退出,先看全量列表和日志:
# -a 显示已退出的容器,STATUS 列会写明退出码
docker ps -a
# 退出原因基本都在日志里
docker logs <容器名>
# 日志都没有,就看退出状态
docker inspect <容器名> --format '{{.State.ExitCode}} {{.State.Error}}'
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 myapp Exited (1) 2 minutes ago myapp
可能原因与解决方法
| 退出码 / 报错 | 常见原因 | 处理 |
|---|---|---|
| Exited (1) | 应用自身报错(配置读不到、依赖连不上) | docker logs 看应用输出的具体报错 |
| Exited (126) | 命令存在但不可执行 | 检查文件执行权限、入口脚本 |
| Exited (127) | 容器里找不到要执行的命令 | 检查 CMD/ENTRYPOINT 拼写,确认镜像里真装了这个命令 |
| Exited (137) | 被 SIGKILL,多半是 OOM | 见下文 OOMKilled 一节 |
| port is already allocated | 宿主机端口被占用 | ss -tlnp 找到占用进程,或换宿主机端口 |
| 挂载路径相关报错 | 挂载的宿主机路径写错 | 核对 -v 两边路径是否存在 |
Exited (0) 表示主进程正常跑完退出了。对一次性任务(数据迁移、批处理)这是预期行为;对 Web 服务这类长驻进程,退出码 0 也说明应用没常驻——常见原因是 CMD 里写的是后台命令,主进程起完就结束了。
容器不断重启
典型报错
$ docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 myapp Restarting (1) 10 seconds ago myapp
容器一直处于 Restarting 状态,隔几秒重启一次,永远起不来。
可能原因
重启本身不是故障,是 --restart 策略在正常工作:容器主进程退出了,Docker 按策略把它拉起来,应用又崩,再拉起,如此循环。真正要查的是应用为什么退出。
| restart 策略 | 行为 |
|---|---|
| no(默认) | 退出后不重启 |
| on-failure | 只在非 0 退出码时重启 |
| always | 无论退出码是多少都重启,宿主机或 Docker 重启后也会拉起 |
| unless-stopped | 和 always 类似,但手动 stop 过的不会再拉起 |
排查命令
# 看应用崩溃前输出了什么
docker logs --tail 100 <容器名>
# 看退出码和已重启次数
docker inspect <容器名> --format '{{.State.ExitCode}} 重启次数: {{.RestartCount}}'
解决方法
先把重启循环打断,让容器停在退出状态再慢慢查:
docker update --restart=no <容器名>
docker stop <容器名>
然后按上一节「容器无法启动」的套路查日志、修应用,修好后再把策略改回来:
docker update --restart=always <容器名>
docker run --rm 是容器退出后自动删除,和 --restart 语义冲突。排查重启循环时也不要用 --rm——容器一删现场就没了,连日志都查不到。排查期间要的是保留现场,不是自动打扫。
OOMKilled
典型报错
容器跑着跑着突然没了,docker ps -a 里显示:
STATUS
Exited (137) 5 minutes ago
137 = 128 + 9,表示进程收到了 SIGKILL。被 OOM Killer 杀掉是最常见的来源。
排查命令
# 直接确认是不是被 OOM Killer 干掉的
docker inspect <容器名> --format '{{.State.OOMKilled}} 退出码: {{.State.ExitCode}}'
# 看容器当前的内存限制(0 表示没限制)
docker inspect <容器名> --format '{{.HostConfig.Memory}}'
# 内核日志里也能找到记录
sudo dmesg | grep -i 'killed process'
Memory cgroup out of memory: Killed process 12345 (java) total-vm:3145728kB, anon-rss:1048576kB
可能原因
- 应用本身内存泄漏,或负载超出预期
- 容器内存限制
-m设小了 - 应用不知道自己被限制——以 JVM 为代表,默认按宿主机内存规划堆大小,容器限制 1G,它敢按 64G 的机器来算堆
解决方法
调大内存限制:
# 运行中的容器直接改
docker update --memory 2g <容器名>
# 或启动时指定
docker run -d --memory 2g <镜像>
JVM 类应用要特别处理。JDK 8u191 / JDK 10 之后 JVM 能识别容器内存限制,建议用百分比参数,不用手写死 -Xmx:
docker run -d --memory 1g -e JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0" <镜像>
老版本 JDK 没有容器感知能力,必须显式指定 -Xmx,并且明显小于容器限制(比如容器 1G,堆给 768M),给堆外内存、线程栈、JIT 留余量,否则堆没满容器先被杀。
排查时可以用 docker stats <容器名> 实时看内存曲线。缓慢爬升直到被杀,大概率是泄漏;一启动就冲上去,就是限制设小了或堆参数没配对。
Permission Denied
典型报错
容器内写文件失败:
touch: /data/app.log: Permission denied
容器启动时直接失败:
open /var/lib/nginx/nginx.pid: permission denied
可能原因
这是新手最常踩的坑:挂载卷会把宿主机目录原样映射进容器,目录的属主和权限也跟着进去。容器里的进程跑在某个 UID 下(比如 nginx 官方镜像的 worker 进程是 UID 101),如果宿主机目录属主是 root 且没给其他人写权限,容器进程就写不了。
平时用容器内 root(UID 0)一切正常,一旦镜像里应用主动降权运行(USER 指令),问题立刻暴露。
排查命令
# 容器内进程的真实 UID
docker exec <容器名> id
# 容器内看到的挂载目录属主(-n 显示数字 UID/GID,避免两边用户名不同造成误解)
docker exec <容器名> ls -ln /data
# 宿主机侧同一个目录的属主
ls -ln /srv/app-data
两边数字对不上,就是这个问题。
解决方法
方法一:把宿主机目录属主改成容器内进程的 UID(用数字即可,不用管名字):
sudo chown -R 101:101 /srv/app-data
方法二:构建镜像时统一 UID,Dockerfile 里 RUN useradd -u 10001 app 并 USER app,宿主机目录也 chown 成 10001,团队内约定一个固定 UID 最省事。
方法三:数据不需要宿主机直接访问的话,改用 named volume。首次挂载空卷时 Docker 会把镜像里对应目录的内容和权限初始化进去,能绕开大部分权限问题。
CentOS / RHEL / Fedora 开着 SELinux 时,即使 UID 权限都对,挂载目录也可能被拒绝访问,报错同样是 Permission denied。挂载时加 :z(多容器共享)或 :Z(单容器独占)让 Docker 自动打标签:
docker run -v /srv/data:/data:Z <镜像>
排查可以用 getenforce 确认 SELinux 状态。
Exec Format Error
典型报错
exec /usr/local/bin/app: exec format error
docker: Error response from daemon: failed to create shim task: OCI runtime create failed:
runc create failed: unable to start container process: exec: "/app": exec format error: unknown.
可能原因
镜像架构和机器架构不一致。最常见的是 Apple Silicon(M 系列芯片,arm64)的机器跑只构建了 amd64 的镜像,树莓派等 ARM 设备同理。脚本缺 shebang 也会报类似错误,但概率小得多,先按架构问题排查。
排查命令
# 机器架构
uname -m
# 镜像架构
docker image inspect <镜像> --format '{{.Os}}/{{.Architecture}}'
一个输出 x86_64、一个输出 linux/arm64,原因就对上了。
解决方法
方法一:用 --platform 强制指定架构,靠 QEMU / Rosetta 模拟运行,能跑但慢,只适合临时应急:
docker run --platform linux/amd64 <镜像>
方法二(推荐):找官方多架构镜像。docker buildx imagetools inspect <镜像> 能看到 manifest 列表里有哪些架构,选支持你平台的 tag;自己的项目则用 buildx 构建多架构镜像推上去。
M 系列 Mac 上如果 amd64 模拟频繁崩溃,在 Docker Desktop 设置里启用 Rosetta 会稳定很多。长期方案还是给项目配上多架构构建,别依赖模拟。
镜像拉取失败
典型报错
网络不通:
Error response from daemon: Get "https://registry-1.docker.io/v2/":
net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
触发限速:
toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit
可能原因
- 服务器到 Docker Hub 的网络不通,国内服务器最常见
- 匿名拉取触发 Docker Hub 限速(匿名每 6 小时 100 次,登录用户 200 次)
- DNS 解析不了 registry 域名
- 私有仓库认证失败(报 401 / unauthorized,要先
docker login)
排查命令
# 测试到 registry 的连通性
curl -v https://registry-1.docker.io/v2/
# DNS 是否解析正常
nslookup registry-1.docker.io
curl 返回 401 是正常的——说明网络通,只是没带认证;超时或 connection refused 才是网络问题。
解决方法
配置镜像加速器,编辑 /etc/docker/daemon.json:
{
"registry-mirrors": ["https://你的加速器地址"]
}
sudo systemctl restart docker
docker pull nginx # 验证
限速问题:docker login 登录后限额翻倍;或把常用镜像转存到私有仓库 / 国内镜像站。
网上流传的公共加速器地址失效很快,用之前先 curl 验证一下能不能通。企业环境建议自建 Harbor 做代理缓存(proxy cache),一劳永逸。
Manifest Unknown
典型报错
$ docker pull nginx:1.25.33
Error response from daemon: manifest for nginx:1.25.33 not found: manifest unknown: manifest unknown
可能原因
- 标签根本不存在——版本号写错是最常见原因(多一位、少一位、记错小版本号)
- 标签存在,但不含当前机器架构的 manifest。在 arm64 机器上拉只发布了 amd64 的 tag,部分镜像报的就是 manifest unknown,而不是 exec format error
- 私有仓库里镜像被清理策略删掉了
排查命令
# 列出这个 tag 支持哪些架构、digest 是什么
docker buildx imagetools inspect nginx:1.25.3
标签不存在会直接报 not found;存在的话会列出各平台的 manifest,对照自己的架构看有没有。
解决方法
- 去 Docker Hub 仓库页面的 Tags 列表核对正确写法,或直接换成确认存在的版本
- 多架构问题:换一个支持当前架构的 tag,或用
--platform指定存在的架构拉取 - 私有仓库:找管理员确认镜像是否还在
拉不存在的 tag 报的是 manifest unknown,而不是直白的 tag not found,很容易误导新人以为仓库坏了。记住:manifest unknown ≈ 你要的标签不存在(或没有你的架构),先检查拼写。
Overlay2 空间占满
典型报错
Error response from daemon: ... write /var/lib/docker/overlay2/xxx/diff/yyy: no space left on device
failed to register layer: ApplyLayer exit status 1 stdout: stderr: write /usr/...: no space left on device
拉镜像、起容器、构建镜像时报这类错,本质都是 /var/lib/docker 所在分区满了。
排查命令
# 分区剩余空间
df -h /var/lib/docker
# Docker 各项占用汇总
docker system df
# overlay2 总占用
sudo du -sh /var/lib/docker/overlay2
docker system df 输出示例:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 25 5 12.5GB 9.8GB (78%)
Containers 12 3 1.2GB 1.1GB (91%)
Local Volumes 8 4 500MB 300MB (60%)
Build Cache 40 0 6.4GB 6.4GB
RECLAIMABLE 列就是可回收的部分,先看哪里能回收再动手。
解决方法
按下一节「Docker 磁盘清理」的套路正常清理即可。如果清完 overlay2 还是很大,说明大头是正在使用中的镜像层,只能删未使用的镜像腾空间。
/var/lib/docker/overlay2/ 下的目录是镜像层和容器层的物理存储,层与层之间有依赖关系。直接 rm 某个目录会导致引用它的镜像和容器集体损坏,报 layer does not exist,基本只能重置 Docker 数据。正确姿势永远是通过 docker 命令(prune、rmi)让 Docker 自己删。
Docker 磁盘清理
先看占用
# 汇总
docker system df
# 明细,能看到每个镜像、容器各占多少
docker system df -v
清理命令
docker container prune # 删所有停止的容器
docker image prune # 只删 dangling 镜像(无标签的 <none>)
docker image prune -a # 删所有未被容器使用的镜像
docker volume prune # 删未被容器使用的卷(注意数据)
docker network prune # 删未使用的自定义网络
docker builder prune # 删构建缓存,往往是占用大头
docker system prune # 上面几样的合集(不含 volume,且只删 dangling 镜像)
docker system prune -a --volumes # 全家桶:未使用镜像 + 卷一起删
dangling 镜像和未使用镜像的区别
| 类型 | 长相 | 怎么来的 | 怎么删 |
|---|---|---|---|
| dangling(悬空) | REPOSITORY 和 TAG 都是 <none> | 重新构建镜像后旧层失去标签 | docker image prune |
| 未使用镜像 | 有正常的仓库名和标签 | 拉取或构建后没有任何容器在用 | docker image prune -a 或 docker rmi |
docker system prune 默认只清 dangling,加 -a 才会动未使用镜像——后者会把本地缓存的常用基础镜像也删掉,下次构建要重新拉,执行前想清楚。
日志文件也在吃磁盘
json-file 日志驱动默认不轮转,日志文件在 /var/lib/docker/containers/<容器ID>/ 下,一个话多的容器写几十 GB 不稀奇。应急清理指定容器的日志:
sudo truncate -s 0 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log
根治方法是配置日志轮转,见下文「日志排查」一节。
测试机可以加个 crontab 每周执行 docker system prune -f。生产机器别自动清未使用镜像(-a),回滚时可能要用旧镜像。
网络异常
典型现象
- 容器 A 用容器名访问容器 B 不通:
ping: bad address 'mysql' - 容器里访问不了外网
- 端口映射配了,外面访问宿主机端口却没反应
排查命令
# 容器在哪个网络里、同网络还有哪些容器
docker network inspect <网络名>
# 容器内测试连通性(精简镜像没装工具,就用 busybox 共享网络命名空间测)
docker exec <容器名> ping -c 2 <另一个容器名>
docker run --rm --network container:<容器名> busybox ping -c 2 223.5.5.5
# 端口映射是否生效
docker port <容器名>
ss -tlnp | grep <宿主机端口>
可能原因与解决方法
-
容器不在同一个自定义网络:默认 bridge 网络不支持用容器名互相访问,只有自定义网络内置 DNS 解析。把容器加进同一个网络:
docker network connect <网络名> <容器名>,或重建时指定--network。 -
firewalld / iptables 规则被冲掉:重启 firewalld 会把 Docker 的 NAT 规则清掉,表现为端口映射全部失效。
sudo systemctl restart docker让 Docker 重建规则即可。 -
宿主机防火墙或云安全组没放行:
docker port显示映射正常、宿主机本机 curl 通、外部不通,基本就是这一层。云服务器别忘了查安全组。
排查网络问题按「容器内出网 → 容器内 DNS → 宿主机端口映射 → 宿主机防火墙 → 云安全组」的顺序一层层往外走,每确认一层就排除一类,比到处乱试快得多。
DNS 异常
典型报错
容器内:
ping: bad address 'www.baidu.com'
curl: (6) Could not resolve host: api.example.com
ping IP 通、解析域名失败,就是典型的 DNS 问题。
可能原因
- 宿主机
/etc/resolv.conf里的 DNS 服务器容器内不可达 - 宿主机用 systemd-resolved,resolv.conf 里写的是
127.0.0.53。容器内这个回环地址指向容器自己,根本用不了——Docker 检测到后会回退到 8.8.8.8,国内环境解析慢甚至失败 - 公司内网要走内网 DNS 才能解析内部域名
排查命令
# 容器实际使用的 DNS 服务器
docker exec <容器名> cat /etc/resolv.conf
# 解析测试
docker exec <容器名> nslookup www.baidu.com
解决方法
单个容器指定:
docker run -d --dns 223.5.5.5 --dns 114.114.114.114 <镜像>
全局指定,写进 /etc/docker/daemon.json:
{
"dns": ["223.5.5.5", "114.114.114.114"]
}
改完 sudo systemctl restart docker,之后新建的容器生效(旧容器要重建才生效)。
Compose 里可以用 dns 字段给单个服务指定 DNS。另外自定义网络里的容器互相解析走的是 Docker 内置的 127.0.0.11,那个按容器名解析,和外部 DNS 是两回事,别混淆。
Volume 故障
典型现象
- 明明挂了卷,容器里目录却是空的
- 删了容器,以为"数据丢了"
- 改了宿主机文件,容器里看不到变化
排查命令
# 看容器实际的挂载关系,挂载源、目标、读写权限都在里面
docker inspect <容器名> --format '{{json .Mounts}}'
# 所有卷
docker volume ls
# 某个卷的实际存储位置
docker volume inspect <卷名>
挂载不生效的常见原因
- bind mount 的宿主机路径写错(相对路径、拼写错误),Docker 会自动创建一个空目录挂进去,看起来就像"挂载了但没生效"
- 容器内目标路径写错,数据写到了容器层里而不是卷里
- 空目录 bind mount 会遮住镜像里同路径的原有内容——比如把空目录挂到 nginx 的
/usr/share/nginx/html,页面全没了。named volume 首次挂载空卷时会自动把镜像内容复制进来,bind mount 不会,这是两者的重要区别
数据"丢失"的常见误解
- 删容器不会删 named volume,数据还在,
docker volume ls能找到 - Dockerfile 里写了
VOLUME指令的镜像,每次重建容器都会生成一个新的匿名卷,旧卷还在,只是新容器挂的是新卷——看起来像"数据丢了",其实是挂错了卷 docker compose down -v和docker volume prune会真的删卷,执行前确认
找回数据:named volume 的数据就在宿主机 /var/lib/docker/volumes/<卷名>/_data 下,直接拷贝即可。
生产环境对带数据的卷执行任何 prune、down -v 之前,先把 _data 目录打包备份。卷删除不可逆,没有回收站。
Healthcheck 故障
典型报错
$ docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 myapp Up 10 minutes (unhealthy) myapp
容器在跑,业务可能也正常,但一直 unhealthy。
排查命令
# 看健康检查的详细输出和失败原因
docker inspect <容器名> --format '{{json .State.Health}}'
{
"Status": "unhealthy",
"FailingStreak": 5,
"Log": [
{
"ExitCode": 127,
"Output": "/bin/sh: 1: curl: not found"
}
]
}
Log 里的 ExitCode 和 Output 直接告诉你失败原因。也可以在容器里手动跑一遍健康检查命令验证退出码。
可能原因与解决方法
-
健康检查命令依赖的工具镜像里没有(ExitCode 127):alpine / slim 系镜像普遍没有 curl。换成
wget -q -O /dev/null、应用自带的检查命令,或构建镜像时装上 curl。 -
start_period太短:应用启动慢(比如 Java 服务要初始化几十秒),还没起完健康检查就开始失败计数,直接被判 unhealthy。把start-period调到大于应用正常启动时间。 -
检查路径不对:
/health实际返回 404 或非 200,curl -f会按失败处理。在容器里手动执行确认。
Compose 写法示例:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 40s
test 用数组格式(["CMD", ...])可以避免 shell 转义的坑。退出码 0 表示健康、1 表示不健康,写自定义检查脚本时确保退出码语义正确。
日志排查
docker logs 看不到日志
最常见的原因:应用没把日志写到标准输出。docker logs 读的是容器主进程的 stdout / stderr,如果应用把日志写进了容器里的文件(比如 /var/log/app/app.log),docker logs 自然空空如也。
验证方法:
# 看主进程(PID 1)的 stdout 指向哪里
docker exec <容器名> ls -l /proc/1/fd/1
指向 /dev/null 或一个没人接的管道,就说明日志没出来。
解决方法:改应用配置,把日志输出到 stdout;或者学 nginx 官方镜像的做法,把日志文件软链到标准输出:
ln -sf /dev/stdout /var/log/nginx/access.log
日志文件在哪里
默认 json-file 驱动下,日志落在宿主机:
# 查具体容器的日志文件位置
docker inspect <容器名> --format '{{.LogPath}}'
# 直接看原始 JSON 日志
sudo tail -f /var/lib/docker/containers/<容器ID>/<容器ID>-json.log
日志驱动与轮转
# 看当前日志驱动
docker info --format '{{.LoggingDriver}}'
json-file 默认不轮转,必须配置。daemon.json 全局配置:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
用了 fluentd、syslog 等驱动把日志转发出去后,docker logs 默认就用不了了。如果 docker logs 报 configured logging driver does not support reading,说明日志被转发走了,去对应的日志平台查。
Docker 性能优化
镜像体积
- 选小基础镜像:alpine(约 5MB)、debian slim、distroless,比完整发行版小一个数量级
- 多阶段构建:编译环境和运行环境分离,最终镜像只带产物
- 同一层里装完就清:包管理器安装后紧接着清缓存(如 apt 的列表缓存),不留到下一层
- 配
.dockerignore排除.git、node_modules、本地文档,减小构建上下文
镜像小意味着拉取快、启动快、磁盘省,CI/CD 里收益最明显。
构建缓存
- 把变动少的指令放前面:基础依赖安装在前,业务代码 COPY 在后,代码改动不会让依赖层缓存失效
- 依赖清单先行:先 COPY
package.json/go.mod装依赖,再 COPY 全部源码 - 用 BuildKit(Docker 23+ 默认启用),缓存粒度更细,还能用 cache mount 跨构建共享包管理器缓存
存储驱动
- 现代 Docker 默认 overlay2,不要再配 devicemapper、aufs 这些过时驱动
- 写密集场景(数据库数据目录、大量小文件写入)挂 named volume,绕过 overlay2 的 copy-on-write 开销
- 确认当前驱动:
docker info --format '{{.Driver}}'
日志轮转与磁盘 IO
- json-file 不轮转会把磁盘写满;高频写入本身就是持续的磁盘 IO 开销
- 标配
max-size/max-file(见「日志排查」一节);日志量特别大的服务考虑换local驱动(默认按 100MB 滚动压缩),或直接把日志交给采集系统 - 生产环境关掉 debug 级别日志,别小看一条一条打印累积起来的量级
Dockerfile 最佳实践
一份精炼清单,详细展开见本系列《Dockerfile、BuildKit 与 Buildx》一篇:
- 基础镜像固定具体版本,不用 latest;优先官方镜像
- 选小镜像:alpine / slim / distroless
- 多阶段构建,编译产物单独拷贝到运行镜像
- 变动少的层放前面,充分利用构建缓存;依赖清单先于源码 COPY
- 合并关联的 RUN 指令,每层结束清理包管理器缓存
- 不用 root 跑应用:USER 指令指定非特权用户
- 配
.dockerignore,排除.git、node_modules等无关文件 - CMD / ENTRYPOINT 用 exec 格式(JSON 数组),让主进程成为 PID 1、正确接收停止信号
- 加 HEALTHCHECK,编排层才能感知应用真实状态
- 不用 ARG 传密钥(会留在镜像历史里),用 BuildKit 的 secret mount
- 打 LABEL(维护者、版本、源码地址),方便追溯
Compose 最佳实践
一份精炼清单,详细展开见本系列《Docker Compose》一篇:
- 镜像固定版本,不用 latest;变量统一放
.env管理 .env、密码文件不进 Git,仓库里放.env.example做模板- 每个长驻服务配 healthcheck,有依赖关系的服务用
depends_on加condition: service_healthy控制启动顺序 - 配资源限制:
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
- 配日志轮转:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped是大多数服务的合理默认值- 数据用 named volume,不要在 compose 文件里写带个人色彩的宿主机绝对路径
- 不同业务用不同网络隔离,对外只暴露必要端口
- 用 profiles 区分环境,避免维护多份 compose 文件
- apply 之前先
docker compose config校验一遍最终渲染出的配置
企业部署规范
一份可直接落地的规范清单,数值按团队实际情况调整:
镜像版本管理
- 禁止用 latest 部署,所有镜像必须有明确的版本 tag
- 版本号用语义化版本(如 1.2.3),与代码仓库的 git tag 对应
- 生产环境用 digest 锁定镜像,杜绝"同名 tag 被覆盖"导致的环境不一致
- 镜像统一推送企业私有仓库(Harbor 等),生产环境不允许直接从 Docker Hub 拉
- 保留最近 N 个版本用于回滚,定期清理更老的版本
命名规范
- 镜像名:
仓库地址/团队或项目/服务名:版本,全小写 - 容器名 / compose 项目名:
项目-服务-环境,例如 mall-order-prod - 卷名带项目前缀,避免多项目共用主机时分不清归属
资源限制标配
- 所有容器必须设内存上限,防止单个容器 OOM 拖垮整台宿主机
- CPU 建议设上限,核心服务按需预留
- 设置 pids-limit(如 200),防 fork 炸弹
- JVM 应用必须显式配置堆参数,且堆小于容器内存限制(见 OOMKilled 一节)
日志标配
- 应用日志统一输出到 stdout / stderr,不在容器内写日志文件
- json-file 驱动必须配
max-size: "10m"、max-file: "3"(或按日志量调整) - 统一接入日志采集(ELK / Loki 等),容器不落地保存日志
- 日志里不打印密码、密钥、身份证号等敏感信息
健康检查标配
- 所有长驻服务必须配 HEALTHCHECK / healthcheck
start_period不小于应用实测启动时间,留 1.5~2 倍余量- 检查端点要轻量,只验证服务可用性,不做重操作
- 编排层根据健康状态做流量摘挂和自动重启
常见 FAQ
Q:容器停了,数据还在吗?
分情况。容器自己的可写层:docker stop 后数据还在,docker rm 之后就没了。放在卷(volume)或宿主机挂载目录里的数据:删容器不受影响,一直都在。重要数据必须放卷里,不要依赖容器层。
Q:Exited (0) 是报错吗?
不是。退出码 0 表示主进程正常执行完毕。如果你预期它常驻运行却退出了,说明主进程本身结束了(比如 CMD 写成了后台命令),这不是 Docker 的故障,去查应用为什么退出。
Q:改了 docker-compose.yml,restart 为什么不生效?
docker compose restart 只是重启容器进程,不会重建容器,配置改动当然不会生效。改配置后要 docker compose up -d,Compose 检测到配置变化会自动重建相关容器。
Q:镜像列表里的 <none> 是什么,能删吗?
是 dangling 镜像:重新构建镜像后,旧镜像层失去了标签,变成 <none>。它们只是占磁盘,没有被引用,docker image prune 可以安全清掉。
Q:容器里没有 bash 也进不去,怎么排查?
精简镜像(alpine、distroless)里可能只有 sh,甚至什么都没有。先试 docker exec -it <容器名> sh;连 sh 都没有的话,用 docker logs 看输出,或用 docker run --rm --network container:<容器名> busybox sh 借一个带工具的容器共享命名空间排查。
Q:容器里时间比宿主机慢 8 小时?
镜像默认用 UTC。容器和宿主机共享内核,时间本身是一致的,差的只是时区显示。设置环境变量 -e TZ=Asia/Shanghai,或挂载 -v /etc/localtime:/etc/localtime:ro 解决。
Q:容器里怎么访问宿主机上的服务?
Docker 20.10+ 的 Linux 上加 --add-host=host.docker.internal:host-gateway,容器内用 host.docker.internal 访问宿主机;Docker Desktop(Mac / Windows)上这个域名默认就有。注意宿主机服务要监听 0.0.0.0 或对应网卡,只监听 127.0.0.1 的话容器访问不到。
Q:生产环境能进容器改文件吗?
规范上不建议用 docker exec 进生产容器改文件、装工具——改动不可追溯,容器重建即丢失。排查问题可以进,改东西要走镜像或配置下发,保证可复现。