Skip to content

第二阶段:Docker 镜像构建


2.1 Dockerfile 基础

2.1.1 什么是 Dockerfile?

Dockerfile 是一个纯文本文件,包含了一系列有序的指令,Docker 根据这些指令自动构建镜像

核心思想:Infrastructure as Code — 将镜像的构建过程代码化、版本化、可复现。

Dockerfile → docker build → Image → docker run → Container

┌──────────────┐      ┌─────────────┐      ┌──────────────┐
│  Dockerfile  │─────▶│   Image     │─────▶│  Container   │
│  (源代码)     │ build│  (可执行程序) │ run  │  (运行进程)   │
└──────────────┘      └─────────────┘      └──────────────┘

2.1.2 Dockerfile 常用指令详解

完整指令速览

dockerfile
# ===== 构建阶段指令 =====
FROM        # 指定基础镜像(必须是第一条指令)
RUN         # 执行命令(构建时)
COPY        # 复制文件/目录到镜像
ADD         # 复制文件(支持 URL 和自动解压)
WORKDIR     # 设置工作目录
ARG         # 定义构建时变量

# ===== 运行阶段指令 =====
CMD         # 容器启动时的默认命令(可被覆盖)
ENTRYPOINT  # 容器启动时的入口命令(不易被覆盖)
ENV         # 设置环境变量
EXPOSE      # 声明端口(文档作用,不实际映射)
VOLUME      # 声明挂载点
USER        # 指定运行用户
HEALTHCHECK # 健康检查
LABEL       # 添加元数据标签
SHELL       # 指定默认 shell
STOPSIGNAL  # 指定停止信号

FROM — 指定基础镜像

dockerfile
# 基本用法
FROM ubuntu:22.04
FROM python:3.12-slim
FROM node:20-alpine

# 使用 scratch(空镜像,用于静态编译的程序)
FROM scratch

# 使用 ARG 动态指定版本
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim

# 多阶段构建中的命名
FROM golang:1.22 AS builder
FROM alpine:3.19 AS runtime

💡 如何选基础镜像?

  • 需要完整工具链 → ubuntu:22.04 / debian:bookworm
  • 追求小体积 → alpine:3.19(约 5MB)
  • 语言运行时 → python:3.12-slim / node:20-alpine
  • 极致安全小巧 → distroless / scratch

RUN — 构建时执行命令

dockerfile
# Shell 形式(默认通过 /bin/sh -c 执行)
RUN apt-get update && apt-get install -y curl

# Exec 形式(直接执行,不经过 shell)
RUN ["apt-get", "install", "-y", "curl"]

# ===== 最佳实践:合并 RUN 减少层数 =====

# ❌ 不推荐:每个 RUN 创建一层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*

# ✅ 推荐:合并为一条 RUN,用 && 连接,末尾清理缓存
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        wget \
        ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# 为什么要合并?
# 1. 减少镜像层数 → 减小体积
# 2. 清理操作必须和安装在同一层,否则删除的文件依然占用空间

COPY vs ADD — 复制文件到镜像

dockerfile
# ===== COPY:推荐使用,行为明确 =====
COPY requirements.txt /app/
COPY . /app/
COPY --chown=1000:1000 app.py /app/      # 同时修改属主
COPY --chmod=755 entrypoint.sh /app/      # 同时修改权限(BuildKit)

# 复制多个文件
COPY package.json package-lock.json /app/

# ===== ADD:额外支持 URL 和自动解压 =====
ADD https://example.com/file.tar.gz /tmp/      # 从 URL 下载(不推荐)
ADD archive.tar.gz /app/                        # 自动解压 tar 文件
ADD rootfs.tar.xz /                             # 解压到根目录

# ===== 对比 =====
# COPY:只做纯粹的文件复制,行为可预测
# ADD:额外支持 URL 下载 + tar 自动解压,行为不够直观
# 结论:除非需要自动解压 tar,否则一律用 COPY
特性COPYADD
复制本地文件
URL 下载✅(但不推荐)
自动解压 tar
行为可预测性
推荐程度✅ 优先使用⚠️ 仅解压时用

WORKDIR — 设置工作目录

dockerfile
# 设置工作目录(后续 RUN、CMD、COPY 等指令都在此目录下执行)
WORKDIR /app

# 可以多次设置,支持相对路径
WORKDIR /app
WORKDIR src        # 等于 /app/src
WORKDIR tests      # 等于 /app/src/tests

# ❌ 不要用 RUN cd 替代 WORKDIR
RUN cd /app && make    # cd 只在当前 RUN 有效,下一条 RUN 又回到默认目录

# ✅ 正确做法
WORKDIR /app
RUN make

EXPOSE — 声明端口

dockerfile
# 声明容器运行时监听的端口(仅文档作用,不会自动映射)
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp
EXPOSE 53/udp

# ⚠️ EXPOSE 不等于端口映射!运行时仍需 -p 参数:
# docker run -p 8080:80 my-image

ENV — 设置环境变量

dockerfile
# 设置环境变量(构建时和运行时都生效)
ENV NODE_ENV=production
ENV APP_HOME=/app
ENV PATH="/app/bin:${PATH}"

# 多个环境变量
ENV DB_HOST=localhost \
    DB_PORT=5432 \
    DB_NAME=mydb

# 在后续指令中使用
WORKDIR ${APP_HOME}
RUN echo "Environment: ${NODE_ENV}"

# 运行时可覆盖
# docker run -e NODE_ENV=development my-image

ARG — 构建时变量

dockerfile
# ARG 只在构建阶段有效,运行时不存在
ARG VERSION=1.0
ARG DEBIAN_FRONTEND=noninteractive

# 在构建时传入
# docker build --build-arg VERSION=2.0 .

# ===== ARG vs ENV =====
# ARG:仅构建时有效,不会保留到镜像中
# ENV:构建时和运行时都有效,会保留到镜像中

