跳到主要内容

运维、安全与监控

这一篇偏运维向:容器跑起来之后,怎么限制它吃资源、日志怎么管、安全怎么加固、出了问题看什么指标。内容按「资源限制 → 日志 → 安全 → 监控 → 安全清单」的顺序展开,每一节都可以单独翻。

环境说明

文中命令基于 Linux 宿主机 + Docker Engine 24+。涉及 cgroup 路径时会同时标注 v1 和 v2 的写法,可以用 stat -fc %T /sys/fs/cgroup/ 查看宿主机用的是哪个版本:输出 cgroup2fs 是 v2,输出 tmpfs 是 v1。

CPU 限制​

Docker 通过 cgroup 限制容器能用的 CPU。常用三个参数,解决的问题不一样:

参数作用适用场景
--cpus限制最多使用多少个 CPU(可以是小数)最常用的硬限制
--cpu-sharesCPU 争抢时的相对权重,默认 1024机器满载时按比例分配
--cpuset-cpus绑定容器只用指定的物理核需要绑核、减少缓存抖动的场景

--cpus:限制 CPU 核数​

# 最多用 1.5 个核
docker run -d --name cpu-limit --cpus 1.5 nginx

它的本质是设置 cgroup 的 CPU 配额:--cpus 1.5 会被换算成「每 100ms 周期内最多用 150ms」。验证方法,直接看容器里的 cgroup 文件:

docker exec cpu-limit cat /sys/fs/cgroup/cpu.max        # cgroup v2
docker exec cpu-limit cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us # cgroup v1
# v2 输出:150000 100000(配额 / 周期,即 1.5 核)
# v1 输出:150000(对应 cpu.cfs_period_us 默认 100000)

--cpu-shares:争抢时的权重​

--cpu-shares 不是硬限制。机器空闲时容器想占多少 CPU 都行,只有多个容器抢 CPU 时才按 share 比例分:

docker run -d --name a --cpu-shares 1024 stress-ng --cpu 4
docker run -d --name b --cpu-shares 512 stress-ng --cpu 4

CPU 打满时,a 分到的 CPU 时间大约是 b 的两倍。对应的 cgroup 文件:

# cgroup v2 会做一次换算,1024 对应 weight 100
docker exec a cat /sys/fs/cgroup/cpu.weight
# cgroup v1 直接是 shares 值
docker exec a cat /sys/fs/cgroup/cpu/cpu.shares

--cpuset-cpus:绑核​

# 只用第 0、2 两个物理核
docker run -d --name pinned --cpuset-cpus 0,2 nginx

验证:

docker exec pinned cat /sys/fs/cgroup/cpuset.cpus.effective   # v2
docker exec pinned cat /sys/fs/cgroup/cpuset/cpuset.cpus # v1
三个参数怎么选
  • 想给容器封顶,用 --cpus,语义最直观。
  • 一堆容器混跑、要分优先级,叠加 --cpu-shares。
  • 只有明确的性能调优需求(比如 DPDK、低延迟服务)才用 --cpuset-cpus,平时别绑,绑了反而降低调度灵活性。

Memory 限制​

-m / --memory:内存硬上限​

# 最多用 512MB 内存
docker run -d --name mem-limit -m 512m nginx

容器内存用到上限还继续申请,就会触发 OOM(下一节细说)。验证:

docker exec mem-limit cat /sys/fs/cgroup/memory.max            # v2
docker exec mem-limit cat /sys/fs/cgroup/memory/memory.limit_in_bytes # v1
536870912

--memory-swap:内存 + swap 的总上限​

--memory-swap 的值是「内存 + swap」加起来的上限,不是单独的 swap 大小,这点很容易搞错:

设置效果
-m 512m,不设 swapswap 上限等于内存上限,即可用 512MB swap
-m 512m --memory-swap 1g内存 512MB + swap 512MB
-m 512m --memory-swap 512m禁用 swap(总上限等于内存上限)
-m 512m --memory-swap -1swap 不限
宿主机没开 swap 会报警告

