
本文摘要文中命令与代码块可直接复制执行未实测的结论均已单独标注。文中给出了可复现的环境与操作步骤并标注了哪些结论已经验证、哪些仍属推测。一、问题与结论团队把 MySQL 从 5.7 升级到 8.0 后需要一个 Go 工具扫描所有表的 DDL确认每张表的编码已从utf8mb4MySQL 5.7 默认迁移到 8.0 引入的utf8mb4_0900_ai_ci排序规则。工具用 Docker 多阶段构建打包构建阶段基于golang:1.22约 1.2GB运行阶段基于alpine:3.19约 7MB最终镜像压到约 90MB。docker build全绿docker push成功CI 流水线没有任何告警——但部署后进程启动即退出2025/06/15 08:30:15 无法创建日志文件: open access.log: permission denied结论COPY --from复制的文件默认归属 UID:GID0:0rootUSER指令只影响其后跟随的RUN、CMD、ENTRYPOINT不追溯修改已存在文件的归属。非 root 进程在 root:root 755 的工作目录中写文件时内核返回 EACCES。二、排查与选择依据先画归属矩阵把每个路径的对象、归属和创建时机列清楚对象归属权限创建时机/app目录root:root755WORKDIR以 root 身份创建/app/server二进制root:root755COPY --from无--chown进程身份uid 1000appuser—USER指令崩溃机制四步走进程以 uid 1000 启动 → 工作目录为/app755非 root 无写权限→ 应用执行os.OpenFile(access.log, os.O_CREATE, ...)→ 内核检查 uid 1000 对/app无写权限返回 EACCES。hadolint 等 lint 工具不会报此问题——USER和COPY各自用法都没错问题在两者的交互效果。如果流水线只做到docker build docker push这个缺陷会一直潜伏到部署后才暴露。替代方案与取舍方案选择条件代价与边界--chown 目录chown通用 Linux 容器推荐默认--chown依赖 BuildKit用户名须在目标镜像/etc/passwd中存在数字 UID--chown1000:1000最终阶段极简如 scratch无/etc/passwd容器内whoami等工具可能返回unknown应用改写日志路径到/tmp仅临时文件/日志场景需改应用代码/tmp重启后数据丢失distroless 基础镜像内置 nonroot 用户生产安全基线要求高无 shell、无包管理器线上排查依赖kubectl debug等外部手段不该用本文方案的情况Windows 容器构建--chown不生效或 Docker Engine 23.0 且未设置DOCKER_BUILDKIT1classic builder 下--chown行为不完整。Docker Desktop 4.19 起默认启用 BuildKit。三、关键原理Dockerfile reference 对COPY的描述复制到目标路径的文件默认以 UID:GID0:0归属除非使用--chown参数。对USER的描述设置运行用户用于其后的RUN、CMD和ENTRYPOINT指令——不改变已有文件归属也不改变COPY的写入身份。--chown使用用户名时解析发生在目标镜像最终FROM的/etc/passwd中。如果RUN adduser在COPY --chown之后执行BuildKit 会因找不到用户名而构建失败。使用数字 UID如--chown1000:1000则不依赖/etc/passwd。关键陷阱叠加WORKDIR /app创建的目录归属为 root:root 755。COPY --chownappuser:appuser /build/server /app/server只改变/app/server的归属不改变/app目录本身。应用若需在工作目录下创建日志或临时文件仍然 EACCES。顺序约束为RUN adduser→chown目录 →COPY --chown三者不可颠倒。四、可运行示例环境Docker Engine ≥ 23.0BuildKit 默认启用Linux amd64。main.go模拟 MySQL 迁移校验服务packagemainimport(fmtlognet/httpos)funcmain(){logFile,err:os.OpenFile(access.log,os.O_CREATE|os.O_WRONLY|os.O_APPEND,0644)iferr!nil{log.Fatalf(无法创建日志文件: %v,err)}deferlogFile.Close()http.HandleFunc(/check,func(w http.ResponseWriter,r*http.Request){logFile.WriteString(fmt.Sprintf(%s %s\n,r.URL.Path,r.RemoteAddr))fmt.Fprintln(w,{status:ok,note:utf8mb4_0900_ai_ci verified})})log.Println(listening on :8080)log.Fatal(http.ListenAndServe(:8080,nil))}go.modmodule mysql-mig-checker go 1.22有 bug 的 Dockerfile构建成功运行时崩溃FROM golang:1.22-alpine AS builder WORKDIR /build COPY go.mod main.go ./ RUN CGO_ENABLED0 go build -o /build/server . FROM alpine:3.19 RUN adduser -D -u 1000 appuser WORKDIR /app COPY --frombuilder /build/server /app/server USER appuser EXPOSE 8080 CMD [/app/server]操作步骤dockerbuild-tmig-checker.dockerrun--rm-p8080:8080 mig-checker预期输出2025/06/15 08:30:15 无法创建日志文件: open access.log: permission denied实际输出与预期一致——构建成功进程因 EACCES 退出。常见失败先 COPY 后 adduser如果把RUN adduser移到COPY --chownappuser:appuser之后BuildKit 报错目标镜像/etc/passwd中没有appuser记录用户名无法解析。修复方法调整顺序先adduser再COPY --chown。修复后的 DockerfileFROM golang:1.22-alpine AS builder WORKDIR /build COPY go.mod main.go ./ RUN CGO_ENABLED0 go build -o /build/server . FROM alpine:3.19 RUN adduser -D -u 1000 appuser \ mkdir -p /app chown appuser:appuser /app WORKDIR /app COPY --frombuilder --chownappuser:appuser /build/server /app/server USER appuser EXPOSE 8080 CMD [/app/server]改动两处顺序有讲究RUN中增加chown appuser:appuser /app在adduser之后COPY增加--chownappuser:appuser。修复后重新执行docker run服务正常监听:8080访问[http://localhost:8080/check](http://localhost:8080/check)返回 JSON。五、验证结果与边界检查项修复前修复后docker build成功成功进程启动EACCES 退出正常监听:8080/app目录归属root:rootappuser:appuser/app/server归属root:rootappuser:appuser已知边界--chown在 classic builderEngine 23.0 且未设DOCKER_BUILDKIT1下行为不完整Windows 容器构建中--chown不生效极简镜像scratch中使用数字 UID--chown时容器内id命令可能不可用镜像从 1.2GB 压到 90MB 的收益依然成立多阶段构建本身没有问题——问题出在归属交接的细节上。建议在 CI 中增加一步docker run冒烟测试覆盖构建全绿但运行时崩溃这类延迟暴露的缺陷。参考资料Dockerfile reference — COPY 指令Dockerfile reference — USER 指令BuildKit 构建器文档