强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Dockerfile 写作精讲 / 08 - USER 与权限

08 - USER 与权限:非 root 运行与权限管理

8.1 为什么需要非 root 运行

默认情况下,Docker 容器以 root 用户运行。这带来严重的安全风险:

风险 说明
容器逃逸 如果存在内核漏洞,root 用户更容易实现容器逃逸
文件系统破坏 root 可以修改容器内任何文件
权限提升 攻击者获得容器 root 权限后可进一步利用
合规要求 安全基线(如 CIS Docker Benchmark)要求非 root 运行

最佳实践:始终在 Dockerfile 中使用 USER 指令切换到非 root 用户。

8.2 USER 指令详解

基本语法

# 使用用户名
USER appuser

# 使用 UID:GID
USER 1001:1001

# 使用用户名:组名
USER appuser:appgroup

完整示例

FROM node:20-alpine

# 创建用户和组
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

# 复制文件并设置权限
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --production

COPY --chown=appuser:appgroup . .

# 切换到非 root 用户
USER appuser

CMD ["node", "server.js"]

8.3 各基础镜像创建用户的方式

Alpine

FROM alpine:3.19

# -G: 指定组  -s: shell  -D: 不创建密码  -H: 不创建主目录
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D -H appuser

USER appuser

Debian/Ubuntu

FROM ubuntu:22.04

# --no-create-home: 不创建主目录  --shell: 指定 shell
RUN groupadd -g 1001 appgroup && \
    useradd -u 1001 -g appgroup --no-create-home --shell /bin/bash appuser

USER appuser

使用数字 UID

# 推荐在生产环境使用数字 UID(避免用户名查找开销)
USER 1001:1001

注意:使用数字 UID 时,文件的 chown 也应使用数字,确保一致性。

8.4 文件所有权管理

COPY –chown

# ✅ 复制时设置所有权(不产生额外层)
COPY --chown=appuser:appgroup . /app/

# ✅ 使用数字 UID/GID
COPY --chown=1001:1001 . /app/

# ❌ 先复制再 chown(产生额外层)
COPY . /app/
RUN chown -R appuser:appgroup /app/

多阶段构建中的权限处理

FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
# nonroot 镜像自带 UID 65532 的用户
COPY --from=builder /server /server
# 直接使用 nonroot 用户
USER nonroot:nonroot
ENTRYPOINT ["/server"]

需要写入的目录

FROM node:20-alpine

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

# 应用代码(只读)
COPY --chown=appuser:appgroup . .

# 创建需要写入的目录并设置权限
RUN mkdir -p /app/data /app/logs && \
    chown -R appuser:appgroup /app/data /app/logs

USER appuser

# 数据和日志可以通过 volume 挂载
VOLUME ["/app/data", "/app/logs"]

CMD ["node", "server.js"]

8.5 权限提升场景

临时使用 root

FROM node:20-alpine

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app
COPY . .

# 安装需要 root 权限的系统包
USER root
RUN apk add --no-cache tini

# 切回非 root 用户
USER appuser

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

使用 gosu 降权

FROM postgres:16

# entrypoint 脚本以 root 启动,完成初始化后降权到 postgres 用户
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]
#!/bin/bash
# docker-entrypoint.sh 片段
set -e

# 以 root 执行初始化
if [ "$1" = 'postgres' ]; then
    # 初始化数据库...
    chown -R postgres /var/lib/postgresql/data
    
    # 使用 gosu 降权到 postgres 用户执行主进程
    exec gosu postgres "$@"
fi

exec "$@"

8.6 Init 系统与进程管理

为什么不直接用 root

# ❌ 以 root 运行 node
CMD ["node", "server.js"]

# ✅ 使用 tini + 非 root
USER appuser
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]

僵尸进程问题

# Python 多进程应用需要 tini 回收僵尸进程
FROM python:3.12-slim
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt

USER appuser
ENTRYPOINT ["tini", "--"]
CMD ["python", "app.py"]

8.7 Volume 与权限

挂载目录的权限问题

# 问题:volume 以 root 权限挂载,非 root 用户无法写入
docker run -v /host/data:/app/data myapp
# Permission denied

# 解决方案一:主机端修改目录权限
chown 1001:1001 /host/data
docker run -v /host/data:/app/data myapp

# 解决方案二:容器内使用 entrypoint 脚本修复权限
# 见下文

Entrypoint 脚本修复 Volume 权限

#!/bin/bash
set -e

# 如果以 root 启动,修复权限后降权
if [ "$(id -u)" = '0' ]; then
    chown -R appuser:appgroup /app/data
    exec gosu appuser "$@"
fi

exec "$@"
FROM node:20-alpine
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app
COPY . .
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

VOLUME ["/app/data"]
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server.js"]

8.8 安全基线检查

检查容器运行用户

# 查看容器以什么用户运行
docker inspect --format='{{.Config.User}}' mycontainer

# 查看容器内进程的用户
docker top mycontainer

# 在运行中的容器内检查
docker exec mycontainer id
docker exec mycontainer whoami

CIS Docker Benchmark 相关规则

规则 说明
4.1 确保容器内以非 root 用户运行
4.2 使用可信的基础镜像
4.6 确保 HEALTHCHECK 指令存在
4.7 不要在容器中安装不必要的软件包
5.12 限制容器的内存和 CPU

8.9 业务场景

场景一:Web 应用

FROM nginx:alpine

# 创建非 root 用户
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

# 修改 Nginx 以非 root 运行
RUN sed -i 's/listen 80/listen 8080/' /etc/nginx/conf.d/default.conf && \
    sed -i 's/user  nginx/user  appuser/' /etc/nginx/nginx.conf && \
    chown -R appuser:appgroup /var/cache/nginx /var/log/nginx /etc/nginx/conf.d && \
    touch /var/run/nginx.pid && \
    chown appuser:appgroup /var/run/nginx.pid

COPY --chown=appuser:appgroup dist/ /usr/share/nginx/html/

USER appuser

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

CMD ["nginx", "-g", "daemon off;"]

场景二:数据库初始化

FROM postgres:16

COPY init.sql /docker-entrypoint-initdb.d/

# Postgres 官方镜像已处理好权限
# postgres 用户在 entrypoint 中创建
# 数据目录由 entrypoint 脚本管理

8.10 常见错误与排查

错误 原因 解决方案
Permission denied 非 root 用户无权访问文件 使用 --chownchown
bind: permission denied 非 root 无法绑定 < 1024 端口 使用 1024 以上端口
Volume 写入失败 挂载目录权限不匹配 修复主机目录权限或使用 entrypoint
用户不存在 未在基础镜像中创建 先创建用户再 USER 切换
PID 文件创建失败 非 root 无法写入运行目录 修改目录权限或使用 tmpfs

8.11 扩展阅读


上一章07 - EXPOSE 与端口 下一章09 - 多阶段构建 — 分阶段编排、构建缓存与最小化镜像。