如果宿主机本身没启用 swap,设置了 --memory-swap 会在启动容器时看到 WARNING: Your kernel does not support swap limit capabilities。要么给宿主机开 swap,要么干脆用 --memory-swap 等于 --memory 的方式禁用容器 swap——对大多数服务来说禁用 swap 反而是好事,宁可 OOM 也不要慢。

--memory-reservation:软限制​

-m 是硬上限,碰到就杀;--memory-reservation 是软限制,平时可以超,只有宿主机内存紧张时 Docker 才会把容器压回软限制以内。生产上一般只配硬上限就够了,软限制用不好反而难排查。

OOM​

容器内存超过 -m 上限,Linux 的 OOM Killer 会直接杀掉容器里的进程。表现就是容器突然退出,日志里往往没有任何应用层的报错。

确认是不是 OOMKilled​

第一招,docker inspect 看状态字段:

docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' mem-limit
true 137

OOMKilled 为 true、退出码 137(128 + 9,被 SIGKILL 杀掉),基本就能确定是 OOM。

第二招,看内核日志,宿主机上执行:

dmesg -T | grep -i -E 'oom|killed process'
[Oct 22 10:15:30] Memory cgroup out of memory: Killed process 12345 (java) total-vm:...

第三招,docker events 能实时看到 oom 事件:

docker events --filter event=oom

怎么处理​

  1. 先确认应用真实的内存需求,把 -m 调大。
  2. 如果是 Java 应用,注意 JVM 堆要和容器限制配合:JDK 8u191+ / JDK 10+ 能识别容器内存上限,用 -XX:MaxRAMPercentage=75.0 留出堆外空间。
  3. Go、Node 这类运行时不读 cgroup 的,要么升级版本,要么自己算好内存参数。
不要随手用 --oom-kill-disable

--oom-kill-disable 是禁止 OOM Killer 杀这个容器。听着很美好,实际效果是容器内存超限后进程被无限挂起(处于 D 状态),既不工作也不退出,比直接被杀更难排查。生产环境别用,正确做法是配好内存上限 + 配置重启策略。

PID Limit​

容器里的进程数默认不受限,一个 fork 炸弹就能把宿主机的 PID 耗光,影响整机。用 --pids-limit 给容器内进程数封顶:

docker run -d --name pid-limit --pids-limit 200 nginx

容器里起第 201 个进程时会报 Resource temporarily unavailable。验证当前用量和上限:

docker exec pid-limit cat /sys/fs/cgroup/pids.current   # 当前进程数,v2
docker exec pid-limit cat /sys/fs/cgroup/pids.max # 上限,v2
# v1 对应 /sys/fs/cgroup/pids/pids.current 和 pids.max

也可以改 daemon 级别的默认值,让所有新容器默认受限:

{
"default-pids-limit": 1024
}
建议给所有容器都设上限

Web 服务、API 服务这类正常进程数很少的容器,设 200~512 足够;跑 CI 构建、起大量子进程的容器可以放宽。有上限兜底,比出事后再排查省心得多。

Ulimit​

ulimit 限制的是单个进程的资源:能打开多少文件、能起多少进程、core dump 大小等。宿主机上用 ulimit -a 看,容器里默认继承 dockerd 的一套默认值,可以用 --ulimit 单独改:

# 提高单进程可打开文件数:软限制 65535,硬限制 65535
docker run -d --name ulimit-demo --ulimit nofile=65535:65535 nginx

# 限制容器内单进程最多 fork 512 个进程/线程
docker run -d --ulimit nproc=512 nginx

# 允许生成 core dump(配合调试用)
docker run -d --ulimit core=-1 ubuntu:24.04 sleep infinity

进容器验证:

docker exec ulimit-demo sh -c 'ulimit -n'
65535

常用的 ulimit 项:

项含义说明
nofile单进程最大打开文件数高并发服务最常调的一项
nproc单用户最大进程/线程数防 fork 炸弹的另一道闸
corecore dump 文件大小上限默认 0,排查崩溃时可临时放开
memlock可锁定内存大小数据库、ES 这类禁用 swap 的服务需要
stack线程栈大小一般不用动
容器 ulimit 和宿主机 ulimit 的关系

