【06续Dockerfile 构建镜像的配方单】

📅 发布时间:2026/8/4 6:54:37
【06续Dockerfile 构建镜像的配方单】 Docker 的引擎读取它逐行去执行指令每一行生成一个新的镜像层Layer一、核心概念镜像层┌─────────────────────────────┐ │ Layer 7: CMD [nginx] │ ← 最终层 ├─────────────────────────────┤ │ Layer 6: COPY html /usr/share/nginx/html │ ├─────────────────────────────┤ │ Layer 5: EXPOSE 80 │ ├─────────────────────────────┤ │ Layer 4: RUN yum install -y nginx │ ├─────────────────────────────┤ │ Layer 3: RUN yum update -y │ ├─────────────────────────────┤ │ Layer 2: LABEL maintainer │ ├─────────────────────────────┤ │ Layer 1: FROM centos:7 │ ← 基础层 └─────────────────────────────┘每一层都是只读的层与层之间是增量叠加的关系。所以理解层的机制是调优的关键。二、逐条指令深度剖析1️⃣FROM一切的起点FROM centos:7 FROM ubuntu:22.04 FROM python:3.11-slim FROM nginx:1.25-alpine规则每个Dockerfile 必须以FROM作为开头它制定了基础镜像后续操作都基于它可以使用多阶段构建出现多次FROM企业选择策略# ❌ 不推荐完整版镜像体积大几百MB到几GB FROM ubuntu:22.04 # ✅ 推荐slim 版本去除编译工具等 FROM python:3.11-slim # ✅ 更推荐alpine 版本最小化5MB 基础层 FROM python:3.11-alpine # ✅ 最推荐distroless —— 无 shell、无包管理器极致安全 FROM gcr.io/distroless/python32️⃣LABEL镜像的身份证# 多标签写法更高效只产生一层 LABEL maintainerwangdunexample.com \ version2.1.0 \ description用户认证微服务 \ teamplatform3️⃣RUN 构建时的干活担当#RUN是构建镜像时执行命令的指令是最常用的指令之一 默认为shell格式/bin/sh -c RUN yum install -y vim RUN echo 构建完成 EXEC 格式 RUN [/bin/bash,-c,echo hello]关键理解每条RUN 都会产生一个层当有多个RUN 对于每个层来说每个层的数据都会固化。# ❌ 写法产生3个层每个层都保存中间结果 RUN yum update -y # Layer 2: ~200MB包含yum缓存 RUN yum install -y nginx # Layer 3: ~50MB包含yum缓存 RUN yum clean all # Layer 4: 只标记删除Layer 2,3的文件仍在 # ✅ 写法合并为1个层安装后立即清理缓存 RUN yum update -y \ yum install -y nginx \ yum clean all \ rm -rf /var/cache/yum Layer 2: [文件A][文件B][yum缓存] ← 已固化无法修改 Layer 3: [文件C][nginx][yum缓存] ← 已固化无法修改 Layer 4: [标记删除yum缓存] ← 只是遮盖实际数据仍在 最终镜像体积 Layer2 Layer3 Layer4 仍然很大4️⃣CMD与ENTRYPOINT 容器的启动命令容易混淆的指令CMD -默认命令可被覆盖#exec 格式 CMD [nginx,-g,daemon off;] #shell 格式 CMD nginx -g daemon off; docker run my-nginx #执行CMD:nginx -g daemon off; docker run my-nginx /bin/bash #CMD 被顶替掉执行 /bin/bashExec 格式和Shell 格式的区别优雅关闭 vs 强制杀死Exec 格式 Docker → nginx nginx 就是 PID 1 Shell 格式Docker → sh → nginx sh 是 PID 1nginx 是 sh 的子进程 这样我们的容器可以监听PID为1的进程 Exec docker stop → 收到 SIGTERM → nginx 直接收优雅关闭 ✓ Shell docker stop → 收到 SIGTERM → sh 收到sh 不转发 → nginx 不知道要停 ✗ENTERPOINT 入口命令不轻易覆盖ENTERPOTIT [curl,-s] # 使用镜像时 docker run my-curl https://example.com # 执行: curl -s https://example.com docker run my-curl -v https://example.com # 执行: curl -s -v https://example.com ┌──────────────────────────────────────────────────────┐ │ docker run image args │ ├──────────────────────────────────────────────────────┤ │ │ │ ENTRYPOINT 存在 ──是── ENTRYPOINT args │ │ │ │ │ │ 否 args 为空 │ │ │ │ │ │ │ ▼ 是 否 │ │ CMD 存在 ──是── 执行 CMD │ │ │ │ │ ▼ ▼ │ │ 否 ENTRYPOINT ENTRYPOINT │ │ │ (无参数) args │ │ ▼ │ │ 报错 │ └──────────────────────────────────────────────────────┘ # 不给参数 docker run my-curl # curl -s https://example.com ← 用 CMD 的默认值 # 给了参数 docker run my-curl https://google.com # curl -s https://google.com ← CMD 被你的参数替换 # 给了选项 docker run my-curl -v https://google.com # curl -s -v https://google.com ← 拼在 ENTRYPOINT 后面ENTRYPOINT 把容器当作一个固定工具CMD 提供默认参数。5️⃣EXPOSE 声明端口EXPOSE 80 EXPOSE 443 EXPOSE 8080/tcp EXPOSE 9090/udp EXPOSE只是文档性质的声明并不会实际开放端口 只是告诉你“我这个镜像需要这些端口” 那么实质的映射还是需要 -p80:80 # 使用 -P大写会自动映射所有 EXPOSE 声明的端口,但是宿主机端口随机 docker run -P my-nginx docker port container_id # 查看自动映射的端口6️⃣WORKDIR 切换工作目录WORKDIR /opt WORKDIR app WORKDIR src # 最终工作目录: /opt/app/src 默认路径在 / 不存在就创建 # ❌ 错误写法RUN cd 不影响下一层只在当前层生效应为每条指令在新容器中执行。 RUN cd /opt/app RUN python main.py # 在根目录 / 执行会报错 # ✅ 正确写法 WORKDIR /opt/app RUN python main.py # 在 /opt/app 执行RUN cd就像在梦里搬了家醒过来还在原地。因为每条指令的容器用完即弃。WORKDIR改变的是镜像的配置永久保存。企业规范-多环境工作目录WORKDIR /app COPY requirments.txt . # /app/requirements.txt RUN pip install -r requirements.txt COPY . . # 把整个宿主机的文件拷贝到/app下 CMD [python, main.py]7️⃣ENV 与 ARG 环境变量和构建参数ENV 运行时环境变量ENV MYSQL_VERSION5.7 ENV APP_HOME/opt/app ENV PATH${APP_HOME}/bin:${PATH} # 构建时和运行时都生效也可在docker history 中看到 RUN echo $MYSQL_VERSION # 构建时可用 # 运行时也存在 docker run my-image env | grep MYSQL_VERSION # 输出: MYSQL_VERSION5.7 # 运行时可覆盖 docker run -e MYSQL_VERSION8.0 my-imageARG 仅构建时参数构建完成后消失。docker history 中不可见ARG VERSION1.0 ARG REGISTRYregistry.cn-hangzhou.aliyuncs.com FROM ${REGISTRY}/ubuntu:22.04 # 注意ARG 在 FROM 之后需要重新声明才能使用 # 因为 FROM 代表的是新的构建阶段开始了之前的 ARG 被清零了。 ARG VERSION LABEL version${VERSION} RUN echo Building version ${VERSION} docker build --build-arg VERSION2.0 --build-arg REGISTRYmyregistry.com . ARG 有默认值时读默认值docker build 命令传了 --build-arg 就读传入的值。FROM 之后重新声明就是去找回这个值。不同环境变量的差异-e就是设置了一个普通的 Linux 环境变量容器里的程序用标准的读环境变量方式就能拿到和你在终端export一个变量没有区别。8️⃣COPY 与 ADD 文件复制**#COPY 简单直接 前面是宿主机文件后面是存储层** COPY app.py /opt/ COPY config/ /etc/app/ COPY requirements.txt . COPY --chownnginx:nginx html/ /usr/share/nginx/html/ #更改文件的所属者和所属组 **#ADD 功能更多容器出问题。企业优先选择COPY,解压和下载有风险。** # 自动解压 tar 包 ADD file.tar.gz /opt/ # 可从 URL 下载但不推荐 ADD https://example.com/file.tar.gz /opt/ **#正确的使用方式** # ❌ 用 ADD隐式解压不够明确 ADD app-v2.1.0.tar.gz /opt/ # ✅ 用 COPY RUN显式操作更可读、可控 COPY app-v2.1.0.tar.gz /tmp/ RUN tar -xzf /tmp/app-v2.1.0.tar.gz -C /opt/ rm -f /tmp/app-v2.1.0.tar.gz9️⃣VOLUME 数据卷声明VOLUME[/data,/var/log/nginx] # 这段命令表示的含义是在该声明后该目录的数据不在写入镜像层而是写入宿主机的匿名卷中 docker run -v /host/data:/data my-app # 绑定挂载docker run -v mydata:/data my-app # 命名卷 企业实践 # 数据库存储目录 VOLUME /var/lib/mysql # 应用日志 VOLUME /var/log/app 注意 企业中更推荐在 docker-compose.yml 或 K8s Pod spec 中声明卷挂载而不是在 Dockerfile 中硬编码。卷挂载属于运行时配置不该写死在镜像里。USER 切换用户# 创建应用用户 RUN groupadd -r appuser useradd -r -g appuser -d /app -s /sbin/nologin appuser # 设置目录权限 RUN chown -R appuser:appuser /app # 切换用户之后的所有 RUN、CMD、ENTRYPOINT 都以该用户执行 USER appuser **# 为什么不用 root** #1.容器逃逸漏洞直接获取到宿主机的ROOT权限风险极高。 #2.可修改容器内任意文件 #3.不符合最小权限原则。1️⃣1️⃣HEALTHCHECK 健康检查# 基础用法 HEALTHCHECK CMD curl -f http://localhost/ || exit 1 # 完整参数 HEALTHCHECK \ --interval30s \ --timeout10s \ --start-period5s \ --retries3 \ CMD curl -f http://localhost:8080/health || exit 1 # curl -f 成功就返回 0任何失败都返回非零。|| exit 1 的作用是把# 所有非零的失败统一收口成 1告诉 Docker我挂了。 时间线 ├─ start-period (容器启动宽限期) ─┤← 开始检查 ├── interval ──┤← 检查两次检查之间的间隔 · timeout 单次检查的超时时间超时算失败。 · ├── interval ──┤← 检查 ...如果连续 retries 次失败 → 状态变为 unhealthy1️⃣2️⃣ONBUILD 触发器# 基础镜像 Dockerfile供他人继承 # base.Dockerfile — 平台团队写的 FROM python:3.11-slim WORKDIR /app ONBUILD COPY requirements.txt . #不会执行只记录。 ONBUILD RUN pip install -r requirements.txt ONBUILD COPY . .# 业务团队 A 的 Dockerfile FROM my-python-base:1.0 COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [python, app.py] # 业务应用 Dockerfile继承基础镜像 FROM my-python-base:1.0 # ONBUILD 指令会自动触发无需手动写 COPY 和 RUN CMD [python, app.py]ONBUILD 是平台团队写在基础镜像里的自动执行清单业务团队继承这个镜像时自动触发不用重复写也不用担心漏掉步骤。应用场景 企业内部构建统一的基础开发镜像各业务团队只需继承即可。1️⃣3️⃣ SHELL 与 STOPSIGNAL# 更改默认 shell默认是 /bin/sh -c SHELL [/bin/bash, -c] # 之后的 RUN 都用 bash 执行 RUN source /etc/profile echo $CUSTOM_VAR# 自定义停止信号 STOPSIGNAL SIGQUIT # 默认是 SIGTERM 信号 编号 作用 进程能捕获吗 优雅停止 **SIGTERM **15 终止进程 ✅能 ✅是 **SIGKILL **9 强制杀死 ❌不能直接干掉 ❌否 **SIGINT **2 中断CtrlC ✅能 ✅是 **SIGHUP **1 挂起 / 重载配置 ✅能 不一定 **SIGQUIT **3 退出 转储核心 ✅ 能 ✅是三、多阶段构建Multi-Stage Build3.1 问题构建工具 VS 运行时我们编译一个GO程序需要 -go编译器 -源码 -依赖包 运行这个程序需要 -编译好的二进制文件 -一个基础的 OS3.2 多阶段构建解法# 阶段一编译 FROM golang:1.21 AS builder WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 GOOSlinux go build -o /app/server . # 阶段二运行 FROM alpine:3.18 RUN apk add --no-cache ca-certificates tzdata COPY --frombuilder /app/server /usr/local/bin/server #拷贝从builder阶段的产物 EXPOSE 8080 CMD [server]四、.dockerignore常被忽视的关键文件# .dockerignore放在项目根目录与 Dockerfile 同级 # Git 相关 .git .gitignore # 依赖目录应该在容器内重新安装 node_modules vendor __pycache__ # Docker 相关 Dockerfile* docker-compose*.yml .dockerignore # IDE 和编辑器 .vscode .idea *.swp # 文档和测试 README.md docs/ tests/ *.md # 系统文件 .DS_Store Thumbs.db # 环境配置不应打包进镜像 .env .env.*为什么重要docker build -t myapp . #会将所有文件打包进镜像构建上下文巨大构建速度缓慢 #可能泄露敏感信息 .env 文件五、缓存优化—构建速度核心5.1 理解环境机制构建第 1 次 Layer 1: FROM ubuntu → 执行下载镜像 Layer 2: RUN apt update → 执行 Layer 3: COPY . . → 执行 Layer 4: RUN make → 执行 构建第 2 次只改了 app.py Layer 1: FROM ubuntu → 缓存命中 ✅跳过 Layer 2: RUN apt update → 缓存命中 ✅跳过 Layer 3: COPY . . → 缓存失效 ❌app.py 变了文件校验值变了 Layer 4: RUN make → 缓存失效 ❌上层变了后续全部重建 ** 核心原则把变化频率低的放到最前面变化频率高的放到最后面。**5.2企业优化实列# ❌ 差的做法每次代码变化都要重新安装依赖 FROM python:3.11-slim WORKDIR /app COPY . . # 代码变了 → 这层缓存失效 RUN pip install -r requirements.txt #上层失效这层也得重装 # ✅ 好的做法依赖安装和代码复制分离 FROM python:3.11-slim WORKDIR /app COPY requirements.txt . # 只有依赖文件变了才重装 RUN pip install --no-cache-dir -r requirements.txt COPY . . # 代码变了 → 只执行这层依赖层缓存命中5.3缓存优化速查表六、安全性调优6.1 企业安全 Dockerfile 检查清单# ✅ 1. 使用特定版本标签不用 latest FROM nginx:1.25.3-alpine3.18 # ✅ 精确版本 # FROM nginx:latest # ❌ 不确定性 # ✅ 2. 使用可信的基础镜像源 FROM registry.cn-hangzhou.aliyuncs.com/library/nginx:1.25-alpine # ✅ 3. 非 root 用户运行 RUN addgroup -S app adduser -S app -G app USER app # ✅ 4. 不安装不必要的包最小化攻击面 RUN apk add --no-cache --virtual .build-deps build-base \ apk add --no-cache runtime-dep1 runtime-dep2 \ apk del .build-deps # ✅ 5. 密钥和敏感信息不写入镜像ro只读模式容器里改不了这个文件 # ❌ COPY id_rsa /root/.ssh/ # ✅ 运行时挂载docker run -v ~/.ssh:/root/.ssh:ro # ✅ 6. 使用只读文件系统运行时只要挂载的位置可写其他位置只读 ✓ # docker run --read-only --tmpfs /tmp my-app # ✅ 7. 扫描镜像漏洞 # docker scout cves my-image # trivy image my-image6.2 使用BuildKit 缓存挂载# syntaxdocker/dockerfile:1 FROM python:3.11-slim RUN --mounttypesecret,idpip_conf,target/etc/pip.conf \ pip install --no-cache-dir -r requirements.txt docker build --secret idpip_conf,src./pip.conf . 1. 临时把 pip.conf 挂载到容器里的 /etc/pip.conf 2. pip install 执行读取了密码装好了依赖 3. 命令执行完毕 4. 挂载消失/etc/pip.conf 不见了 5. 拍快照镜像层里没有密码七、完整企业级实战模板7.1 Python 微服务模板# syntaxdocker/dockerfile:1 # 构建阶段 FROM python:3.11-slim AS builder WORKDIR /build # 系统依赖编译用 RUN apt-get update \ apt-get install -y --no-install-recommends gcc libpq-dev \ rm -rf /var/lib/apt/lists/* # Python 依赖 COPY requirements.txt . RUN pip install --no-cache-dir --prefix/install -r requirements.txt # 运行阶段 FROM python:3.11-slim # 安全元数据 LABEL maintainerplatform-teamcompany.com \ version2.1.0 \ description用户认证微服务 # 运行时系统依赖 RUN apt-get update \ apt-get install -y --no-install-recommends libpq5 curl \ rm -rf /var/lib/apt/lists/* # 创建非 root 用户 RUN groupadd -r app useradd -r -g app -d /app -s /sbin/nologin app # 复制 Python 包 COPY --frombuilder /install /usr/local WORKDIR /app # 先复制依赖文件利用缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY --chownapp:app . . # 切换用户 USER app # 环境变量 ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 EXPOSE 8080 HEALTHCHECK --interval30s --timeout5s --start-period10s --retries3 \ CMD curl -sf http://localhost:8080/health || exit 1 CMD [gunicorn, \ --bind, 0.0.0.0:8080, \ --workers, 4, \ --threads, 2, \ --timeout, 120, \ app:create_app()]7.2 前后端分离部署模板# 前端构建 FROM node:18-alpine AS frontend-builder WORKDIR /build COPY frontend/package.json frontend/package-lock.json ./ RUN npm ci COPY frontend/ . RUN npm run build # 后端构建 FROM python:3.11-slim AS backend-builder WORKDIR /build COPY backend/requirements.txt . RUN pip install --no-cache-dir --prefix/install -r requirements.txt # 最终镜像 FROM python:3.11-slim RUN apt-get update \ apt-get install -y --no-install-recommends nginx supervisor curl \ rm -rf /var/lib/apt/lists/* # 复制后端 COPY --frombackend-builder /install /usr/local COPY backend/ /app/ # 复制前端静态资源到 nginx COPY --fromfrontend-builder /build/dist /var/www/html # Nginx Supervisor 配置 COPY nginx.conf /etc/nginx/conf.d/default.conf COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf RUN useradd -r app chown -R app:app /app /var/www/html USER app EXPOSE 80 HEALTHCHECK --interval30s --timeout5s \ CMD curl -sf http://localhost/ || exit 1 CMD [/usr/bin/supervisord]八、常见错误与调试8.1 十大常见错误# ❌ 错误 1忘记 .dockerignore发送了巨大的构建上下文 # ✅ 创建 .dockerignore 文件 # ❌ 错误 2每条 RUN 单独写产生过多层 RUN apt-get update RUN apt-get install -y curl RUN apt-get clean # ✅ 合并为一条 RUN # ❌ 错误 3用了 COPY . . 导致依赖层缓存失效 COPY . . RUN pip install -r requirements.txt # ✅ 先 COPY requirements.txt安装依赖再 COPY . . # ❌ 错误 4使用 root 运行应用 # ✅ 创建并切换到非 root 用户 # ❌ 错误 5密钥写入镜像 COPY .env /app/.env # ✅ 运行时注入环境变量 # ❌ 错误 6使用 latest 标签 FROM nginx:latest # ✅ 使用精确版本号 # ❌ 错误 7Shell 格式 CMD 导致信号处理问题 CMD nginx -g daemon off; # ✅ 使用 Exec 格式 CMD [nginx, -g, daemon off;] # ❌ 错误 8在 RUN 中用 cd 改目录 RUN cd /app python main.py # ✅ 使用 WORKDIR # ❌ 错误 9编译工具留在最终镜像 FROM ubuntu RUN apt-get install -y gcc make gcc main.c -o app # ✅ 使用多阶段构建编译工具不进最终镜像 # ❌ 错误 10没有 HEALTHCHECK # ✅ 始终添加健康检查8.2 调试技巧# 1. 查看构建过程中的每一层 docker build --no-cache -t debug . # 2. 在失败的层处启动容器调试 docker build -t debug . # 假设第 3 步失败了 docker run -it 第2步成功的镜像ID /bin/bash # 然后手动执行第 3 步的命令观察错误 # 3. 使用 BuildKit 查看详细构建信息 DOCKER_BUILDKIT1 docker build --progressplain . # 4. 分析镜像层大小 docker history my-image docker inspect my-image # 5. 使用 dive 工具分析每一层的内容 dive my-image # 6. 查看镜像体积 docker images my-image docker system df九、速查清单