Skip to main content

Dockerfile 最佳实践

基础结构

基本 Dockerfile

# 使用官方基础镜像
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 3000

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

选择基础镜像

镜像大小对比

# Ubuntu 完整镜像 (~77MB)
FROM ubuntu:22.04

RUN apt-get update && \
apt-get install -y nodejs npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

常用基础镜像推荐

# Node.js
FROM node:18-alpine

# Python
FROM python:3.11-slim

# .NET
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine

# Go
FROM golang:1.21-alpine AS builder
FROM alpine:3.18

# Java
FROM eclipse-temurin:17-jre-alpine

多阶段构建

.NET 应用示例

# 构建阶段
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# 复制项目文件
COPY ["MyApp/MyApp.csproj", "MyApp/"]
RUN dotnet restore "MyApp/MyApp.csproj"

# 复制所有文件并构建
COPY . .
WORKDIR "/src/MyApp"
RUN dotnet build "MyApp.csproj" -c Release -o /app/build

# 发布阶段
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish /p:UseAppHost=false

# 运行阶段
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY --from=publish /app/publish .

# 创建非 root 用户
RUN addgroup -g 1001 -S appuser && \
adduser -S appuser -u 1001
USER appuser

EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]

Node.js 应用示例

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app

# 安装依赖
COPY package*.json ./
RUN npm ci

# 构建应用
COPY . .
RUN npm run build && \
npm prune --production

# 生产阶段
FROM node:18-alpine
WORKDIR /app

# 复制必要文件
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# 非 root 用户
RUN addgroup -g 1001 nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
USER nodejs

EXPOSE 3000
CMD ["node", "dist/main.js"]

Go 应用示例

# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /build

# 下载依赖
COPY go.mod go.sum ./
RUN go mod download

# 构建
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# 运行阶段
FROM alpine:3.18
WORKDIR /app

# 安装必要的证书
RUN apk --no-cache add ca-certificates

# 复制编译后的二进制文件
COPY --from=builder /build/main .

# 非 root 用户
RUN addgroup -g 1001 appuser && \
adduser -D -u 1001 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser

EXPOSE 8080
CMD ["./main"]

层缓存优化

合理安排 COPY 顺序

# ❌ 不好的做法 - 代码改变会重新安装依赖
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install

# ✅ 好的做法 - 依赖不变时使用缓存
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

合并 RUN 命令

# ❌ 不好的做法 - 创建多个层
FROM alpine:3.18
RUN apk update
RUN apk add curl
RUN apk add git
RUN apk add vim

