深色模式
Dockerfile 编写与多阶段构建
摘要:本文教你写出可维护的 Dockerfile:基础指令、构建上下文与 .dockerignore、缓存利用技巧,以及用多阶段构建显著缩小镜像体积。附一份可直接使用的 Go 与 Node 示例。
适用环境
- 已安装 Docker(含 buildx 插件)
- 示例项目:任意 Go / Node.js 小项目
- 构建机可访问基础镜像仓库
操作步骤
一、核心指令速查
| 指令 | 作用 | 要点 |
|---|---|---|
FROM | 指定基础镜像 | 尽量用 -alpine / -slim 变体 |
WORKDIR | 设置工作目录 | 不要用 cd |
COPY | 复制文件 | 优先于 ADD(ADD 会隐式解包/下载) |
RUN | 构建期执行命令 | 多条命令用 && 合并减少层 |
ENV | 设置环境变量 | 运行时仍生效 |
EXPOSE | 声明端口 | 仅文档作用,不实际映射 |
USER | 指定运行用户 | 安全加固必做 |
CMD / ENTRYPOINT | 启动命令 | 一个 Dockerfile 只生效最后一个 CMD |
二、先写 .dockerignore
bash
cat > .dockerignore <<'EOF'
.git
node_modules
*.log
target
dist
Dockerfile
.dockerignore
EOF1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
少了这一步,.git 和 node_modules 会被送进构建上下文,构建既慢又容易把本地依赖带进镜像。
三、单阶段写法(对比用,不推荐)
dockerfile
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]1
2
3
4
5
2
3
4
5
这个镜像包含完整 Go 工具链,产物通常 800MB 以上,且暴露了编译器。
四、多阶段构建(推荐)
dockerfile
# 阶段一:构建
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server .
# 阶段二:运行
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata \
&& adduser -D -u 10001 appuser
WORKDIR /app
COPY --from=builder /out/server ./server
USER 10001
EXPOSE 8080
CMD ["./server"]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
关键点:
- 只把构建产物拷到运行阶段,编译器、源码、缓存全部丢弃。
CGO_ENABLED=0生成静态二进制,才能跑在 alpine 上。- 用非 root 用户(UID 10001)运行,降低被提权风险。
五、Node.js 版本示例
dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
六、执行构建与检查
bash
docker build -t myapp:1.0.0 .
docker images myapp
docker history myapp:1.0.0 # 看每层大小,定位体积来源
docker run --rm -p 8080:8080 myapp:1.0.01
2
3
4
2
3
4
七、利用构建缓存的顺序技巧
先只复制依赖清单文件(go.mod / package.json)并安装依赖,再复制源码。这样改代码不会让依赖层缓存失效,二次构建从几十秒降到数秒。
建议
RUN 里写 apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*,不要把 update 单独成层——否则缓存命中时会装上过期的旧包。
验证
- [ ]
docker build成功且无警告 - [ ]
docker images显示镜像体积相比单阶段明显下降 - [ ]
docker run后服务端口可访问 - [ ] 进入容器执行
id,返回的不是 root
常见坑
- 改了代码但镜像里还是旧逻辑:构建上下文被
.dockerignore排除了,或修改的文件不在COPY范围内。 - alpine 里程序报
not found:动态链接到 glibc 而 alpine 是 musl,用静态编译或换debian:slim。 - 构建极慢:未配
.dockerignore,每次都上传几百 MB 上下文。 COPY --from=builder路径写错:阶段名要与你AS定义的名字一致,大小写敏感。