主题
第二阶段: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| 特性 | COPY | ADD |
|---|---|---|
| 复制本地文件 | ✅ | ✅ |
| 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 makeEXPOSE — 声明端口
dockerfile
# 声明容器运行时监听的端口(仅文档作用,不会自动映射)
EXPOSE 80
EXPOSE 443
EXPOSE 8080/tcp
EXPOSE 53/udp
# ⚠️ EXPOSE 不等于端口映射!运行时仍需 -p 参数:
# docker run -p 8080:80 my-imageENV — 设置环境变量
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-imageARG — 构建时变量
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 NONELABEL — 元数据标签
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 中最容易混淆的两个指令,必须彻底理解。
基本区别
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
| 作用 | 容器启动时的默认命令 | 容器启动时的入口命令 |
| 可覆盖性 | 容易被 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 + 静态文件 → ~25MB2.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 | ~77MB | apt | 完整 Ubuntu | 需要丰富工具链 |
debian:bookworm-slim | ~74MB | apt | 精简 Debian | 需要 apt 但追求较小体积 |
alpine:3.19 | ~7MB | apk | 超小体积 | 追求极致小镜像 |
python:3.12 | ~1GB | apt+pip | 完整 Python | 开发调试 |
python:3.12-slim | ~150MB | apt+pip | 精简 Python | 生产推荐 |
python:3.12-alpine | ~50MB | apk+pip | Alpine Python | 小镜像(可能有兼容性问题) |
node:20 | ~1GB | apt+npm | 完整 Node | 开发调试 |
node:20-slim | ~200MB | apt+npm | 精简 Node | 生产环境 |
node:20-alpine | ~130MB | apk+npm | Alpine Node | 生产推荐 |
gcr.io/distroless/static | ~2MB | 无 | 无 shell | Go 静态二进制 |
scratch | 0MB | 无 | 空镜像 | 静态链接的二进制 |
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-slimDistroless 镜像
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:debug2.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.02.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/_catalog2.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:abc12342.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
└── .dockerignoreapp.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.0Dockerfile:
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-appDemo 2:Node.js Express 应用容器化
项目结构:
node-demo/
├── src/
│ └── index.js
├── package.json
├── Dockerfile
└── .dockerignorepackage.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-appDemo 3:Go 应用多阶段构建(极致优化)
项目结构:
go-demo/
├── cmd/
│ └── server/
│ └── main.go
├── go.mod
└── Dockerfilego.mod:
module go-demo
go 1.22cmd/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-appDemo 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
└── Dockerfileentrypoint.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.txtQ4: 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/localQ6: 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 dockerQ7: 如何在 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_TOKENQ8: 如何调试 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 多阶段构建),能直观感受镜像体积优化的效果。掌握构建缓存机制后,你的日常构建速度会有质的提升。