运维、安全与监控
这一篇偏运维向:容器跑起来之后,怎么限制它吃资源、日志怎么管、安全怎么加固、出了问题看什么指标。内容按「资源限制 → 日志 → 安全 → 监控 → 安全清单」的顺序展开,每一节都可以单独翻。
文中命令基于 Linux 宿主机 + Docker Engine 24+。涉及 cgroup 路径时会同时标注 v1 和 v2 的写法,可以用 stat -fc %T /sys/fs/cgroup/ 查看宿主机用的是哪个版本:输出 cgroup2fs 是 v2,输出 tmpfs 是 v1。
CPU 限制
Docker 通过 cgroup 限制容器能用的 CPU。常用三个参数,解决的问题不一样:
| 参数 | 作用 | 适用场景 |
|---|---|---|
--cpus | 限制最多使用多少个 CPU(可以是小数) | 最常用的硬限制 |
--cpu-shares | CPU 争抢时的相对权重,默认 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,不设 swap | swap 上限等于内存上限,即可用 512MB swap |
-m 512m --memory-swap 1g | 内存 512MB + swap 512MB |
-m 512m --memory-swap 512m | 禁用 swap(总上限等于内存上限) |
-m 512m --memory-swap -1 | 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
怎么处理
- 先确认应用真实的内存需求,把
-m调大。 - 如果是 Java 应用,注意 JVM 堆要和容器限制配合:JDK 8u191+ / JDK 10+ 能识别容器内存上限,用
-XX:MaxRAMPercentage=75.0留出堆外空间。 - Go、Node 这类运行时不读 cgroup 的,要么升级版本,要么自己算好内存参数。
--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 炸弹的另一道闸 |
core | core dump 文件大小上限 | 默认 0,排查崩溃时可临时放开 |
memlock | 可锁定内存大小 | 数据库、ES 这类禁用 swap 的服务需要 |
stack | 线程栈大小 | 一般不用动 |
两者是独立的。容器默认值来自 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 | 宿主机二进制文件 | 高性能、占用小,支持轮转 | 单机且量大 |
journald | systemd journal | 和系统日志统一管理 | systemd 体系的发行版 |
syslog | 转发到 syslog 服务 | 接入传统日志体系 | 老运维体系 |
fluentd | 转发到 Fluentd | 结构化日志管道的标配 | 日志进 Elasticsearch 等 |
gelf | 转发到 GELF 端点 | Graylog 生态 | Graylog 用户 |
awslogs / gcplogs | 云厂商日志服务 | 免自建 | 云上环境 |
none | 丢弃 | 不存日志 | 完全外置采集时用 |
用 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 不轮转,一个爱打日志的服务跑几个月,单文件能长到几十 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
- 容器不能绑定 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 会给容器全部 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 日志。
老教程里经常能看到 --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
容器报 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
这两个选项会递归 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(内存盘大小上限,防止写爆内存)。
先只加 --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 部署在容器里时,抓取宿主机的 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 的 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)。靠人记的清单总会漏,靠默认配置兜底的不会。