两者是独立的。容器默认值来自 dockerd(daemon.json 里的 default-ulimits),不是从宿主机 shell 继承。改了宿主机的 /etc/security/limits.conf 不会影响到容器,要给容器调就用 --ulimit 或者 daemon 配置。另外注意:nofile 只是单进程上限,一个容器 200 个进程、每个开 65535 个 fd,照样能把宿主机 inode/fd 耗尽,所以要和 --pids-limit 配合着用。

Docker Logging Driver​

容器的标准输出、标准错误由日志驱动接管。选哪个驱动,取决于日志最终要去哪。

驱动写入位置特点适用场景
json-file宿主机 JSON 文件默认驱动,docker logs 直接可用单机、小规模
local宿主机二进制文件高性能、占用小,支持轮转单机且量大
journaldsystemd journal和系统日志统一管理systemd 体系的发行版
syslog转发到 syslog 服务接入传统日志体系老运维体系
fluentd转发到 Fluentd结构化日志管道的标配日志进 Elasticsearch 等
gelf转发到 GELF 端点Graylog 生态Graylog 用户
awslogs / gcplogs云厂商日志服务免自建云上环境
none丢弃不存日志完全外置采集时用
非本地驱动时 docker logs 不可用

用 fluentd、syslog 这类转发型驱动后,docker logs 默认是看不到日志的。Docker 20.10+ 有个折中方案:daemon.json 里开启 dual logging cache,转发的同时在本地留一份缓存供 docker logs 读。

全局配置:daemon.json​

{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"labels": "service",
"env": "ENV"
}
}

改完 systemctl reload docker 生效,只对之后新建的容器生效。

单容器配置:--log-driver / --log-opt​

docker run -d --name web \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=5 \
nginx

max-size=10m 表示单个日志文件到 10MB 就轮转,max-file=5 表示最多保留 5 份(含当前文件)。两个参数必须一起用,只设 max-file 不生效。

json-file 不轮转会把磁盘打满

默认配置下 json-file 不轮转,一个爱打日志的服务跑几个月,单文件能长到几十 GB,直接把 /var/lib/docker 所在分区写满,然后整机的容器一起出问题。生产环境必须配 max-size + max-file,或者用 local 驱动(默认 100MB × 5 轮转)。

日志采集​

json-file 的文件位置​

json-file 驱动下,每个容器的日志都在宿主机固定路径:

ls /var/lib/docker/containers/<容器ID>/<容器ID>-json.log

一行一条 JSON,包含 log、stream(stdout/stderr)、time 三个字段。知道这个位置有两个用处:磁盘告警时能定位是哪个容器写的;采集器(filebeat 等)可以直接 tail 这个目录。

三种常见采集思路​