# 常见模式:ARG 接收参数,ENV 持久化
ARG APP_VERSION=1.0
ENV APP_VERSION=${APP_VERSION}

VOLUME — 声明匿名卷

dockerfile
# 声明挂载点(Docker 自动创建匿名卷)
VOLUME /data
VOLUME ["/data", "/logs"]

# ⚠️ VOLUME 指令之后对该目录的修改不会保留在镜像层中
VOLUME /data
RUN echo "hello" > /data/test.txt   # 这个文件不会被保存!

USER — 指定运行用户

dockerfile
# 创建非 root 用户并切换(安全最佳实践)
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser

# 或使用 UID:GID
USER 1000:1000

# 之后的 RUN、CMD、ENTRYPOINT 都以该用户身份运行

HEALTHCHECK — 健康检查

dockerfile
# 定义健康检查命令
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# 参数说明:
# --interval=30s     每 30 秒检查一次
# --timeout=10s      超时时间 10 秒
# --start-period=5s  启动后等待 5 秒再开始检查
# --retries=3        连续失败 3 次标记为 unhealthy

# 禁用健康检查(继承基础镜像的健康检查时)
HEALTHCHECK NONE

LABEL — 元数据标签

dockerfile
LABEL maintainer="yourname@example.com"
LABEL version="1.0"
LABEL description="My awesome application"

# OCI 标准标签
LABEL org.opencontainers.image.title="My App"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.authors="dev@example.com"

2.1.3 CMD vs ENTRYPOINT 深入对比

这是 Dockerfile 中最容易混淆的两个指令,必须彻底理解。

基本区别

特性CMDENTRYPOINT
作用容器启动时的默认命令容器启动时的入口命令
可覆盖性容易被 docker run 参数覆盖不容易被覆盖(需 --entrypoint
多条指令只有最后一条生效只有最后一条生效
适用场景提供默认参数定义固定的执行程序

三种语法形式

dockerfile
# ===== Exec 形式(推荐)=====
# 直接执行,不经过 shell,PID 1 是你的程序
CMD ["python", "app.py"]
ENTRYPOINT ["python", "app.py"]

# ===== Shell 形式 =====
# 通过 /bin/sh -c 执行,PID 1 是 sh 而不是你的程序
# 这意味着你的程序收不到 SIGTERM 信号!
CMD python app.py
ENTRYPOINT python app.py

# ===== 参数形式(CMD 配合 ENTRYPOINT 使用)=====
ENTRYPOINT ["python"]
CMD ["app.py"]
# 等价于运行:python app.py

组合使用详解

dockerfile
# ===== 场景一:CMD 单独使用 =====
FROM python:3.12-slim
CMD ["python", "app.py"]

# docker run my-image              → 执行 python app.py
# docker run my-image bash         → 执行 bash(CMD 被覆盖)

# ===== 场景二:ENTRYPOINT 单独使用 =====
FROM python:3.12-slim
ENTRYPOINT ["python", "app.py"]

# docker run my-image              → 执行 python app.py
# docker run my-image bash         → 执行 python app.py bash(bash 变成参数!)
# docker run --entrypoint bash my-image  → 才能覆盖 ENTRYPOINT

# ===== 场景三:ENTRYPOINT + CMD 组合(最佳实践)=====
FROM python:3.12-slim
ENTRYPOINT ["python"]
CMD ["app.py"]

# docker run my-image              → 执行 python app.py
# docker run my-image test.py      → 执行 python test.py(CMD 被覆盖,ENTRYPOINT 不变)
# docker run my-image -c "print('hi')"  → 执行 python -c "print('hi')"

经典用法模式

dockerfile
# ===== 模式一:可执行容器 =====
# 把容器当做一个命令行工具
FROM alpine:3.19
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["--help"]

# docker run my-curl               → curl --help
# docker run my-curl https://example.com  → curl https://example.com
# docker run my-curl -I https://example.com  → curl -I https://example.com

# ===== 模式二:带初始化脚本的服务 =====
FROM python:3.12-slim
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

# entrypoint.sh 内容:
# #!/bin/bash
# echo "Running migrations..."
# python manage.py migrate
# echo "Starting server..."
# exec "$@"    # ← 关键!exec 使 CMD 的命令替换当前进程,成为 PID 1

⚠️ PID 1 与信号处理:容器停止时 Docker 发送 SIGTERM 给 PID 1。如果用 Shell 形式,PID 1 是 sh,你的应用收不到信号,会被 10 秒后 SIGKILL 强杀。始终使用 Exec 形式 + exec "$@" 模式

2.1.4 .dockerignore 文件

.dockerignore 类似于 .gitignore,指定构建时不需要发送到 Docker Daemon 的文件。

bash
# .dockerignore 文件内容

# 版本控制
.git
.gitignore
.svn

# IDE 和编辑器
.vscode
.idea
*.swp
*.swo

# 依赖目录(容器内重新安装)
node_modules
.venv
__pycache__
*.pyc

# 构建产物
dist
build
*.tar.gz

# Docker 相关
Dockerfile*
docker-compose*.yml
.dockerignore

# 文档和测试
README.md
docs/
tests/
*.test.js
*.spec.ts

# 环境配置(敏感信息)
.env
.env.local
*.pem
*.key

# OS 文件
.DS_Store
Thumbs.db

# 日志
*.log
logs/

为什么需要 .dockerignore

没有 .dockerignore:
┌──────────────────────┐         ┌──────────────────┐
│   项目目录 (500MB)    │────────▶│  Docker Daemon   │   发送 500MB
│  - 代码 (5MB)        │  Build  │                  │   构建上下文
│  - node_modules(200MB)│ Context │                  │
│  - .git (300MB)      │         │                  │
└──────────────────────┘         └──────────────────┘

有 .dockerignore:
┌──────────────────────┐         ┌──────────────────┐
│   项目目录 (500MB)    │────────▶│  Docker Daemon   │   只发送 5MB
│  - 代码 (5MB) ✅     │  Build  │                  │   构建上下文
│  - node_modules ❌   │ Context │                  │
│  - .git ❌           │         │                  │
└──────────────────────┘         └──────────────────┘

效果:

  • ✅ 加速构建(发送的上下文更小)
  • ✅ 避免敏感文件泄露到镜像中
  • ✅ 避免不必要的缓存失效

2.2 镜像构建最佳实践

2.2.1 多阶段构建(Multi-stage Build)

多阶段构建是优化镜像体积的最重要技术,核心思想:构建环境和运行环境分离

传统构建:                          多阶段构建:
┌─────────────────────┐           ┌────────────────┐  ┌──────────────┐
│  编译器 + 工具链     │           │  Stage 1: Build │  │ Stage 2: Run │
│  源代码             │           │  编译器 + 工具链 │  │ 仅运行时依赖  │
│  依赖库             │           │  源代码          │  │ 编译产物 ✅   │
│  编译产物           │           │  编译产物 ──────────▶│              │
│                     │           └────────────────┘  └──────────────┘
│  镜像: 800MB        │           最终镜像: 仅包含 Stage 2 → 20MB
└─────────────────────┘

Go 应用多阶段构建示例

dockerfile
# ===== Stage 1: 构建阶段 =====
FROM golang:1.22 AS builder

WORKDIR /app

# 先复制依赖文件,利用缓存
COPY go.mod go.sum ./
RUN go mod download

# 再复制源代码
COPY . .

# 编译为静态二进制文件
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server

# ===== Stage 2: 运行阶段 =====
FROM alpine:3.19

# 安装必要的证书(HTTPS 请求需要)
RUN apk --no-cache add ca-certificates tzdata

WORKDIR /app

# 从构建阶段复制编译产物
COPY --from=builder /app/server .
COPY --from=builder /app/configs ./configs

# 创建非 root 用户
RUN adduser -D -u 1000 appuser
USER appuser

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=5s \
    CMD wget -qO- http://localhost:8080/health || exit 1

CMD ["./server"]
bash
# 构建并查看大小
docker build -t my-go-app .
docker images my-go-app

# 对比:
# golang:1.22 基础镜像   → ~800MB
# 多阶段构建最终镜像     → ~20MB  (缩小 40 倍!)

Python 应用多阶段构建示例

dockerfile
# ===== Stage 1: 构建依赖 =====
FROM python:3.12 AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ===== Stage 2: 运行阶段 =====
FROM python:3.12-slim

WORKDIR /app

# 从构建阶段复制已安装的依赖
COPY --from=builder /install /usr/local

COPY . .

RUN useradd -r -u 1000 appuser
USER appuser

EXPOSE 8000

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]

