Docker Dockerfile

FROM/RUN/COPY/CMD/ENTRYPOINT 的语义差别,多阶段构建,以及分层缓存怎么用对。

Beginner
Key Points把变化最少的指令放前面以命中缓存;COPY 依赖清单先于 COPY 源码,是多阶段构建之外最有效的优化。

最小可用

Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

指令速查

InstructionMeaningCreates a layer
FROMBase image—
WORKDIRSet working dir (creates it)Yes
COPY / ADDCopy files in (ADD also untars URLs)Yes
RUNExecute at build timeYes
ENV / ARGRuntime env / build-time argYes
EXPOSEDocument the port (does not publish)Yes
CMDDefault command, overridableNo
ENTRYPOINTFixed executable, args appendedNo

CMD 与 ENTRYPOINT

Dockerfile
# 只有 CMD:docker run img bash 会覆盖成 bash
CMD ["node", "server.js"]

# ENTRYPOINT + CMD:docker run img --port=8080 → node server.js --port=8080
ENTRYPOINT ["node", "server.js"]
CMD ["--port=3000"]

Rule of thumb: ENTRYPOINT is what the image *is*, CMD is the default arguments. Use the exec form, not the shell form — the shell form wraps in /bin/sh -c, so PID 1 is the shell and your process never sees SIGTERM.

多阶段构建

Dockerfile
# 构建阶段
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN mvn -q dependency:go-offline
COPY src ./src
RUN mvn -q package -DskipTests

# 运行阶段
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /src/target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

The final image ships only the JRE and the jar. Size drops from ~700MB to ~180MB and the attack surface shrinks with it.

缓存怎么命中

Dockerfile
# 好:先只拷依赖清单,代码变了也不影响这一层缓存
COPY package*.json ./
RUN npm ci
COPY . .

# 坏:一改代码,依赖就要重装
COPY . .
RUN npm ci

常用技巧

  • .dockerignore 排除 node_modules、.git、target
  • RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/* 写在一条里
  • 用非 root 用户运行:USER app
  • 需要编译工具但不想留在镜像里,就用多阶段
Splitting apt-get update and apt-get install into two RUNs hits a cache trap: the update layer is cached, so install uses a stale index and fails.

Friends & Partners