跳到主要内容

故障排查与最佳实践

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)

可能原因​

按出现频率排:

  1. /etc/docker/daemon.json 写错——最常见,多一个逗号、少一个引号、字段名拼错,Docker 都会直接拒绝启动
  2. 存储驱动配置和现有数据不兼容(比如原来用 overlay2,改成了别的驱动)
  3. iptables / 内核模块缺失,常见于精简过的系统或内核升级后
  4. 磁盘满了,Docker 起不来
  5. 升级后老配置项被废弃

排查命令​

# 看服务状态和最近几行日志
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 的习惯

每次改 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 <容器名>
--rm 和重启策略别混用

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 会把镜像里对应目录的内容和权限初始化进去,能绕开大部分权限问题。

SELinux 环境

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

可能原因​

  1. 标签根本不存在——版本号写错是最常见原因(多一位、少一位、记错小版本号)
  2. 标签存在,但不含当前机器架构的 manifest。在 arm64 机器上拉只发布了 amd64 的 tag,部分镜像报的就是 manifest unknown,而不是 exec format error
  3. 私有仓库里镜像被清理策略删掉了

排查命令​

# 列出这个 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 还是很大,说明大头是正在使用中的镜像层,只能删未使用的镜像腾空间。

不要手动删 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 <宿主机端口>

可能原因与解决方法​

  1. 容器不在同一个自定义网络:默认 bridge 网络不支持用容器名互相访问,只有自定义网络内置 DNS 解析。把容器加进同一个网络:docker network connect <网络名> <容器名>,或重建时指定 --network。

  2. firewalld / iptables 规则被冲掉:重启 firewalld 会把 Docker 的 NAT 规则清掉,表现为端口映射全部失效。sudo systemctl restart docker 让 Docker 重建规则即可。

  3. 宿主机防火墙或云安全组没放行: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 <卷名>

挂载不生效的常见原因​

  1. bind mount 的宿主机路径写错(相对路径、拼写错误),Docker 会自动创建一个空目录挂进去,看起来就像"挂载了但没生效"
  2. 容器内目标路径写错,数据写到了容器层里而不是卷里
  3. 空目录 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 直接告诉你失败原因。也可以在容器里手动跑一遍健康检查命令验证退出码。

可能原因与解决方法​

  1. 健康检查命令依赖的工具镜像里没有(ExitCode 127):alpine / slim 系镜像普遍没有 curl。换成 wget -q -O /dev/null、应用自带的检查命令,或构建镜像时装上 curl。

  2. start_period 太短:应用启动慢(比如 Java 服务要初始化几十秒),还没起完健康检查就开始失败计数,直接被判 unhealthy。把 start-period 调到大于应用正常启动时间。

  3. 检查路径不对:/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》一篇:

  1. 基础镜像固定具体版本,不用 latest;优先官方镜像
  2. 选小镜像:alpine / slim / distroless
  3. 多阶段构建,编译产物单独拷贝到运行镜像
  4. 变动少的层放前面,充分利用构建缓存;依赖清单先于源码 COPY
  5. 合并关联的 RUN 指令,每层结束清理包管理器缓存
  6. 不用 root 跑应用:USER 指令指定非特权用户
  7. 配 .dockerignore,排除 .git、node_modules 等无关文件
  8. CMD / ENTRYPOINT 用 exec 格式(JSON 数组),让主进程成为 PID 1、正确接收停止信号
  9. 加 HEALTHCHECK,编排层才能感知应用真实状态
  10. 不用 ARG 传密钥(会留在镜像历史里),用 BuildKit 的 secret mount
  11. 打 LABEL(维护者、版本、源码地址),方便追溯

Compose 最佳实践​

一份精炼清单,详细展开见本系列《Docker Compose》一篇:

  1. 镜像固定版本,不用 latest;变量统一放 .env 管理
  2. .env、密码文件不进 Git,仓库里放 .env.example 做模板
  3. 每个长驻服务配 healthcheck,有依赖关系的服务用 depends_on 加 condition: service_healthy 控制启动顺序
  4. 配资源限制:
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
  1. 配日志轮转:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
  1. restart: unless-stopped 是大多数服务的合理默认值
  2. 数据用 named volume,不要在 compose 文件里写带个人色彩的宿主机绝对路径
  3. 不同业务用不同网络隔离,对外只暴露必要端口
  4. 用 profiles 区分环境,避免维护多份 compose 文件
  5. 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 进生产容器改文件、装工具——改动不可追溯,容器重建即丢失。排查问题可以进,改东西要走镜像或配置下发,保证可复现。