前端应用多阶段构建示例

dockerfile
# ===== Stage 1: 构建前端资源 =====
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --production=false

COPY . .
RUN npm run build

# ===== Stage 2: Nginx 托管静态文件 =====
FROM nginx:1.25-alpine

# 自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 从构建阶段复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
bash
# 对比大小:
# node:20-alpine + node_modules → ~400MB
# 最终 nginx:alpine + 静态文件  → ~25MB

2.2.2 利用构建缓存加速构建

Docker 构建时逐层检查缓存,如果某一层及其之前的层都没有变化,就直接使用缓存。

缓存命中规则:
┌────────────────────────────────────────────────────────────────┐
│ Layer 1: FROM python:3.12-slim         ✅ 缓存命中            │
│ Layer 2: WORKDIR /app                  ✅ 缓存命中            │
│ Layer 3: COPY requirements.txt .       ✅ 缓存命中(文件没变) │
│ Layer 4: RUN pip install ...           ✅ 缓存命中            │
│ Layer 5: COPY . .                      ❌ 缓存失效(代码改了) │
│ Layer 6: CMD ["python", "app.py"]      ❌ 重新执行(上层失效) │
└────────────────────────────────────────────────────────────────┘

关键:一旦某层缓存失效,之后的所有层都会重新构建!

最佳实践:将变化频率低的操作放在前面

dockerfile
# ❌ 错误顺序:每次代码改动都会重新安装依赖
FROM python:3.12-slim
WORKDIR /app
COPY . .                           # 代码经常变 → 缓存失效
RUN pip install -r requirements.txt # 每次都重新安装 → 慢!
CMD ["python", "app.py"]

# ✅ 正确顺序:依赖没变就走缓存,只重新复制代码
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .            # 依赖文件很少变 → 缓存命中
RUN pip install -r requirements.txt # 走缓存 → 快!
COPY . .                           # 只有这层及之后需要重建
CMD ["python", "app.py"]
dockerfile
# Node.js 的等价写法
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./   # 先复制依赖声明
RUN npm ci                                # 走缓存
COPY . .                                  # 再复制代码
RUN npm run build
CMD ["node", "dist/index.js"]

2.2.3 最小化镜像层数

dockerfile
# ❌ 不好:5 个 RUN = 5 层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*