1. filebeat 直接读文件。在宿主机部署 filebeat,配置 input 为 /var/lib/docker/containers/*/*.log,解析 JSON 后送 Elasticsearch / Kafka。优点是简单、不依赖 Docker 转发;缺点是要自己处理容器元数据(名字、标签),可以靠 filebeat 的 add_docker_metadata 处理器补全。

2. 换 fluentd 日志驱动。容器日志直接推给 Fluentd:

docker run -d --name app \
--log-driver fluentd \
--log-opt fluentd-address=127.0.0.1:24224 \
--log-opt tag=docker.app \
myapp

Fluentd 端再做过滤、富化、分发。优点是链路清晰、元数据天然带全;缺点是要保证 Fluentd 先启动(或者加 fluentd-async 异步写,避免 Fluentd 挂了阻塞容器)。

3. Sidecar / DaemonSet 模式(K8s 环境)。每个节点跑一个采集器 DaemonSet,挂载宿主机日志目录采集,这是 Kubernetes 集群的标准做法,单机 Docker 用不到。

采集前先把应用日志规范好

采集器再强也救不了混乱的日志。应用侧尽量做到:日志写 stdout/stderr 而不是容器内文件;单行 JSON 或固定格式;带上 traceId。这样不管换哪种采集方案都不用大改。

Rootless Docker​

默认的 Docker 架构里,dockerd 以 root 运行,容器内 root 就是宿主机 root(只是被 namespace 隔离)。一旦容器逃逸,拿到的就是宿主机 root 权限。Rootless 模式让 dockerd 和容器都跑在普通用户下,即使逃逸成功,拿到的也只是这个普通用户的权限,破坏面小得多。

原理上靠两点:

  • 用户命名空间(user namespace):容器内的 root(uid 0)映射到宿主机的一个普通 uid(比如 100000+),在宿主机看来没有任何特权。
  • 无特权组件:网络用 slirp4netns 代替 veth/bridge,cgroup 用 systemd user slice 管理。

安装要点(以 Ubuntu 为例):

# 1. 装依赖
sudo apt install uidmap dbus-user-session fuse-overlayfs slirp4netns

# 2. 确保子 uid/gid 映射存在
grep ^$USER /etc/subuid /etc/subgid
# 没有就加:sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

# 3. 用官方包里的脚本安装(Docker 已装好的情况下)
dockerd-rootless-setuptool.sh install

# 4. 配置环境变量(脚本输出会提示)
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

# 5. 让用户 linger,关闭会话后 dockerd 不退出
sudo loginctl enable-linger $USER
Rootless 的限制
  • 容器不能绑定 1024 以下端口(除非放开 net.ipv4.ip_unprivileged_port_start)。
  • 网络走用户态转发,性能略低于 bridge 模式。
  • 资源限制依赖 cgroup v2 + systemd,老系统(cgroup v1)下 --cpus、-m 这些可能不生效。
  • 数据在 ~/.local/share/docker,和 rootful 的 /var/lib/docker 完全隔离,镜像要重新拉。

Linux Capability​

Linux 把 root 的特权拆成了 40 来个 capability,比如绑定低端口、改系统时间、加载内核模块各是一项。Docker 默认给容器的只是其中一小部分,够日常使用但远不是完整 root。

默认 capabilities​

默认包含的比较重要的几项:

Capability作用
NET_BIND_SERVICE绑定 1024 以下端口
CHOWN / DAC_OVERRIDE改文件属主、绕过文件权限检查
FSETID / FOWNER文件权限相关
KILL向任意进程发信号
SETUID / SETGID切换 uid/gid(su 类操作)
SETPCAP修改进程 capability
NET_RAW使用 raw socket(ping 需要)
MKNOD创建设备文件
AUDIT_WRITE写审计日志

默认不包含的有 SYS_ADMIN(mount、命名空间操作,接近完整 root)、NET_ADMIN(改网络配置)、SYS_TIME(改时间)、SYS_MODULE(加载内核模块)等。

查看容器实际持有的 capability:

docker exec mycontainer sh -c 'grep Cap /proc/self/status'
# 或用 capsh 解码
docker exec mycontainer capsh --decode=$(grep CapEff /proc/self/status | awk '{print $2}')

--cap-add / --cap-drop​

最小权限原则:先全丢,再按需加回。

# 典型例子:Web 服务只需要绑 80/443,其余全不要
docker run -d --name minimal-web \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
nginx
# 需要修改容器内 iptables(如一些 VPN 镜像)
docker run -d --cap-add NET_ADMIN myimage
--privileged 等于放弃防护

--privileged 会给容器全部 capability、放开设备访问、关闭 seccomp/AppArmor 限制,基本等于把宿主机交给容器。除非明确知道镜像需要什么(比如 Docker-in-Docker 的构建场景),否则不要用。能列出来的需求就用 --cap-add 精确加,比 privileged 安全得多。

Seccomp​

Capability 管「能不能做这类操作」,seccomp 管「能不能调这个系统调用」。Linux 有 300 多个系统调用,Docker 的默认 seccomp profile 放行了大部分常用的,拦截了 40 多个危险或容器里用不到的,比如 mount、reboot、kexec_load、keyctl。

查看默认 profile 是否生效(Seccomp: 2 表示 filter 模式启用):

docker exec mycontainer grep Seccomp /proc/self/status
Seccomp:	2
Seccomp_filters: 1

自定义 profile​

默认 profile 内容很长(Docker 仓库里的 profiles/seccomp/default.json),自定义一般是基于它增删。最小示例:

{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "openat", "close", "exit", "exit_group"],
"action": "SCMP_ACT_ALLOW"
}
]
}

使用时通过 --security-opt 指定:

docker run -d --security-opt seccomp=my-seccomp.json myapp

SCMP_ACT_ERRNO 作为默认动作表示「不在白名单里的调用一律返回错误」。拦截的调用在 dmesg 里能看到 audit 日志。

不要为了省事用 seccomp=unconfined

老教程里经常能看到 --security-opt seccomp=unconfined 用来「解决」某个报错,这等于完全关掉 seccomp。正确做法是查清楚应用需要哪个系统调用,在自定义 profile 里精确放行。Chrome headless 这类特殊场景可以用 Docker 官方维护的 chrome.json。

AppArmor​

AppArmor 是 Ubuntu / Debian 系默认开启的 MAC(强制访问控制)模块,通过路径做访问控制:即使进程有权限,profile 不允许的路径也不让碰。dockerd 启动时会自动生成并加载一个叫 docker-default 的 profile,新容器默认套上它,限制了比如挂载文件系统、读写 /proc、/sys 敏感文件等操作。

查看宿主机 AppArmor 状态:

sudo aa-status
apparmor module is loaded.
39 profiles are in enforce mode.
docker-default
...

查看某个容器用的 AppArmor profile:

docker inspect --format '{{.AppArmorProfile}}' mycontainer
docker-default

自定义 profile 加载后用 --security-opt apparmor=... 指定:

sudo apparmor_parser -r -W /etc/apparmor.d/my-profile
docker run -d --security-opt apparmor=my-profile myapp
AppArmor 报错排查

容器报 can't set AppArmor profile 之类错误时,先确认内核是否启用 AppArmor(cat /sys/module/apparmor/parameters/enabled,输出 Y 才是开了),再看 profile 是否已加载。容器内进程的 profile 可以看 /proc/<PID>/attr/current。

SELinux​

SELinux 是 RHEL / CentOS / Fedora 系的 MAC 模块,思路和 AppArmor 不同:它给文件、进程、端口都打上安全标签(context),用策略定义「什么标签的进程能访问什么标签的资源」,基于标签而不是路径。Docker 在开启 SELinux 的系统上会用 container_t 类型约束容器进程,即使容器逃逸拿到 root,SELinux 策略也不允许它乱读宿主机文件。

查看状态:

getenforce      # Enforcing / Permissive / Disabled
sestatus # 详细信息

查看容器进程的 SELinux 标签:

docker exec mycontainer cat /proc/self/attr/current
ps -eZ | grep container
system_u:system_r:container_t:s0:c123,c456

挂载卷的 :z / :Z 大坑​

这是 SELinux 系统上用 Docker 最常踩的坑:把宿主机目录挂载进容器后,容器里读写报 Permission denied,宿主机上看文件权限明明没问题——因为宿主机目录的 SELinux 标签是 user_home_t 之类的类型,container_t 进程无权访问。

解决办法是在挂载时让 Docker 重新打标签:

# :z 多容器共享的标签(container_file_t,多个容器都能访问)
docker run -d -v /srv/data:/data:z myapp

# :Z 独占标签(带 MCS 类别,只有这一个容器能访问)
docker run -d -v /srv/data:/data:Z myapp
:z/:Z 会修改宿主机文件标签,别对系统目录用

这两个选项会递归 relabel 宿主机目录。如果对 /、/etc、$HOME 这种目录用了 :z,会把整个目录树的标签改成容器标签,导致宿主机上的其他服务(sshd、桌面环境等)读不了自己的文件。修复代价很大(restorecon -R 甚至要重打全系统标签)。只对专门给容器用的数据目录加 :z/:Z。

判断 SELinux 是不是拦截源,可以把模式临时切成 Permissive 复测,或查审计日志:

sudo ausearch -m avc -ts recent

ReadOnly RootFS​

容器的最佳实践之一:根文件系统只读,应用能写的地方只有明确挂载的卷和 tmpfs。好处是防止恶意进程落地后门、改应用文件,也让「容器内不该有状态」这个约定变成硬约束。

docker run -d --name ro-web --read-only nginx

只读后 nginx 起不来,因为它要写 /var/cache/nginx、/var/run。缺什么补什么,用 tmpfs 挂内存盘:

docker run -d --name ro-web \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=32m \
--tmpfs /var/run:rw,noexec,nosuid,size=8m \
nginx

tmpfs 的几个挂载选项值得固定写成习惯:noexec(不允许执行,防落地木马)、nosuid(忽略 suid 位)、size(内存盘大小上限,防止写爆内存)。

怎么知道要挂哪些 tmpfs

先只加 --read-only 跑起来,看容器日志里报哪个目录写不进去,逐个补 tmpfs。有持久化需求的数据(比如数据库文件)不能用 tmpfs,挂 named volume。应用在容器里写本地文件本身就该被审视:临时文件进 tmpfs,持久数据进卷,除此之外不该有别的写入。

Docker Metrics​

docker stats:实时看板​

docker stats                 # 所有运行中容器,持续刷新
docker stats --no-stream # 打一次快照退出,适合脚本

输出 CPU 百分比、内存用量/上限、网络 IO、块设备 IO、进程数:

CONTAINER ID   NAME     CPU %   MEM USAGE / LIMIT   MEM %   NET I/O        BLOCK I/O   PIDS
a1b2c3d4e5f6 web 0.50% 120MiB / 512MiB 23.4% 1.2MB / 800kB 0B / 0B 25

docker stats 的数据来自 cgroup,不用额外装任何东西。缺点是只能看当前值,没有历史,也没有告警——要历史数据就得走 metrics 端点 + Prometheus 这条路。

daemon 的 /metrics 端点​

Docker daemon 本身可以暴露 Prometheus 格式的指标(容器数量、镜像数、构建指标、engine 健康状态等),需要手动开:

{
"metrics-addr": "127.0.0.1:9323",
"experimental": true
}

重启 dockerd 后验证:

curl -s http://127.0.0.1:9323/metrics | head
# HELP builder_builds_failed_total Number of failed image builds
# TYPE builder_builds_failed_total counter
builder_builds_failed_total{reason="buildCanceled"} 0
...
两个注意点

一是这个端点目前还在 experimental,所以必须开 experimental: true;二是 metrics-addr 建议绑 127.0.0.1,由本机的 Prometheus 来抓,直接暴露到局域网等于把主机信息白送人。另外它暴露的是 daemon 自己的指标,不是每个容器的 CPU/内存——容器级指标要靠下一节的 cAdvisor。

Prometheus 监控 Docker​

Prometheus 通过 pull 模型定期抓目标的 /metrics。抓 Docker daemon 的配置很直接:

scrape_configs:
- job_name: 'docker-daemon'
scrape_interval: 15s
static_configs:
- targets: ['127.0.0.1:9323']

抓取成功后,在 Prometheus 的 Targets 页能看到 docker-daemon 处于 UP 状态。常用的 daemon 指标:

指标含义
engine_daemon_container_states_containers各状态(running/paused/stopped)容器数
engine_daemon_health_checks_failed_total健康检查失败次数
builder_builds_failed_total镜像构建失败数
engine_daemon_network_actions_seconds_count网络操作次数

举个告警例子:有容器健康检查失败就报警。

- alert: DockerHealthCheckFailing
expr: increase(engine_daemon_health_checks_failed_total[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "Docker 容器健康检查持续失败"
Prometheus 自己也可以跑在容器里

Prometheus 部署在容器里时,抓取宿主机的 daemon 指标要把 targets 写成宿主机 IP(Linux 上是 172.17.0.1 或自定义网桥网关地址),写 127.0.0.1 指的是 Prometheus 容器自己。同时 daemon.json 的 metrics-addr 要绑到对应网桥地址,不能只绑 lo。

cAdvisor​

daemon 指标里没有每个容器的 CPU/内存明细,这部分交给 cAdvisor(Container Advisor)。它是 Google 开源的容器监控组件,直接读 cgroup,按容器维度暴露资源指标,是「Prometheus 监控容器」的标准答案。

部署​

docker run 方式:

docker run -d --name cadvisor \
--restart=always \
-p 8080:8080 \
-v /:/rootfs:ro \
-v /var/run:/var/run:ro \
-v /sys:/sys:ro \
-v /var/lib/docker/:/var/lib/docker:ro \
gcr.io/cadvisor/cadvisor:v0.49.1

Compose 方式:

services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
container_name: cadvisor
restart: always
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro

启动后访问 http://宿主机:8080 有自带的 Web UI 能直接看图表,暴露的指标在 http://宿主机:8080/metrics。

接入 Prometheus​

scrape_configs:
- job_name: 'cadvisor'
scrape_interval: 15s
static_configs:
- targets: ['<宿主机IP>:8080']

常用指标(查询时一般用 name=~".+" 过滤出容器,排除 cgroup 根路径的噪音):

指标含义
container_cpu_usage_seconds_total容器累计 CPU 使用时间(用 rate() 算使用率)
container_memory_working_set_bytes容器当前工作集内存(接近 docker stats 的 MEM USAGE)
container_network_receive_bytes_total / transmit网络收发包
container_fs_usage_bytes容器文件系统占用

配 Grafana 时搜 cadvisor / docker 关键词有现成的 dashboard(比如 ID 193 的「Docker monitoring」),导入即用。

cAdvisor 本身也有开销

容器数量多(几百个)时 cAdvisor 的 CPU 占用会比较明显,可以适当调大 --housekeeping_interval(默认 1s 采集间隔)。另外 8080 端口不要对公网开放,指标里包含主机和容器的详细信息。

Docker 安全最佳实践​

最后把这篇和前面几篇的安全要点汇总成一份清单,上线前逐条过一遍:

镜像与构建

  • 用最小化基础镜像(alpine、distroless),镜像里不带 shell 和多余工具,攻击者进来了也没东西可用。
  • 固定镜像 tag 或用 digest 锁定,不用 latest 上生产。
  • 定期扫镜像漏洞(Trivy、Grype),CI 里卡阈值。
  • 不往镜像里塞密钥,secret 走 build secret 或运行时注入。

容器运行

  • 容器内应用不以 root 运行(Dockerfile 里 USER 切普通用户)。
  • capability 最小化:--cap-drop ALL 起步,按需 --cap-add。
  • 根文件系统只读(--read-only),临时目录用 tmpfs。
  • 设置资源限制(-m、--cpus、--pids-limit),防单个容器耗尽整机资源。
  • 不挂宿主机的 docker.sock 进容器——挂着它的容器等于拥有整台宿主机。CI 需要构建镜像时优先用 rootless 构建器或远程 builder。
  • 不给容器 --privileged,不放行 --net host 除非确有必要。
  • seccomp、AppArmor / SELinux 保持默认开启,不用 unconfined 糊事。
  • SELinux 系统挂载卷记得加 :z / :Z。

网络与日志

  • 容器间通信用自定义网络,按业务隔离网段;不用的端口不映射。
  • 日志配置轮转(max-size + max-file),磁盘告警接到监控系统。

守护进程与宿主机

  • 敏感环境评估 rootless 模式或 user namespace 重映射(userns-remap)。
  • daemon 的 metrics 端口、远程 API 端口(2375/2376)不对公网暴露,远程访问必须走 TLS。
  • docker 组等价于 root 权限,加入这个组的用户要严格控制。
把清单变成默认配置

能写进 daemon.json 的就写进去(日志轮转、默认 pids-limit、默认 ulimits),能写进 compose 模板的就固化下来(read_only、cap_drop、deploy.resources)。靠人记的清单总会漏,靠默认配置兜底的不会。