Dockerfile 写作精讲 / 13 - 语言最佳实践
13 - 语言最佳实践:Node.js / Go / Java / Python / Rust
13.1 通用原则
无论使用哪种编程语言,以下原则始终适用:
| 原则 | 说明 |
|---|---|
| 多阶段构建 | 构建阶段与运行阶段分离 |
| 依赖缓存 | 先复制依赖清单,后复制源码 |
| 非 root 运行 | 使用 USER 指令 |
| 最小化镜像 | 选择合适的基础镜像 |
| 健康检查 | 使用 HEALTHCHECK 指令 |
| 安全扫描 | 定期更新依赖 |
13.2 Node.js
生产 Dockerfile
# syntax=docker/dockerfile:1
# ===== 阶段一:依赖安装 =====
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# ===== 阶段二:构建 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 如果需要生产依赖但不需要 devDependencies
RUN npm prune --production
# ===== 阶段三:生产 =====
FROM node:20-alpine AS production
WORKDIR /app
# 安全加固
RUN apk add --no-cache tini && \
addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -s /bin/sh -D appuser
# 仅复制生产所需文件
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]
Monorepo 处理
# 仅复制特定 workspace 的依赖
FROM node:20-alpine AS deps
WORKDIR /app
# 复制 workspace 配置
COPY package.json package-lock.json pnpm-workspace.yaml ./
COPY packages/api/package.json ./packages/api/
COPY packages/shared/package.json ./packages/shared/
RUN --mount=type=cache,target=/root/.npm \
npm ci --workspace=packages/api
# 构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build --workspace=packages/api
Next.js 应用
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js 需要环境变量进行构建
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
FROM node:20-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
COPY --from=builder --chown=appuser:appgroup /app/.next/standalone ./
COPY --from=builder --chown=appuser:appgroup /app/.next/static ./.next/static
COPY --from=builder --chown=appuser:appgroup /app/public ./public
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
pnpm
FROM node:20-alpine AS deps
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
13.3 Go
标准构建
# syntax=docker/dockerfile:1
# ===== 阶段一:构建 =====
FROM golang:1.22-alpine AS builder
WORKDIR /src
# 依赖缓存
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# 编译
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-ldflags="-s -w -extldflags '-static'" \
-o /server \
./cmd/server
# ===== 阶段二:生产 =====
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
COPY --from=builder /src/configs /configs
EXPOSE 8080
ENTRYPOINT ["/server"]
使用 scratch
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server ./cmd/server
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
CGO 依赖处理
FROM golang:1.22-alpine AS builder
# 安装 CGO 依赖
RUN apk add --no-cache gcc musl-dev sqlite-dev
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=1(需要 SQLite 等 C 库时)
RUN CGO_ENABLED=1 go build -tags "netgo osusergo" \
-ldflags="-s -w -linkmode external -extldflags '-static'" \
-o /server ./cmd/server
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
13.4 Java
Spring Boot
# syntax=docker/dockerfile:1
# ===== 阶段一:依赖解析 =====
FROM eclipse-temurin:21-jdk-alpine AS deps
WORKDIR /app
COPY .mvn .mvn
COPY mvnw pom.xml ./
RUN --mount=type=cache,target=/root/.m2 \
./mvnw dependency:go-offline --no-transfer-progress
# ===== 阶段二:构建 =====
FROM deps AS builder
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
./mvnw package -DskipTests --no-transfer-progress
# ===== 阶段三:提取层(Spring Boot Layers) =====
FROM eclipse-temurin:21-jdk-alpine AS extractor
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract --destination extracted
# ===== 阶段四:生产 =====
FROM eclipse-temurin:21-jre-alpine AS production
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -s /bin/sh -D appuser
WORKDIR /app
# 分层复制(利用缓存:不常变化的层先复制)
COPY --from=extractor /app/extracted/dependencies/ ./
COPY --from=extractor /app/extracted/spring-boot-loader/ ./
COPY --from=extractor /app/extracted/snapshot-dependencies/ ./
COPY --from=extractor /app/extracted/application/ ./
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", \
"org.springframework.boot.loader.launch.JarLauncher"]
GraalVM Native Image
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn -Pnative native:compile -DskipTests
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/target/myapp /app
EXPOSE 8080
ENTRYPOINT ["/app"]
13.5 Python
Django/Flask
# syntax=docker/dockerfile:1
# ===== 阶段一:构建 =====
FROM python:3.12-slim AS builder
WORKDIR /app
# 安装编译依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --prefix=/install -r requirements.txt
# ===== 阶段二:生产 =====
FROM python:3.12-slim AS production
# 安装运行时依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 curl && \
rm -rf /var/lib/apt/lists/*
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# 从 builder 复制 Python 包
COPY --from=builder /install /usr/local
# 复制应用代码
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", "--workers", "4", \
"--timeout", "120"]
FastAPI
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --prefix=/install -r requirements.txt
FROM python:3.12-slim AS production
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Poetry
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install poetry
COPY pyproject.toml poetry.lock ./
RUN --mount=type=cache,target=/root/.cache/pypoetry \
poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansi --only main
FROM python:3.12-slim AS production
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --chown=appuser:appuser . .
USER appuser
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
13.6 Rust
基本构建
# syntax=docker/dockerfile:1
# ===== 阶段一:构建 =====
FROM rust:1.77-alpine AS builder
WORKDIR /src
# 安装编译依赖(如果需要 OpenSSL 等)
RUN apk add --no-cache musl-dev openssl-dev
# 依赖缓存:先复制 Cargo.toml 和 Cargo.lock
COPY Cargo.toml Cargo.lock ./
# 创建空的 main.rs 以构建依赖缓存
RUN mkdir src && echo 'fn main() {}' > src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo build --release && \
rm -rf src
# 复制真正的源码并重新编译
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo build --release && \
cp target/release/server /server
# ===== 阶段二:生产 =====
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
使用 cargo-chef 优化缓存
# syntax=docker/dockerfile:1
FROM rust:1.77-alpine AS chef
RUN cargo install cargo-chef
WORKDIR /src
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /src/recipe.json recipe.json
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo build --release && \
cp target/release/server /server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
13.7 语言选择速查表
| 语言 | 基础镜像 | 生产镜像 | 特殊处理 |
|---|---|---|---|
| Node.js | node:20-alpine | node:20-alpine | npm ci + prune |
| Go | golang:1.22-alpine | distroless/static | CGO_ENABLED=0 |
| Java | eclipse-temurin:21-jdk-alpine | eclipse-temurin:21-jre-alpine | Spring Boot 分层 |
| Python | python:3.12-slim | python:3.12-slim | –prefix=/install |
| Rust | rust:1.77-alpine | distroless/static | cargo-chef |
| Ruby | ruby:3.3-slim | ruby:3.3-slim | bundle install |
| .NET | mcr.microsoft.com/dotnet/sdk:8.0 | mcr.microsoft.com/dotnet/aspnet:8.0 | dotnet publish |
13.8 扩展阅读
- Node.js Docker Best Practices
- Go Docker Best Practices
- Java Docker Best Practices
- Python Docker Best Practices
- Rust Docker Best Practices
上一章:12 - 镜像安全 下一章:14 - 常见构建模式 — Builder 模式、Rootless 模式与 Init 系统。