# ✅ 好:1 个 RUN = 1 层,并且清理缓存
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        wget \
        vim && \
    rm -rf /var/lib/apt/lists/*

# 为什么 rm 必须和 install 在同一层?
# 因为 Docker 镜像层是叠加的,即使上层删除了文件,下层的文件依然占空间
# 只有在同一层中执行删除,才能真正减小体积

其他减小体积的技巧:

dockerfile
# 1. 使用 --no-install-recommends 避免安装不必要的包
RUN apt-get install -y --no-install-recommends curl

# 2. pip 不缓存
RUN pip install --no-cache-dir -r requirements.txt

# 3. npm 清理缓存
RUN npm ci && npm cache clean --force

# 4. 删除文档和 man pages
RUN rm -rf /usr/share/doc /usr/share/man

# 5. 使用 .dockerignore 排除不需要的文件

2.2.4 选择合适的基础镜像

基础镜像大小包管理器特点适用场景
ubuntu:22.04~77MBapt完整 Ubuntu需要丰富工具链
debian:bookworm-slim~74MBapt精简 Debian需要 apt 但追求较小体积
alpine:3.19~7MBapk超小体积追求极致小镜像
python:3.12~1GBapt+pip完整 Python开发调试
python:3.12-slim~150MBapt+pip精简 Python生产推荐
python:3.12-alpine~50MBapk+pipAlpine Python小镜像(可能有兼容性问题)
node:20~1GBapt+npm完整 Node开发调试
node:20-slim~200MBapt+npm精简 Node生产环境
node:20-alpine~130MBapk+npmAlpine Node生产推荐
gcr.io/distroless/static~2MB无 shellGo 静态二进制
scratch0MB空镜像静态链接的二进制

Alpine 注意事项

dockerfile
# Alpine 使用 musl libc 而不是 glibc,可能导致兼容性问题

# 常见问题:Python 包编译失败
# 解决:安装必要的编译依赖
FROM python:3.12-alpine
RUN apk add --no-cache \
    build-base \
    libffi-dev \
    openssl-dev \
    && pip install --no-cache-dir -r requirements.txt \
    && apk del build-base   # 安装后删除编译工具

# 如果兼容性问题太多,改用 slim 版本更省心
FROM python:3.12-slim

Distroless 镜像

dockerfile
# Google 的 Distroless 镜像:没有 shell、包管理器、任何多余的东西
# 优点:极小体积,极高安全性(攻击面最小)
# 缺点:无法 exec 进入容器调试

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

# 使用 distroless 作为运行镜像
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /
CMD ["/server"]

# 如果需要调试,可以使用 debug 版本(带 busybox shell)
# FROM gcr.io/distroless/static-debian12:debug

2.2.5 镜像安全扫描

bash
# ===== Docker Scout(官方工具)=====
# 扫描本地镜像漏洞
docker scout cves my-image:latest

# 查看镜像推荐的改进
docker scout recommendations my-image:latest

# 快速概览
docker scout quickview my-image:latest

# ===== Trivy(开源工具,推荐)=====
# 安装
docker pull aquasec/trivy

# 扫描镜像
docker run --rm aquasec/trivy image my-image:latest

# 只显示高危和严重漏洞
docker run --rm aquasec/trivy image --severity HIGH,CRITICAL my-image:latest

# ===== 安全最佳实践 =====
# 1. 定期更新基础镜像
# 2. 不使用 latest 标签,锁定具体版本
# 3. 使用非 root 用户运行
# 4. 最小化安装包
# 5. 扫描镜像后再推送

2.3 镜像管理

2.3.1 推送镜像到 Docker Hub

bash
# ===== Step 1: 登录 Docker Hub =====
docker login
# 输入用户名和密码

# ===== Step 2: 给镜像打标签 =====
# 格式:docker tag <本地镜像> <用户名>/<仓库名>:<标签>
docker tag my-app:latest username/my-app:1.0.0
docker tag my-app:latest username/my-app:latest

# ===== Step 3: 推送 =====
docker push username/my-app:1.0.0
docker push username/my-app:latest

# ===== Step 4: 验证 =====
# 在另一台机器上拉取
docker pull username/my-app:1.0.0

2.3.2 推送到私有仓库

bash
# ===== 使用腾讯云 TCR =====
# 登录
docker login ccr.ccs.tencentyun.com --username=<user>

# 打标签
docker tag my-app:latest ccr.ccs.tencentyun.com/my-namespace/my-app:1.0.0

# 推送
docker push ccr.ccs.tencentyun.com/my-namespace/my-app:1.0.0

# ===== 自建 Registry(简单测试用)=====
# 启动本地 Registry
docker run -d -p 5000:5000 --name registry registry:2

# 打标签
docker tag my-app:latest localhost:5000/my-app:1.0.0

# 推送
docker push localhost:5000/my-app:1.0.0

# 拉取
docker pull localhost:5000/my-app:1.0.0

# 查看仓库中的镜像
curl http://localhost:5000/v2/_catalog

2.3.3 镜像版本管理与标签策略

bash
# ===== 推荐的标签策略 =====

# 1. 语义化版本号(Semantic Versioning)
my-app:1.0.0          # 精确版本
my-app:1.0            # 次版本
my-app:1              # 主版本

# 2. Git commit SHA(CI/CD 中常用)
my-app:abc1234        # Git 短 SHA
my-app:main-abc1234   # 分支 + SHA

# 3. 日期标签
my-app:20240301       # 构建日期

# 4. latest 标签
my-app:latest         # 最新版本(不推荐在生产中使用)

# ===== 构建时打多个标签 =====
docker build \
  -t my-app:1.2.3 \
  -t my-app:1.2 \
  -t my-app:1 \
  -t my-app:latest \
  .

# ===== 生产环境建议 =====
# ❌ 不要用 latest → 不明确,回滚困难
# ✅ 用精确版本号 → my-app:1.2.3
# ✅ CI/CD 中用 commit SHA → my-app:abc1234

2.3.4 镜像分层原理深入

bash
# 查看镜像的每一层及其大小
docker history my-app:latest

# 输出示例:
# IMAGE          CREATED          CREATED BY                                      SIZE
# abc123def456   2 minutes ago    CMD ["python" "app.py"]                         0B
# 789ghi012jkl   2 minutes ago    COPY . /app                                    15.2kB
# mno345pqr678   3 minutes ago    RUN pip install --no-cache-dir -r requirements  45.3MB
# stu901vwx234   3 minutes ago    COPY requirements.txt .                         89B
# yz5678abcdef   3 minutes ago    WORKDIR /app                                    0B
# <base-image>   2 weeks ago      ...                                             150MB

# 查看镜像分层详情(JSON)
docker inspect my-app:latest | jq '.[0].RootFS.Layers'

# 使用 Dive 工具可视化分析镜像层(强烈推荐)
# 安装 Dive
docker pull wagoodman/dive

# 分析镜像
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive my-app:latest

# Dive 功能:
# - 逐层查看文件系统变化
# - 发现浪费的空间
# - 评估镜像效率分数
镜像分层存储示意:

磁盘上的存储:                    运行时的视图:
┌─────────────────┐
│   Layer 5       │─────┐
│   COPY . /app   │     │
├─────────────────┤     │    ┌──────────────────────┐
│   Layer 4       │     ├───▶│   统一的文件系统视图   │
│   RUN pip ...   │     │    │   /app/               │
├─────────────────┤     │    │   /usr/local/lib/     │
│   Layer 3       │     │    │   /etc/               │
│   WORKDIR /app  │─────┤    │   ...                 │
├─────────────────┤     │    └──────────────────────┘
│   Layer 2       │     │         ↑
│   apt-get ...   │─────┤    UnionFS 联合挂载
├─────────────────┤     │
│   Layer 1       │     │
│   Base Image    │─────┘
└─────────────────┘

共享机制:
Image A: [Base] → [Python] → [App A]
Image B: [Base] → [Python] → [App B]

        Layer 共享!只存储一份

2.4 实践 Demo

Demo 1:Python Flask 应用容器化

项目结构:

flask-demo/
├── app.py
├── requirements.txt
├── Dockerfile
└── .dockerignore

app.py:

python
from flask import Flask, jsonify
import os
import socket

app = Flask(__name__)

@app.route("/")
def home():
    return jsonify({
        "message": "Hello from Docker! 🐳",
        "hostname": socket.gethostname(),
        "python_env": os.getenv("FLASK_ENV", "production"),
    })

@app.route("/health")
def health():
    return jsonify({"status": "healthy"}), 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

requirements.txt:

flask==3.0.0
gunicorn==21.2.0

Dockerfile:

dockerfile
# 使用 slim 版本,平衡体积和兼容性
FROM python:3.12-slim

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    FLASK_ENV=production

WORKDIR /app

# 先复制依赖文件(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再复制应用代码
COPY . .

# 创建非 root 用户
RUN useradd -r -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1

# 使用 gunicorn 生产级服务器
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:5000", "--workers", "2", "--access-logfile", "-"]

.dockerignore:

__pycache__
*.pyc
.venv
.git
.gitignore
Dockerfile
docker-compose.yml
README.md
.env

构建和运行:

bash
# 进入项目目录
mkdir -p flask-demo && cd flask-demo

# (创建上述文件后)

# 构建镜像
docker build -t flask-demo:1.0 .

# 查看镜像大小
docker images flask-demo
# REPOSITORY   TAG   IMAGE ID       CREATED          SIZE
# flask-demo   1.0   xxxxxxxxxxxx   10 seconds ago   ~170MB

# 运行容器
docker run -d --name flask-app -p 5000:5000 flask-demo:1.0

# 测试
curl http://localhost:5000
# {"hostname":"a1b2c3d4e5f6","message":"Hello from Docker! 🐳","python_env":"production"}

curl http://localhost:5000/health
# {"status":"healthy"}

# 查看日志
docker logs -f flask-app

# 清理
docker stop flask-app && docker rm flask-app

Demo 2:Node.js Express 应用容器化

项目结构:

node-demo/
├── src/
│   └── index.js
├── package.json
├── Dockerfile
└── .dockerignore

package.json:

json
{
  "name": "node-docker-demo",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

src/index.js:

javascript
const express = require('express');
const os = require('os');

const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({
    message: 'Hello from Docker! 🐳',
    hostname: os.hostname(),
    platform: os.platform(),
    uptime: process.uptime(),
    nodeVersion: process.version,
  });
});

app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});

// 优雅关闭
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  process.exit(0);
});

Dockerfile:

dockerfile
FROM node:20-alpine

# 设置生产环境
ENV NODE_ENV=production

WORKDIR /app

# 先复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev && npm cache clean --force

# 复制源代码
COPY . .

# 使用 node 内置用户(Alpine 镜像自带)
USER node

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

CMD ["node", "src/index.js"]

.dockerignore:

node_modules
npm-debug.log
.git
.gitignore
Dockerfile
docker-compose.yml
README.md
.env

构建和运行:

bash
mkdir -p node-demo && cd node-demo

# 初始化并安装依赖(生成 package-lock.json)
npm install

# 构建镜像
docker build -t node-demo:1.0 .

# 查看大小
docker images node-demo
# ~135MB(alpine 基础镜像 + express 依赖很小)

# 运行
docker run -d --name node-app -p 3000:3000 node-demo:1.0

# 测试
curl http://localhost:3000
curl http://localhost:3000/health

# 清理
docker stop node-app && docker rm node-app

Demo 3:Go 应用多阶段构建(极致优化)

项目结构:

go-demo/
├── cmd/
│   └── server/
│       └── main.go
├── go.mod
└── Dockerfile

go.mod:

module go-demo

go 1.22

cmd/server/main.go:

go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"runtime"
	"time"
)

var startTime = time.Now()

type Response struct {
	Message  string `json:"message"`
	Hostname string `json:"hostname"`
	GoVer    string `json:"go_version"`
	Uptime   string `json:"uptime"`
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		hostname, _ := os.Hostname()
		resp := Response{
			Message:  "Hello from Docker! 🐳 (Go)",
			Hostname: hostname,
			GoVer:    runtime.Version(),
			Uptime:   time.Since(startTime).String(),
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	})

	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"status":"healthy"}`))
	})

	log.Printf("Server starting on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

Dockerfile(三种优化级别对比):

dockerfile
# ╔══════════════════════════════════════════════╗
# ║  Level 1: 基础多阶段构建(~15MB)             ║
# ╚══════════════════════════════════════════════╝

# ===== 构建阶段 =====
FROM golang:1.22-alpine AS builder

WORKDIR /app

COPY go.mod ./
RUN go mod download

COPY . .

# 静态编译 + 去掉调试信息 + 去掉符号表
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o server ./cmd/server/

# ===== 运行阶段 =====
FROM alpine:3.19

RUN apk --no-cache add ca-certificates tzdata && \
    adduser -D -u 1000 appuser

WORKDIR /app
COPY --from=builder /app/server .

USER appuser
EXPOSE 8080

CMD ["./server"]

# ╔══════════════════════════════════════════════╗
# ║  Level 2: 使用 Distroless(~8MB)             ║
# ╚══════════════════════════════════════════════╝
# 将运行阶段改为:
# FROM gcr.io/distroless/static-debian12
# COPY --from=builder /app/server /
# USER 1000
# CMD ["/server"]

# ╔══════════════════════════════════════════════╗
# ║  Level 3: 使用 Scratch(~6MB,最小)           ║
# ╚══════════════════════════════════════════════╝
# 将运行阶段改为:
# FROM scratch
# COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# COPY --from=builder /app/server /
# USER 1000
# CMD ["/server"]

构建并对比大小:

bash
mkdir -p go-demo && cd go-demo

# 构建三个版本
docker build -t go-demo:alpine .
# 修改 Dockerfile 后:
# docker build -t go-demo:distroless .
# docker build -t go-demo:scratch .

# 对比大小
docker images go-demo
# REPOSITORY   TAG          SIZE
# go-demo      alpine       ~15MB
# go-demo      distroless   ~8MB
# go-demo      scratch      ~6MB
# (对比 golang:1.22 基础镜像 ~800MB,缩小 100+ 倍!)

# 运行测试
docker run -d --name go-app -p 8080:8080 go-demo:alpine
curl http://localhost:8080

# 清理
docker stop go-app && docker rm go-app

Demo 4:构建缓存优化实验

bash
# ===== 实验:验证缓存效果 =====

# 使用 Demo 1 的 Flask 项目

# 第一次构建(无缓存)
time docker build -t flask-demo:v1 --no-cache .
# 输出每一步都显示 "Running"

# 不修改任何文件,第二次构建(完全命中缓存)
time docker build -t flask-demo:v2 .
# 输出每一步都显示 "CACHED" → 秒级完成

# 只修改 app.py(不改 requirements.txt)
echo "# comment" >> app.py
time docker build -t flask-demo:v3 .
# 前面的层(包括 pip install)走缓存 ✅
# 只有 COPY . . 之后的层重新构建

# 修改 requirements.txt(增加一个包)
echo "requests==2.31.0" >> requirements.txt
time docker build -t flask-demo:v4 .
# pip install 层缓存失效 → 重新安装所有依赖

# ===== 查看构建耗时对比 =====
# 无缓存构建:~30s
# 完全缓存:~1s
# 只改代码:~3s(只重建 COPY . . 之后的层)
# 改依赖:~25s(pip install 需要重新执行)

Demo 5:使用 entrypoint.sh 初始化脚本

项目结构:

entrypoint-demo/
├── app.py
├── entrypoint.sh
├── requirements.txt
└── Dockerfile

entrypoint.sh:

bash
#!/bin/bash
set -e

echo "========================================="
echo "  Container starting at $(date)"
echo "  Environment: ${APP_ENV:-production}"
echo "  Python version: $(python --version)"
echo "========================================="

# 执行初始化操作
if [ "$APP_ENV" = "development" ]; then
    echo "Running in development mode..."
    echo "Installing dev dependencies..."
    pip install --no-cache-dir debugpy 2>/dev/null || true
fi

# 数据库迁移(如果需要)
if [ "$RUN_MIGRATIONS" = "true" ]; then
    echo "Running database migrations..."
    # python manage.py migrate
fi

# 关键:用 exec 替换当前进程,使 CMD 的命令成为 PID 1
exec "$@"

Dockerfile:

dockerfile
FROM python:3.12-slim

ENV APP_ENV=production

WORKDIR /app

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

COPY . .

RUN chmod +x entrypoint.sh

ENTRYPOINT ["./entrypoint.sh"]
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:5000"]
bash
# 生产模式运行
docker run -d --name prod-app -p 5000:5000 entrypoint-demo

# 开发模式运行
docker run -d --name dev-app -p 5001:5000 \
  -e APP_ENV=development \
  entrypoint-demo

# 覆盖 CMD 进行调试
docker run -it --rm entrypoint-demo bash
# → entrypoint.sh 先执行初始化,然后进入 bash

# 覆盖 ENTRYPOINT(跳过初始化)
docker run -it --rm --entrypoint bash entrypoint-demo
# → 直接进入 bash,不执行 entrypoint.sh

# 查看日志
docker logs prod-app
# =========================================
#   Container starting at Fri Mar 13 10:00:00 UTC 2026
#   Environment: production
#   Python version: Python 3.12.x
# =========================================

Demo 6:Dive 镜像分析实战

bash
# 拉取 Dive 工具
docker pull wagoodman/dive

# 分析 Flask Demo 的镜像
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive flask-demo:1.0

# Dive 交互界面操作:
# Tab     → 切换左右面板
# ↑/↓     → 浏览镜像层/文件
# Space   → 展开/折叠目录
# Ctrl+A  → 显示/隐藏已添加的文件
# Ctrl+R  → 显示/隐藏已移除的文件
# Ctrl+M  → 显示/隐藏已修改的文件
# Ctrl+U  → 显示/隐藏未修改的文件

# Dive 会给出效率评分:
# Image efficiency score: 98%   ← 越高越好
# Potential wasted space: 2.1MB ← 越低越好

# CI 中使用(非交互模式)
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive --ci flask-demo:1.0
# 返回非 0 退出码表示镜像效率不达标

2.5 常见问题 QA

Q1: 构建时报 COPY failed: file not found

原因:文件不在构建上下文中,或被 .dockerignore 排除了。

bash
# 排查步骤:

# 1. 确认文件确实存在
ls -la ./your-file

# 2. 检查 .dockerignore 是否排除了该文件
cat .dockerignore | grep your-file

# 3. 确认构建上下文路径(docker build 的最后一个参数)
docker build -t my-app .     # "." 是构建上下文
docker build -t my-app ./src # "./src" 是构建上下文

# 4. 注意:Dockerfile 中的路径是相对于构建上下文的,不是相对于 Dockerfile 的
# 如果 Dockerfile 在子目录中:
docker build -f docker/Dockerfile -t my-app .
# COPY 路径仍然相对于 "."(项目根目录),不是 docker/ 目录

Q2: RUN apt-get update 缓存问题 — 安装的包版本过旧

问题:单独的 RUN apt-get update 层被缓存后,后续构建不会再更新包索引。

dockerfile
# ❌ 错误:update 被缓存,install 可能找不到新版本的包
RUN apt-get update
RUN apt-get install -y some-new-package

# ✅ 正确:update 和 install 必须在同一个 RUN 中
RUN apt-get update && \
    apt-get install -y some-new-package && \
    rm -rf /var/lib/apt/lists/*

# 如果需要强制不使用缓存:
docker build --no-cache -t my-app .

Q3: pip install 很慢,如何加速?

dockerfile
# 方案一:使用国内镜像源
RUN pip install --no-cache-dir \
    -i https://mirrors.tencent.com/pypi/simple/ \
    --trusted-host mirrors.tencent.com \
    -r requirements.txt

# 方案二:设置全局镜像源
ENV PIP_INDEX_URL=https://mirrors.tencent.com/pypi/simple/ \
    PIP_TRUSTED_HOST=mirrors.tencent.com

RUN pip install --no-cache-dir -r requirements.txt

# 方案三:使用 BuildKit 的缓存挂载(最优)
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Q4: Shell 形式和 Exec 形式的 CMD/ENTRYPOINT 有什么区别?

dockerfile
# ===== Shell 形式 =====
CMD python app.py
# 实际执行:/bin/sh -c "python app.py"
# 进程树:
#   PID 1: /bin/sh -c "python app.py"
#     └── PID 2: python app.py
# 问题:python 不是 PID 1,收不到 SIGTERM 信号!

# ===== Exec 形式 =====
CMD ["python", "app.py"]
# 实际执行:python app.py(直接执行)
# 进程树:
#   PID 1: python app.py
# 正确:python 是 PID 1,能正确处理信号

# ===== 影响 =====
# docker stop 发送 SIGTERM 给 PID 1
# Shell 形式:sh 收到信号但不转发给 python → 等 10 秒后 SIGKILL 强杀
# Exec 形式:python 直接收到信号 → 可以优雅关闭

# ===== 结论 =====
# 始终使用 Exec 形式!
CMD ["python", "app.py"]           # ✅
ENTRYPOINT ["python", "app.py"]    # ✅

Q5: 如何减小 Python 镜像的体积?

dockerfile
# ===== 方法对比 =====

# python:3.12       → ~1GB(完整版,含编译工具)
# python:3.12-slim  → ~150MB(精简版,推荐!)
# python:3.12-alpine → ~50MB(可能有兼容性问题)

# ===== 最佳实践组合 =====
FROM python:3.12-slim

# 1. 使用 --no-cache-dir
RUN pip install --no-cache-dir -r requirements.txt

# 2. 多阶段构建
# 3. .dockerignore 排除不必要的文件
# 4. 删除 pyc 文件
ENV PYTHONDONTWRITEBYTECODE=1

# 5. 如果有 C 扩展依赖,用多阶段构建
FROM python:3.12 AS builder
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
COPY --from=builder /install /usr/local

Q6: docker build 时报 no space left on device

原因:Docker 存储空间不足。

bash
# 1. 查看 Docker 磁盘使用
docker system df

# 2. 清理未使用的资源
docker system prune -a          # 清理未使用的镜像
docker builder prune            # 清理构建缓存
docker volume prune             # 清理未使用的卷

# 3. 查看 Docker 数据目录大小
sudo du -sh /var/lib/docker/

# 4. 如果需要,更改 Docker 数据存储路径
# 编辑 /etc/docker/daemon.json
{
  "data-root": "/mnt/large-disk/docker"
}
sudo systemctl restart docker

Q7: 如何在 Dockerfile 中使用 SSH 密钥(如拉取私有仓库)?

dockerfile
# ❌ 绝对不要这样做!密钥会保留在镜像层中
COPY id_rsa /root/.ssh/id_rsa
RUN git clone git@github.com:private/repo.git
RUN rm /root/.ssh/id_rsa    # 删除也没用,之前的层还在!

# ✅ 方案一:使用 BuildKit 的 SSH 挂载(推荐)
# syntax=docker/dockerfile:1
FROM python:3.12-slim
RUN --mount=type=ssh git clone git@github.com:private/repo.git

# 构建时:
# DOCKER_BUILDKIT=1 docker build --ssh default -t my-app .

# ✅ 方案二:使用 BuildKit 的 secret 挂载
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=my_token \
    TOKEN=$(cat /run/secrets/my_token) && \
    git clone https://${TOKEN}@github.com/private/repo.git

# 构建时:
# docker build --secret id=my_token,src=./token.txt -t my-app .

# ✅ 方案三:多阶段构建(在构建阶段使用,不带入最终镜像)
FROM alpine AS downloader
ARG GIT_TOKEN
RUN git clone https://${GIT_TOKEN}@github.com/private/repo.git /src

FROM python:3.12-slim
COPY --from=downloader /src /app
# 最终镜像中不包含 GIT_TOKEN

Q8: 如何调试 Dockerfile 构建失败的问题?

bash
# ===== 方法一:查看构建输出 =====
# 使用 --progress=plain 查看完整输出
docker build --progress=plain -t my-app .

# ===== 方法二:在失败的层之前插入调试命令 =====
# 临时在 Dockerfile 中添加:
RUN ls -la /app/          # 查看文件列表
RUN cat /app/config.yml   # 查看文件内容
RUN which python          # 查看命令路径
RUN env                   # 查看环境变量

# ===== 方法三:从中间层启动容器 =====
# BuildKit 模式下构建并指定 target
docker build --target builder -t debug-image .
docker run -it --rm debug-image sh

# ===== 方法四:使用 docker build 的 --no-cache 跳过缓存 =====
docker build --no-cache -t my-app .

# ===== 方法五:交互式调试 =====
# 基于基础镜像启动容器,手动执行 Dockerfile 中的命令
docker run -it --rm python:3.12-slim bash
# 在容器内逐条执行 RUN 指令,找出问题

Q9: 多阶段构建中如何从指定阶段复制文件?

dockerfile
# 使用 AS 命名阶段,然后用 --from= 引用
FROM golang:1.22 AS builder
RUN go build -o /app/server .

FROM node:20-alpine AS frontend
RUN npm run build

# 最终阶段可以从多个阶段复制
FROM alpine:3.19
COPY --from=builder /app/server /app/server
COPY --from=frontend /app/dist /app/static

# 也可以从外部镜像复制(不需要在 Dockerfile 中定义)
COPY --from=nginx:1.25-alpine /etc/nginx/nginx.conf /etc/nginx/

Q10: latest 标签的坑 — 为什么不推荐在生产中使用?

bash
# ===== 问题场景 =====
# 1. latest 不代表"最新",它只是一个标签名
# 2. 推送新版本不会自动更新已拉取的 latest

# 第一天:
docker build -t my-app:latest .    # 版本 A
docker push my-app:latest

# 第二天:
docker build -t my-app:latest .    # 版本 B
docker push my-app:latest

# 问题:其他机器上已经拉取过的 latest 还是版本 A!
# 除非手动 docker pull my-app:latest

# ===== 生产建议 =====
# 1. 使用精确版本号
docker build -t my-app:1.2.3 .
docker push my-app:1.2.3

# 2. CI/CD 中使用 Git SHA
docker build -t my-app:$(git rev-parse --short HEAD) .

# 3. 同时打多个标签
docker build \
  -t my-app:1.2.3 \
  -t my-app:1.2 \
  -t my-app:latest \
  .

2.6 Dockerfile 模板合集

Python 生产模板

dockerfile
FROM python:3.12-slim AS base

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

FROM base AS builder
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM base
COPY --from=builder /install /usr/local
COPY . .
RUN useradd -r -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000", "-w", "4"]

Node.js 生产模板

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production

FROM node:20-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
USER node

EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s \
    CMD wget -qO- http://localhost:3000/health || exit 1

CMD ["node", "dist/index.js"]

Go 生产模板

dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/server/

FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata && adduser -D -u 1000 appuser
WORKDIR /app
COPY --from=builder /app/server .
USER appuser

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s \
    CMD wget -qO- http://localhost:8080/health || exit 1

CMD ["./server"]

前端 SPA 生产模板

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s \
    CMD wget -qO- http://localhost:80/ || exit 1

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

2.7 命令速查表

┌──────────────────────────────────────────────────────────────────┐
│                    镜像构建命令速查表                              │
├──────────────────┬───────────────────────────────────────────────┤
│                  │                                               │
│  构建镜像         │  docker build -t name:tag .                   │
│                  │  docker build -f Dockerfile.prod .             │
│                  │  docker build --no-cache .                     │
│                  │  docker build --target stage-name .            │
│                  │  docker build --build-arg KEY=VAL .            │
│                  │  docker build --progress=plain .               │
│                  │                                               │
├──────────────────┼───────────────────────────────────────────────┤
│                  │                                               │
│  镜像分析         │  docker history <image>     查看构建历史       │
│                  │  docker inspect <image>     完整信息           │
│                  │  docker system df           磁盘使用           │
│                  │  dive <image>               可视化分层分析      │
│                  │                                               │
├──────────────────┼───────────────────────────────────────────────┤
│                  │                                               │
│  镜像推送         │  docker login                                 │
│                  │  docker tag src dst         打标签             │
│                  │  docker push <image>        推送镜像           │
│                  │                                               │
├──────────────────┼───────────────────────────────────────────────┤
│                  │                                               │
│  安全扫描         │  docker scout cves <image>                    │
│                  │  trivy image <image>                          │
│                  │                                               │
├──────────────────┼───────────────────────────────────────────────┤
│                  │                                               │
│  清理             │  docker image prune         清理悬空镜像       │
│                  │  docker image prune -a      清理未使用镜像      │
│                  │  docker builder prune       清理构建缓存       │
│                  │                                               │
└──────────────────┴───────────────────────────────────────────────┘

📝 学习建议:本阶段的核心是 Dockerfile 的编写和优化。建议动手完成所有 Demo,特别是 Demo 3(Go 多阶段构建),能直观感受镜像体积优化的效果。掌握构建缓存机制后,你的日常构建速度会有质的提升。