# ✅ 好的做法 - 减少层数
FROM alpine:3.18
RUN apk update && \
apk add --no-cache \
curl \
git \
vim && \
rm -rf /var/cache/apk/*

利用 BuildKit 缓存挂载

# syntax=docker/dockerfile:1

FROM node:18-alpine AS builder
WORKDIR /app

# 使用缓存挂载加速 npm install
RUN --mount=type=cache,target=/root/.npm \
npm ci

COPY . .
RUN npm run build

减小镜像体积

1. 清理缓存和临时文件

FROM node:18-alpine

WORKDIR /app

# 单个 RUN 命令中完成所有操作
RUN apk add --no-cache python3 make g++ && \
npm ci && \
npm run build && \
npm prune --production && \
apk del python3 make g++ && \
rm -rf /root/.npm /tmp/*

2. 使用 .dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.github
.gitignore
README.md
.env
.env.local
*.md
coverage
.vscode
.idea
dist
build

3. 只复制必要文件

# ❌ 不好的做法
COPY . .

# ✅ 好的做法
COPY package*.json ./
RUN npm ci
COPY src ./src
COPY public ./public

4. 使用多阶段构建

# 完整示例
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 只保留生产文件
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
CMD ["node", "dist/main.js"]

安全最佳实践

1. 使用非 root 用户

FROM node:18-alpine

WORKDIR /app

# 安装依赖(需要 root)
COPY package*.json ./
RUN npm ci

# 复制应用文件
COPY . .

# 创建用户并切换
RUN addgroup -g 1001 nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app

USER nodejs

EXPOSE 3000
CMD ["node", "server.js"]

2. 不在镜像中存储敏感信息

# ❌ 不好的做法
FROM node:18-alpine
COPY .env .
RUN echo "API_KEY=secret123" > .env

# ✅ 好的做法 - 使用环境变量
FROM node:18-alpine
ENV NODE_ENV=production
# 在运行时通过 -e 或 docker-compose 传递敏感信息

3. 使用固定版本标签

# ❌ 不好的做法
FROM node:latest

# ✅ 好的做法
FROM node:18.17.1-alpine3.18

4. 扫描漏洞

# 使用 Docker Scout
docker scout quickview myapp:latest

# 使用 Trivy
trivy image myapp:latest

# 使用 Snyk
snyk container test myapp:latest

5. 最小化攻击面

FROM node:18-alpine

# 只安装必要的包
RUN apk add --no-cache tini

# 使用 tini 作为 init 进程
ENTRYPOINT ["/sbin/tini", "--"]

# 只读文件系统
# docker run --read-only --tmpfs /tmp myapp

USER nodejs
CMD ["node", "server.js"]

健康检查

基本健康检查

FROM node:18-alpine

WORKDIR /app
COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js

CMD ["node", "server.js"]

HTTP 健康检查

FROM node:18-alpine

# 安装 curl
RUN apk add --no-cache curl

WORKDIR /app
COPY . .

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

EXPOSE 3000
CMD ["node", "server.js"]

自定义健康检查脚本

FROM node:18-alpine

WORKDIR /app
COPY . .
COPY healthcheck.sh /usr/local/bin/

RUN chmod +x /usr/local/bin/healthcheck.sh

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["/usr/local/bin/healthcheck.sh"]

CMD ["node", "server.js"]
# healthcheck.sh
#!/bin/sh
response=$(wget --no-verbose --tries=1 --spider http://localhost:3000/health 2>&1)
if [ $? -eq 0 ]; then
exit 0
else
exit 1
fi

环境变量和参数

构建参数 (ARG)

# 定义构建参数
ARG NODE_VERSION=18
ARG APP_VERSION=1.0.0

FROM node:${NODE_VERSION}-alpine

WORKDIR /app

# ARG 可以在构建时传递
LABEL version="${APP_VERSION}"

COPY . .
RUN npm ci

CMD ["node", "server.js"]
# 构建时传递参数
docker build --build-arg NODE_VERSION=20 --build-arg APP_VERSION=2.0.0 -t myapp .

环境变量 (ENV)

FROM node:18-alpine

# 设置环境变量
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info

WORKDIR /app
COPY . .

EXPOSE ${PORT}
CMD ["node", "server.js"]

运行时环境变量

# docker run 传递
docker run -e NODE_ENV=development -e PORT=8080 myapp

# docker-compose.yml
version: '3.8'
services:
app:
image: myapp
environment:
NODE_ENV: production
PORT: 3000
env_file:
- .env

实用技巧

1. 条件复制文件

# 使用通配符复制可选文件
COPY package*.json ./
COPY *.config.js ./

2. 使用 ONBUILD 指令

# 基础镜像 Dockerfile
FROM node:18-alpine
WORKDIR /app
ONBUILD COPY package*.json ./
ONBUILD RUN npm ci
ONBUILD COPY . .

# 使用基础镜像
FROM myorg/node-base
EXPOSE 3000
CMD ["node", "server.js"]

3. 多平台构建

# 创建构建器
docker buildx create --name mybuilder --use

# 构建多平台镜像
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

# 构建并推送
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

4. 使用 heredoc

# syntax=docker/dockerfile:1

FROM alpine:3.18

# 使用 heredoc 创建文件
RUN <<EOF
apk update
apk add nginx
mkdir -p /run/nginx
EOF

# 创建配置文件
COPY <<EOF /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
server {
listen 80;
root /var/www/html;
}
}
EOF

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

5. 使用缓存挂载

# syntax=docker/dockerfile:1

FROM golang:1.21-alpine AS builder

WORKDIR /build

# 使用缓存挂载加速构建
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download

COPY . .

RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o app .

FROM alpine:3.18
COPY --from=builder /build/app /app
CMD ["/app"]

常见场景示例

静态网站 (Nginx)

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

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

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

Python Flask 应用

FROM python:3.11-slim

WORKDIR /app

# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用
COPY . .

# 非 root 用户
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
USER appuser

EXPOSE 5000

# 使用 gunicorn 运行
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]

Java Spring Boot 应用

FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app

# 复制 Maven 文件
COPY pom.xml .
COPY src ./src

# 构建
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# 复制 jar 文件
COPY --from=builder /app/target/*.jar app.jar

# 非 root 用户
RUN addgroup -g 1001 spring && \
adduser -D -u 1001 -G spring spring
USER spring

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

调试技巧

1. 在特定阶段停止

# 在构建阶段停止以检查
FROM node:18-alpine AS debug
WORKDIR /app
COPY package*.json ./
RUN npm ci
# 构建时: docker build --target debug -t myapp:debug .

2. 保留构建缓存

# 不使用缓存
docker build --no-cache -t myapp .

# 使用特定缓存
docker build --cache-from myapp:latest -t myapp .

3. 查看镜像层

# 查看镜像历史
docker history myapp:latest

# 查看详细信息
docker inspect myapp:latest

4. 导出文件系统

# 导出镜像文件系统
docker save myapp:latest -o myapp.tar
tar -xvf myapp.tar

# 或使用 dive 工具
dive myapp:latest

性能优化

1. 并行构建

# syntax=docker/dockerfile:1

FROM node:18-alpine

WORKDIR /app

# 并行复制和安装
COPY package*.json ./
RUN --network=none npm ci

# 使用 BuildKit 并行
RUN --mount=type=cache,target=/root/.npm \
npm ci

2. 减少上下文大小

# 检查构建上下文大小
du -sh .

# 使用 .dockerignore
# 只构建必要文件
docker build -f Dockerfile -t myapp:latest .

3. 使用本地缓存

# 启用 BuildKit
export DOCKER_BUILDKIT=1

# 使用内联缓存
docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t myapp .

最佳实践清单

Dockerfile 最佳实践

基础

  • ✅ 使用官方基础镜像
  • ✅ 使用特定版本标签
  • ✅ 选择最小化的镜像(alpine、slim)
  • ✅ 使用多阶段构建

层优化

  • ✅ 合并 RUN 命令
  • ✅ 合理安排 COPY 顺序
  • ✅ 使用 .dockerignore
  • ✅ 清理缓存和临时文件

安全

  • ✅ 使用非 root 用户
  • ✅ 不存储敏感信息
  • ✅ 扫描漏洞
  • ✅ 最小化攻击面
  • ✅ 使用健康检查

可维护性

  • ✅ 添加 LABEL 元数据
  • ✅ 使用 ARG 提高灵活性
  • ✅ 明确暴露端口
  • ✅ 使用明确的 CMD/ENTRYPOINT
  • ✅ 添加注释说明

相关资源