Docker 学习笔记(二):Dockerfile 与镜像最佳实践

写在前面

本文讲 Dockerfile——如何把应用打包成镜像。重点讲多阶段构建镜像瘦身,这是写出生产级镜像的关键。一个臃肿的镜像(2GB)和一个精简的镜像(50MB),在拉取、部署、安全上差距巨大。


一、Dockerfile 指令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 基础镜像
FROM nginx:1.25

# 维护者(已弃用,用 LABEL)
LABEL maintainer="jiwei"

# 运行命令(构建时执行)
RUN apt-get update && apt-get install -y curl

# 设置工作目录
WORKDIR /app

# 复制文件(推荐用 COPY 而非 ADD)
COPY . .
COPY package.json ./

# ADD:能解压 tar、能下 URL(但少用)
ADD https://example.com/app.tar.gz /tmp/

# 环境变量
ENV NODE_ENV=production
ENV PATH="/app/node_modules/.bin:$PATH"

# 构建参数(构建时可变)
ARG VERSION=1.0

# 暴露端口(文档作用,实际映射要 -p)
EXPOSE 8080

# 启动命令
CMD ["node", "server.js"]

# 入口点(和 CMD 配合)
ENTRYPOINT ["docker-entrypoint.sh"]

CMD vs ENTRYPOINT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CMD:容器默认启动命令,可被 docker run 后的参数覆盖
  CMD ["nginx", "-g", "daemon off;"]
  docker run myimage command  → 用 command 替换 CMD

ENTRYPOINT:固定入口,docker run 的参数追加在后面
  ENTRYPOINT ["nginx"]
  docker run myimage -v  → 实际执行 nginx -v

最佳实践:ENTRYPOINT(固定)+ CMD(默认参数)
  ENTRYPOINT ["node"]
  CMD ["server.js"]

二、一个典型 Web 应用镜像

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# .NET 应用示例
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY MyApp.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
1
2
3
4
5
6
7
8
# Node.js 应用示例
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

三、多阶段构建(重要)

多阶段构建是镜像瘦身的核心——用大镜像构建,把产物拷到小镜像

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 阶段1:构建(大镜像,含 SDK/编译器)
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .       # 编译出二进制

# 阶段2:运行(极小镜像)
FROM alpine:3.19
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]

# 效果:
#   builder 阶段镜像 ~800MB(含 Go 工具链)
#   最终镜像 ~15MB(只有 alpine + 二进制)
#   SDK/源码/中间产物都不进最终镜像
1
2
3
4
5
6
7
为什么重要:
  不用多阶段:镜像含 SDK(GB 级)、源码、构建工具 → 大、不安全
  多阶段:最终镜像只含运行时 + 产物 → 小、安全、快

  Go/Rust:最终镜像可到几 MB(静态二进制 + scratch/alpine)
  .NET/Java:用 runtime 镜像(不含 SDK)
  Node/Python:用 alpine 变体

四、镜像瘦身最佳实践

4.1 选小基础镜像

1
2
3
4
5
6
7
基础镜像大小对比(Node 为例):
  node:20          ~1GB    (完整 Debian)
  node:20-slim     ~250MB  (精简 Debian)
  node:20-alpine   ~150MB  (Alpine,最小)

  生产优先 alpine 或 slim
  注意:alpine 用 musl libc,少数库不兼容(要测试)

4.2 合并 RUN、清理缓存

1
2
3
4
5
6
7
8
9
# ❌ 多个 RUN,每层都大,缓存残留
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git

# ✅ 合并 RUN,同一层内清理
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl git \
    && rm -rf /var/lib/apt/lists/*

4.3 利用构建缓存

1
2
3
4
5
6
7
8
# ❌ 先 COPY 全部,依赖没变也重建
COPY . .
RUN npm ci

# ✅ 先 COPY 依赖描述,依赖没变就用缓存
COPY package*.json ./
RUN npm ci           # 缓存命中(除非 package.json 变)
COPY . .             # 代码变只重新 COPY
1
2
3
4
Dockerfile 指令顺序影响构建速度:
  把"变化少的"放前面(依赖、系统包)
  把"变化多的"放后面(源代码)
  → 充分利用缓存,加快构建

4.4 .dockerignore

1
2
3
4
5
6
7
8
9
# .dockerignore(类似 .gitignore)
node_modules
.git
*.log
.env
dist
build

  避免 COPY . . 把无用文件打进镜像(减小体积、加速构建、防泄密)

4.5 用 distroless / scratch

1
2
3
4
5
6
7
distroless:Google 出品,只有运行时,没有 shell/包管理器
  → 极小、极安全(攻击面小)
  gcr.io/distroless/nodejs20-debian12

scratch:空镜像,只放静态二进制
  → 最小(几 MB)
  适合 Go/Rust 静态编译的程序

五、安全实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# ❌ 默认用 root 运行(危险)
FROM node:20
CMD ["node", "server.js"]

# ✅ 创建非 root 用户
FROM node:20
RUN groupadd -r app && useradd -r -g app appuser
USER appuser                    # 切换用户
CMD ["node", "server.js"]

# 或用基础镜像自带的非 root 用户
FROM node:20
USER node                       # node 镜像自带 node 用户
1
2
3
4
5
6
7
其他安全要点:
  ✓ 用非 root 用户(USER 指令)
  ✓ 最小权限(只装需要的)
  ✓ 用特定版本 tag(node:20.11),别用 latest(不可控)
  ✓ 扫描漏洞(docker scout / trivy)
  ✓ 不把密钥/密码写进镜像(用环境变量/secret)
  ✓ 多阶段构建(不暴露源码和构建工具)

六、构建与推送

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 构建(注意最后的 . 表示上下文目录)
docker build -t myapp:1.0 .
docker build -t myapp:1.0 -f Dockerfile.prod .   # 指定 Dockerfile

# 多架构构建(ARM/x86)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0 .

# 打标签
docker tag myapp:1.0 registry.example.com/myapp:1.0

# 登录仓库
docker login registry.example.com

# 推送
docker push registry.example.com/myapp:1.0

七、常见问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
镜像太大?
  → 多阶段构建 + alpine/slim + .dockerignore + 合并 RUN

构建慢?
  → 调整指令顺序利用缓存(依赖在前,代码在后)

CMD 不生效?
  → docker run 后的参数会覆盖 CMD;用 ENTRYPOINT 固定

apt-get 卡住?
  → 配国内镜像源,或用 alpine(apk add)

时区不对?
  → RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  或设 TZ 环境变量

八、小结

  • Dockerfile 指令:FROM/RUN/COPY/WORKDIR/ENV/CMD/ENTRYPOINT/EXPOSE
  • CMD vs ENTRYPOINT:CMD 可覆盖,ENTRYPOINT 固定;推荐 ENTRYPOINT + CMD
  • 多阶段构建:大镜像构建 → 产物拷到小镜像,瘦身核心
  • 瘦身:alpine/slim 基础镜像、合并 RUN 清理、利用缓存、.dockerignore、distroless/scratch
  • 安全:非 root 用户、固定版本 tag、漏洞扫描、密钥不入镜像
  • 缓存优化:变化少的指令在前,变化多的在后

下一篇讲 Docker Compose——本地多容器编排。