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 用户无权访问文件 | 使用 --chown 或 chown |
bind: permission denied | 非 root 无法绑定 < 1024 端口 | 使用 1024 以上端口 |
| Volume 写入失败 | 挂载目录权限不匹配 | 修复主机目录权限或使用 entrypoint |
| 用户不存在 | 未在基础镜像中创建 | 先创建用户再 USER 切换 |
| PID 文件创建失败 | 非 root 无法写入运行目录 | 修改目录权限或使用 tmpfs |
8.11 扩展阅读
上一章:07 - EXPOSE 与端口 下一章:09 - 多阶段构建 — 分阶段编排、构建缓存与最小化镜像。