作为开发者,我们几乎每天都在和 Docker 容器打交道,但真正遇到"容器退出后如何进入调试"、"日志撑爆磁盘"、"CPU/内存限制不合理导致雪崩"等问题时,很多人还是会手忙脚乱。本文从容器全生命周期的视角,系统梳理容器管理中的关键方法和常见问题经验,帮助你把容器的掌控力提升一个等级。
1. 理解容器状态 ------ 管理的基础
容器的生命周期状态直接决定了你可以对它做什么操作。用 docker ps -a 看到的 STATUS 字段包含以下核心状态:
| 状态 | 说明 | 常见原因 |
|---|---|---|
created |
已创建但从未启动 | docker create 而未 start |
running |
正在运行 | 主进程(PID 1)正常运行 |
paused |
已暂停 | 通过 docker pause 冻结了 cgroup |
exited (0) |
正常退出 | 主进程任务完成 |
exited (非0) |
异常退出 | 主进程崩溃、OOM、收到终止信号等 |
dead |
无法被 Docker 守护进程接管 | 一般是手动删除了 containerd 对应资源,极少见 |
查看命令基础:
bash
docker ps # 仅运行中容器
docker ps -a # 所有容器
docker ps -a --filter "status=exited" # 只看已退出
docker ps -a --filter "name=web" # 按名称过滤
docker inspect <container> # 查看完整元数据、挂载、网络等
2. 进入容器调试 ------ 不止是 exec
2.1 常规操作:进入运行中的容器
这是日常中最频繁的操作,注意选择正确的 Shell:
bash
docker exec -it <container> /bin/bash # 多数 Linux 镜像
docker exec -it <container> /bin/sh # Alpine 等轻量镜像
docker exec -it <container> bash # bash 在 PATH 中时
几个易错点:
- 有些镜像极简(如
scratch、busybox的某些版本)根本没 bash/sh,这时exec会失败。只能用docker export导出文件系统查看,或者将容器提交为新镜像再添加 shell。 -it必须一起用:-i保持 STDIN 打开,-t分配伪终端,否则你无法交互式操作。- 作为最后手段,可以通过
nsenter从宿主机进入容器的命名空间(需先获取 PID),但通常不推荐绕过 Docker 直接操作。
2.2 特殊场景:如何进入已经退出的容器?
这个问题难倒过很多人,因为 docker exec 只能在运行中的容器上工作。已退出的容器本质上是进程已终止,但文件系统和元数据仍然保留(除非被删除)。有几种方法可以让你"重新进入":
方法一:提交为镜像再启动(最常用)
将已退出的容器保存为新镜像,然后用该镜像启动一个临时容器,覆盖掉原来的入口命令:
bash
docker commit <exited-container> debug-image
docker run -it --rm --entrypoint /bin/bash debug-image
# 调查完毕后删除镜像
docker rmi debug-image
这样做的好处是保留了容器退出时的文件系统状态(日志、残留文件、配置等),非常适合事后排查。
方法二:导出文件系统并创建新镜像
如果容器已经被清理(只有 ps -a 中没有,但 /var/lib/docker 可能还有),可以尝试:
bash
docker export <container> -o container.tar
docker import container.tar debug-image:v2
docker run -it --rm debug-image:v2 /bin/bash
export 会丢失元数据(环境变量、端口、卷等),仅保留文件系统,适合提取数据。
方法三:直接访问宿主机上的容器文件系统
Docker 容器的根文件系统存在于 /var/lib/docker/overlay2/...(以 overlay2 为例)。通过 docker inspect 获取 Merged 目录后可以直接用宿主 Shell 查看:
bash
docker inspect <container> | jq -r '.[0].GraphDriver.Data.MergedDir'
sudo ls /var/lib/docker/overlay2/abc123.../merged
极其危险的操作,仅建议在只读或紧急抢救数据时使用,且绝不能直接修改文件,否则可能导致挂载层损坏。
3. 容器日志管理 ------ 避免磁盘被写爆
3.1 查看日志的基本命令
bash
docker logs <container> # 全量日志
docker logs -f <container> # 跟踪(tail -f)
docker logs --tail 100 <container># 最近 100 行
docker logs --since 2025-01-01T00:00:00 <container> # 按时间
docker logs --until "10 minutes ago" <container> # 相对时间
docker logs --timestamps <container> # 显示时间戳
3.2 日志驱动与轮转
默认日志驱动是 json-file,日志会无限增长,是磁盘占满的常见根源。强烈建议在部署时或 Docker daemon 配置中开启日志轮转:
容器级别设置:
bash
docker run \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp
全局配置(/etc/docker/daemon.json):
json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
之后重启 Docker。生产环境中还可以将日志驱动改为 syslog、fluentd、loki 等集中式系统,避免依赖本地文件。
3.3 查找"写日志最凶"的容器
快速定位占用空间大的容器日志:
bash
# 找到容器的日志路径(json-file 驱动下)
docker inspect --format='{{.LogPath}}' <container>
# 查看大小
du -sh /var/lib/docker/containers/*/*log
然后对目标容器执行 echo "" > ... 清空(需暂停容器或让其自行轮转更稳妥),或者直接触发 docker run 时的日志策略。
4. 容器清理与空间回收 ------ 给 Docker 瘦身
定期清理是防止开发机/CI 节点磁盘告急的必要操作,但要分清安全边界。
| 命令 | 作用 | 风险等级 |
|---|---|---|
docker container prune |
删除所有已停止的容器 | 中(确认确实不需要它们) |
docker image prune |
删除悬空 镜像(tag 为 <none>) |
低 |
docker image prune -a |
删除所有未被任何容器使用的镜像 | 高(会导致下次构建重新拉取) |
docker volume prune |
删除未被任何容器使用的卷 | 高(数据可能丢失) |
docker network prune |
删除未被任何容器使用的自定义网络 | 低 |
docker system prune |
一键清理已停止容器、悬空镜像、缓存等 | 中 |
docker system prune -a |
加上未被使用的镜像 | 高(小心生产环境!) |
生产环境最佳实践:
-
永远先执行
docker ps -a和docker images确认待清理对象。 -
使用
--filter增加时间条件,例如只清理 72 小时前退出且非今日构建的容器:bashdocker container prune --filter "until=72h" -
不要直接
prune已命名的卷,除非你明确知道里面的数据可以丢弃。
排查"谁吃掉了我的磁盘":
bash
docker system df # 概览:镜像、容器、卷占用
docker system df -v # 详细列出每个对象的大小
5. 资源限制与配置 ------ 让容器安分守己
默认情况下,容器对宿主机资源是无限制访问的(受 cgroup 父组约束),一台机器上一个失控的容器会拖垮所有服务。务必根据业务特点设置资源边界。
5.1 CPU 限制
bash
docker run \
--cpus="1.5" \ # 最多使用 1.5 个核(Docker 1.13+)
--cpu-shares=512 \ # 相对权重(默认1024),仅当CPU争抢时生效
--cpuset-cpus="0-1,3" \ # 绑定到物理核 0、1、3
myapp
--cpus是硬限制,容器内的 CPU 占用率上限由它决定。--cpu-shares是软限制,只在 CPU 过载时按比例分配,但不阻止容器在空闲时使用更多 CPU。多数场景下两者配合使用。
5.2 内存限制
bash
docker run \
--memory="512m" \ # 物理内存上限
--memory-swap="1g" \ # 物理+Swap 上限;设为0或等于--memory可禁用Swap
--memory-reservation="256m" \# 软限制,系统回收内存时尽量保证该值
--oom-kill-disable \ # 关闭OOM Killer(慎用)
myapp
- 若容器内存超限,会被 OOM Killer 杀死并标记为
exited (137),请检查dmesg或docker inspect中的OOMKilled字段。 - Java 应用应同时设置 JVM 堆内存小于容器限制,否则堆可能撑满容器而触发 OOM。
5.3 存储限制
存储限制在很大程度上取决于底层存储驱动:
1. 限制容器读写层大小(仅部分驱动支持)
devicemapper 和 btrfs 支持 --storage-opt size=10G,但广泛使用的 overlay2 不支持直接限制容器层大小。其推荐做法:
- 所有持久化数据写入挂载卷(Volume / Bind Mount)。
- 对卷所在底层文件系统启用配额(例如 XFS 项目配额、ZFS 数据集大小限制)。
tmpfs挂载可限制大小:--tmpfs /tmp:size=64m,noexec,nosuid。
2. I/O 吞吐/IOPS 限制
bash
docker run \
--device-read-bps=/dev/sda:1mb \
--device-write-bps=/dev/sda:1mb \
--device-read-iops=/dev/sda:100 \
--blkio-weight=500 \
myapp
5.4 网络配置
基本网络模式:
bash
--network bridge # 默认,NAT 桥接,通过端口映射通信
--network host # 共享宿主机网络栈,性能最高但端口冲突风险
--network none # 无网络
--network container:<name> # 与另一容器共享网络栈(Pod 模式)
自定义网络、固定 IP 和 DNS:
bash
# 创建自定义桥接网络
docker network create --subnet=192.168.100.0/24 my-net
# 指定固定 IP
docker run --network my-net --ip 192.168.100.10 --dns 8.8.8.8 myapp
带宽限速 ------ Docker 原生不支持直接对容器网卡设置带宽,但可以:
- 使用
tc(Traffic Control)在宿主机veth对上设置。 - 采用第三方网络插件(如 Calico 支持网络策略和带宽管理)。
- 借助 Docker 的
--publish模式不影响容器对外的总带宽,仅做端口映射。
6. 监控容器运行时指标
docker stats 是快速诊断的利器:
bash
docker stats # 实时刷屏显示所有容器 CPU/内存/网络/IO
docker stats --no-stream # 仅打印一次快照
docker stats <container> # 指定容器
输出中 MEM USAGE / LIMIT 直接反映容器是否接近内存限制;NET I/O 和 BLOCK I/O 可以帮助定位异常流量或磁盘瓶颈。
更深入的指标可以结合 docker top <container> 查看进程级信息,或者将 cadvisor、Prometheus 等接入监控体系。
7. 常见问题排查经验速查表
| 现象 | 排查步骤 |
|---|---|
容器启动后立即退出 (Exited (1) 等) |
1. docker logs <container> 查看错误输出 2. 确认启动命令和 ENTRYPOINT 是否正确 3. 检查环境变量、配置文件挂载路径 4. 用 --entrypoint /bin/bash 覆盖启动命令手动调试 |
容器被 OOM Kill (Exited (137)) |
1. docker inspect <container> 查看 OOMKilled 字段 2. `dmesg |
| 磁盘占用暴涨 | 1. docker system df -v 定位是大日志、大量镜像层还是过度的卷 2. 检查容器日志路径大小 du -sh /var/lib/docker/containers/*/ 3. 配置日志轮转并清理无用镜像/容器 |
| 网络不通 | 1. docker inspect 检查容器的网络模式、IP、端口映射 2. 确保宿主机防火墙规则允许对应端口 3. 对于自定义网络,检查 DNS 解析 docker exec 进去 nslookup 其他容器名 4. 检查是否因 --icc=false 关闭了容器间通信 |
执行 docker exec 无响应或报错 |
1. 确认容器状态是 running 2. 容器内 PID 1 进程可能把信号屏蔽或陷入不可中断睡眠,检查 docker top 3. 尝试 docker exec -it <container> sh 而非 bash |
| 容器内修改文件但重启后丢失 | 确认写入的目录是否在挂载的卷上;容器可写层仅存在于当前容器生命周期,docker restart 不影响,但 docker rm+docker run 会丢失原有容器层(除非使用卷或 commit) |
8. 写在最后
容器管理不是背命令,而是一个清晰的思维流程:先确认状态 → 再选定进入方式 → 检查日志和资源指标 → 回溯配置 → 清理与调优。每一次报错或异常,其实都是理解容器原理的绝佳机会。将上述方法内化到日常开发工作流中,你会发现自己不仅能快速解决问题,更能从设计阶段就避免大量生产隐患。
记住三条核心原则:
- 无状态容器打日志要轮转;
- 有状态服务必须挂卷,并且配套备份;
- 每个容器都要显式设置 CPU/内存限制。
这样,你的容器才能真正成为可预测、可调式、可生存的服务载体。