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 maintainer="wangdun@example.com" \ version="2.1.0" \ description="用户认证微服务" \ team="platform"3️⃣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 1,nginx 是 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_VERSION=5.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_VERSION=5.7 # 运行时可覆盖 docker run -e MYSQL_VERSION=8.0 my-imageARG 仅构建时参数,构建完成后消失。docker history 中不可见
ARG VERSION=1.0 ARG REGISTRY=registry.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 VERSION=2.0 --build-arg REGISTRY=myregistry.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 --chown=nginx: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 \ --interval=30s \ --timeout=10s \ --start-period=5s \ --retries=3 \ 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 中断(Ctrl+C) ✅能 ✅是 **SIGHUP **1 挂起 / 重载配置 ✅能 不一定 **SIGQUIT **3 退出 + 转储核心 ✅ 能 ✅是三、多阶段构建(Multi-Stage Build)
3.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_ENABLED=0 GOOS=linux go build -o /app/server . # ============ 阶段二:运行 ============ FROM alpine:3.18 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder /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 缓存挂载
# syntax=docker/dockerfile:1 FROM python:3.11-slim RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \ pip install --no-cache-dir -r requirements.txt docker build --secret id=pip_conf,src=./pip.conf . 1. 临时把 pip.conf 挂载到容器里的 /etc/pip.conf 2. pip install 执行,读取了密码,装好了依赖 3. 命令执行完毕 4. 挂载消失,/etc/pip.conf 不见了 5. 拍快照(镜像层)里没有密码七、完整企业级实战模板
7.1 Python 微服务模板
# syntax=docker/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 maintainer="platform-team@company.com" \ version="2.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 --from=builder /install /usr/local WORKDIR /app # 先复制依赖文件(利用缓存) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY --chown=app:app . . # 切换用户 USER app # 环境变量 ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ 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 --from=backend-builder /install /usr/local COPY backend/ /app/ # 复制前端静态资源到 nginx COPY --from=frontend-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 --interval=30s --timeout=5s \ 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 # ✅ 使用精确版本号 # ❌ 错误 7:Shell 格式 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_BUILDKIT=1 docker build --progress=plain . # 4. 分析镜像层大小 docker history my-image docker inspect my-image # 5. 使用 dive 工具分析每一层的内容 dive my-image # 6. 查看镜像体积 docker images my-image docker system df