目录
Docker 安全最佳实践教程:让容器更安全的 10 种方法
容器提供了一定程度的隔离,但并不保证安全。以下是从开发到部署都应采取的基本防护措施,每一项都附有实际运行结果。
1. 不要以 root 身份运行
$ docker run --rm alpine:3.23 id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
$ docker run --rm -u 10001:10001 alpine:3.23 id
uid=10001 gid=10001 groups=10001
$ docker run --rm node:24-alpine id node
uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
- 容器内默认以 root(uid=0)身份运行。如果程序存在漏洞并遭到入侵,攻击者在容器内就拥有最高权限,再结合其他漏洞,就有可能影响主机。
-u 10001:10001可以在运行时指定用户,无需修改镜像。- 许多官方镜像已经内置普通用户,例如
node镜像中的node(uid 1000),以及 distroless 镜像中的nonroot。
最佳做法是在 Dockerfile 中切换用户:
RUN adduser -D -u 10001 app # Alpine
# RUN useradd --system --uid 10001 app # Debian / Ubuntu
USER app
实际示例请参阅 Python 应用 和 Node.js 应用。
2. 使用可信的基础镜像并固定版本
- 优先选择 Docker Official Images(例如
python、node、nginx)或知名组织发布的镜像。 - 不要使用来源不明的镜像,尤其是名称与热门镜像非常相似的镜像(例如
ngnix)。 - 固定版本,例如
node:24-alpine或更精确的node:24.21-alpine,不要使用node或latest。要完全固定版本,可以加上 digest:node:24-alpine@sha256:...。
3. 尽量缩小镜像
镜像中的每个软件包都可能存在漏洞。使用 slim、alpine、distroless 或多阶段构建,只保留运行所需的内容。不需要的工具(编译器、curl、ssh)越少,攻击者可利用的东西就越少,漏洞扫描结果也会更干净。
4. 不要把机密放进镜像
实测:机密会留在镜像中
以下 Dockerfile 犯了三个常见错误:
FROM alpine:3.23
ARG NPM_TOKEN
ENV DB_PASSWORD=super-secret
COPY .env /app/.env
RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
CMD ["sh"]
最后一行删除了用过的 .env,看起来没问题。构建并检查:
$ docker build -q -f Dockerfile.bad --build-arg NPM_TOKEN=npm_abc123secret -t leaky .
sha256:c9523bb4d0484e3abf049d15dec57df3282ff8203574c03127a8dfc6ee37bf9b
$ docker run --rm leaky ls /app
$ docker inspect -f "{{.Config.Env}}" leaky
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin DB_PASSWORD=super-secret]
$ docker history --no-trunc --format "{{.CreatedBy}}" leaky | head -n 4
CMD ["sh"]
RUN |1 NPM_TOKEN=npm_abc123secret /bin/sh -c echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env # buildkit
COPY .env /app/.env # buildkit
ENV DB_PASSWORD=super-secret
ls /app没有输出,.env看起来已经被删除了。- 但
docker inspect直接显示了ENV设置的DB_PASSWORD=super-secret。 docker history显示了构建时传入的NPM_TOKEN=npm_abc123secret:ARG的值会记录在使用它的层中。
.env 呢?镜像由多个层叠加而成,rm 只会在新的一层标记“删除”,COPY 那一层的内容仍然存在。使用 docker save 导出镜像,再从各层中找出 .env:
$ docker save leaky -o leaky.tar && mkdir x && tar -xf leaky.tar -C x && for l in x/blobs/sha256/*; do tar -tzf $l 2>/dev/null | grep -q "app/.env" && tar -xzOf $l app/.env; done; rm -rf x leaky.tar
API_KEY=sk-live-1234567890abcdef
.env 的完整内容被提取出来了。任何能拉取这个镜像的人都可以这样做。推送到 Registry 的镜像,应假设所有人都能看到每一层的内容。
使用 docker build --check 尽早发现问题
docker build --check 会根据变量名称判断 ARG、ENV 是否看起来像机密:
$ docker build --check -f Dockerfile.bad . 2>&1 | sed -n "/Check complete/,\$p"
Check complete, 2 warnings have been found!
WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN")
Dockerfile.bad:2
--------------------
1 | FROM alpine:3.23
2 | >>> ARG NPM_TOKEN
3 | ENV DB_PASSWORD=super-secret
4 | COPY .env /app/.env
--------------------
WARNING: SecretsUsedInArgOrEnv - https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
Do not use ARG or ENV instructions for sensitive data (ENV "DB_PASSWORD")
Dockerfile.bad:3
--------------------
1 | FROM alpine:3.23
2 | ARG NPM_TOKEN
3 | >>> ENV DB_PASSWORD=super-secret
4 | COPY .env /app/.env
5 | RUN echo "use the token: $NPM_TOKEN" > /dev/null && rm /app/.env
--------------------
NPM_TOKEN、DB_PASSWORD 都被标记了。但它只检查 ARG、ENV 的名称,无法发现 COPY .env 这种把机密文件复制进去的问题,所以仍然需要 .dockerignore。
正确做法
- 不要在 Dockerfile 中使用
ENV或ARG传入密码、API 密钥。 - 使用
.dockerignore排除.env、私钥、.git,避免它们被COPY . .复制进去。 - 运行时需要的机密,通过环境变量、
--env-file或 Compose secrets 在运行时传入,参阅环境变量与 .env 文件。 - 构建时需要的机密(例如安装私有软件包所需的 token),使用 BuildKit 的 secret 挂载:
FROM alpine:3.23
RUN --mount=type=secret,id=npm_token \
test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
CMD ["sh"]
$ NPM_TOKEN=npm_abc123secret docker build --progress plain --no-cache --secret id=npm_token,env=NPM_TOKEN -f Dockerfile.good -t safe . 2>&1 | grep "token length"
#5 [stage-0 2/2] RUN --mount=type=secret,id=npm_token test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)"
#5 0.261 token length: 16
$ docker history --no-trunc --format "{{.CreatedBy}}" safe | head -n 2
CMD ["sh"]
RUN /bin/sh -c test -s /run/secrets/npm_token && echo "token length: $(wc -c < /run/secrets/npm_token)" # buildkit
$ docker run --rm safe ls /run/secrets
ls: /run/secrets: No such file or directory
- 构建期间,
RUN可以从/run/secrets/npm_token读取 token(长度为 16)。 docker history中只有命令本身,没有 token 的值。- 镜像中也没有
/run/secrets,secret 只在该RUN执行期间挂载。
5. 不要向外部开放不必要的端口
# 不好:数据库对全网开放
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:18
# 好:只有本机可以连接
docker run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=secret postgres:18
在 Linux 上,Docker 会直接修改 iptables 规则,-p 开放的端口会绕过 ufw、firewalld 等防火墙的设置:即使 ufw 没有允许 5432,外部仍然可以连接进来。最安全的做法是数据库、缓存等内部服务完全不使用 -p,只允许其他容器通过 Docker 网络连接,例如在 Nginx 反向代理 示例中,只有 Nginx 配置了 ports。
6. 限制容器权限
只读文件系统
$ docker run --rm --read-only alpine:3.23 touch /tmp/x
touch: /tmp/x: Read-only file system
$ docker run --rm --read-only --tmpfs /tmp alpine:3.23 sh -c "touch /tmp/x && echo ok"
ok
--read-only 会将容器文件系统设为只读,攻击者就无法修改程序或写入恶意文件。对于程序需要写入的位置(例如 /tmp),可以另外使用 --tmpfs 挂载内存中的临时空间,或挂载 Volume。
移除 Linux capabilities
在 Linux 中,root 的权限被拆分为许多 capabilities,例如 CHOWN(更改文件所有者)、NET_BIND_SERVICE(绑定 1024 以下的端口)。Docker 默认会为容器提供一部分 capabilities:
$ docker run --rm alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok
$ docker run --rm --cap-drop ALL alpine:3.23 chown nobody /etc/hostname
chown: /etc/hostname: Operation not permitted
$ docker run --rm --cap-drop ALL --cap-add CHOWN alpine:3.23 chown nobody /etc/hostname && echo "chown ok"
chown ok
- 默认情况下可以执行
chown。 - 使用
--cap-drop ALL移除所有 capabilities 后,即使是 root 也会遇到Operation not permitted。 - 使用
--cap-add只添加真正需要的 capabilities。
大多数 Web 应用不需要任何 capabilities,可以直接使用 --cap-drop ALL。
禁止提升权限
$ docker run --rm alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs: 0
$ docker run --rm --security-opt no-new-privileges alpine:3.23 grep NoNewPrivs /proc/self/status
NoNewPrivs: 1
NoNewPrivs: 1 表示容器中的进程无法通过 setuid 程序(例如 sudo、su)获得更高权限。即使以普通用户运行,也建议加上此选项。
综合使用:
docker run -d \
--user 10001:10001 \
--read-only --tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
myapp
Compose 的写法:
services:
app:
image: myapp
user: "10001:10001"
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
绝对不要随意使用 --privileged,它会为容器提供所有 capabilities,并允许容器访问主机的所有设备,几乎等于关闭了所有隔离。
7. 限制资源
$ docker run --rm --pids-limit 50 alpine:3.23 sh -c "for i in \$(seq 100); do sleep 5 & done; wait" 2>&1 | sort | uniq -c
1 sh: can't fork: Resource temporarily unavailable
$ docker run -d --name limited --memory 512m --cpus 1 --pids-limit 200 nginx:1.30-alpine
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
2edbff0c81d73b83f0055b781d8d8ad20661b2b64ef1dfd5a6def334ef02723c
$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}" limited
NAME CPU % MEM USAGE / LIMIT PIDS
limited 0.00% 3.406MiB / 512MiB 2
$ docker inspect -f "Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}} PidsLimit={{.HostConfig.PidsLimit}}" limited
Memory=536870912 NanoCpus=1000000000 PidsLimit=200
$ docker rm -f limited
limited
- 设置
--pids-limit 50时,容器尝试创建 100 个进程,超过上限后会出现can't fork: Resource temporarily unavailable。这可以防止 fork 炸弹拖垮整台主机。 --memory 512m --cpus 1会限制内存和 CPU,docker stats的LIMIT显示为 512MiB。超过内存上限的进程会被系统终止(OOM),避免整台主机内存耗尽。WARNING: Your kernel does not support swap limit capabilities表示主机内核没有启用 swap 限制。内存限制仍然有效,只是无法同时限制 swap,可以忽略此警告。
Compose 的写法:
services:
app:
image: myapp
mem_limit: 512m
cpus: 1
pids_limit: 200
8. 保护 Docker socket
/var/run/docker.sock 是 Docker 的控制接口。能够访问它的人可以创建任何容器,包括挂载主机的根目录。下面实测将主机的 / 挂载到容器中:
$ docker run --rm -v /:/host alpine:3.23 sh -c "head -n 2 /host/etc/os-release; ls /host/var/lib/docker | head -n 5"
NAME="Alpine Linux"
ID=alpine
buildkit
containerd
containers
engine-id
image
容器直接读取到了 Docker 主机的 /etc/os-release 和 /var/lib/docker(本教程的测试环境是 Alpine Linux)。同样的方法也能读写 /etc/shadow、SSH 密钥等任何文件。因此:
- 能够运行
docker命令,就等同于拥有主机的 root 权限。将用户加入docker组,就等于赋予其 root 权限。 - 不要随意将
-v /var/run/docker.sock:/var/run/docker.sock挂载给容器,除非你完全信任该镜像(例如 Traefik、Portainer、Trivy 这类需要它的工具)。 - 不要将 Docker daemon 的 TCP 端口(2375)开放到网络上,这等于公开主机的 root 权限。
- 需要更强的隔离时,可以了解 Docker 的 rootless 模式。
9. 扫描镜像漏洞
开源工具 Trivy 可以扫描镜像中操作系统软件包和编程语言软件包的已知漏洞。通过容器运行,无需安装:
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx:1.30-alpine
Report Summary
┌───────────────────────────────────┬────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────────────────┼────────┼─────────────────┤
│ nginx:1.30-alpine (alpine 3.24.2) │ alpine │ 3 │
└───────────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
nginx:1.30-alpine (alpine 3.24.2)
=================================
Total: 3 (HIGH: 3, CRITICAL: 0)
┌──────────┬─────────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────┼─────────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ libexpat │ CVE-2026-93990 │ HIGH │ fixed │ 2.8.4-r0 │ 2.8.5-r0 │ expat: Expat: XML Injection via Malformed UTF-16 Input │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-93990 │
├──────────┼─────────────────┤ │ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ pcre2 │ CVE-2026-103111 │ │ │ 10.48-r0 │ 10.49-r0 │ pcre2: pcre2: Out-of-bounds write via crafted regular │
│ │ │ │ │ │ │ expression │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-103111 │
├──────────┼─────────────────┤ │ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────┤
│ tiff │ CVE-2026-4775 │ │ │ 4.7.1-r0 │ 4.7.2-r0 │ libtiff: libtiff: Arbitrary code execution or denial of │
│ │ │ │ │ │ │ service via signed integer... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-4775 │
└──────────┴─────────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────┘
--severity HIGH,CRITICAL只显示高危及以上级别的漏洞,--quiet会隐藏下载漏洞数据库时的进度。- 挂载
docker.sock让 Trivy 读取本机镜像;trivy-cacheVolume 保存漏洞数据库,下次扫描时无需重新下载。 - 结果显示
nginx:1.30-alpine的基础 Alpine 镜像有 3 个 HIGH 级别的漏洞,Status都是fixed,并且列出了Fixed Version:也就是说 Alpine 已经发布了修复版本,但构建该镜像时还没有包含这些修复。
扫描结果会随时间变化:每天都会发现新的漏洞,镜像也会不断更新。你运行时看到的结果很可能不同。
Docker Desktop 和新版 CLI 也内置了 Docker Scout(docker scout quickview、docker scout cves),登录 Docker 账号后即可使用。建议在 CI 流程中自动扫描,发现严重漏洞时阻止部署。
10. 定期更新
基础镜像会持续修复漏洞。接着使用上面的扫描结果,在 Dockerfile 中升级软件包后重新扫描:
$ cat Dockerfile
FROM nginx:1.30-alpine
RUN apk upgrade --no-cache
$ docker build -q -t nginx-patched .
sha256:c88ea56e7062e7703dccc5eb2fedea74c333f33982d9735753056a50858646f2
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v trivy-cache:/root/.cache aquasec/trivy:latest image --quiet --scanners vuln --severity HIGH,CRITICAL nginx-patched
Report Summary
┌───────────────────────────────┬────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────────────┼────────┼─────────────────┤
│ nginx-patched (alpine 3.24.2) │ alpine │ 0 │
└───────────────────────────────┴────────┴─────────────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)
apk upgrade 将有漏洞的软件包升级到修复版本后,HIGH 和 CRITICAL 漏洞数量变成了 0。
在实际使用中,更好的做法是定期使用 docker build --pull 重新构建(--pull 会先下载基础镜像的最新版本),然后重新部署。可以使用 Dependabot、Renovate 等工具自动提醒你基础镜像有新版本。镜像只构建一次、之后几年都不更新,漏洞只会越来越多。
检查清单
- Dockerfile 中有
USER,不以 root 身份运行 - 基础镜像来源可信,版本固定
- 使用精简镜像或多阶段构建
- 镜像中没有密码、密钥、
.env(检查docker history和docker inspect) - 构建时使用
RUN --mount=type=secret传入机密 - 数据库等内部服务没有向外部开放端口
- 根据需要使用
--read-only、--cap-drop ALL、no-new-privileges - 没有使用
--privileged,也没有随意挂载 docker.sock - 设置了内存、CPU 和 PID 上限
- CI 中配置了镜像漏洞扫描
- 定期使用
--pull重新构建以获取安全更新
重点回顾
- 在 Dockerfile 中添加
USER,以非 root 用户运行。 ARG、ENV和已删除的文件仍会留在镜像中;构建时的机密应使用--secret。- 不需要对外提供服务的容器不要使用
-p;-p会绕过 ufw。 - 能够访问 docker.sock 就等同于拥有主机 root 权限。
- 定期扫描并使用
--pull重新构建。
动手练习
练习 1:使用 docker history --no-trunc 检查你自己构建的镜像,看看是否出现密码或 token?
提示
查看 CREATED BY 栏中的 ARG、ENV、RUN 命令内容,以及 docker inspect -f "{{json .Config.Env}}" 镜像。如果发现机密,应改用 --secret 或运行时环境变量,并撤销已经泄露的密钥。
常见问题
容器里的 root 就是主机的 root 吗?
通常模式下它们是同一个 uid,只是受到 namespace、capabilities、seccomp 的限制。要让它不再是主机的 root,请使用 [Rootless 模式](/zh/docker/rootless-podman/)。
镜像扫描出很多漏洞怎么办?
先处理有 Fixed Version 的高危漏洞:重新构建(docker build --pull)或升级软件包。没有修复版本的漏洞,则评估是否真的会用到相关软件包。
Docker Scout 和 Trivy 有什么区别?
它们都是漏洞扫描工具。Scout 集成在 Docker Desktop 中,需要登录;Trivy 是开源工具,可以通过容器运行。
更新于 2026年10月14日