Docker Dockerfile
FROM/RUN/COPY/CMD/ENTRYPOINT 的语义差别,多阶段构建,以及分层缓存怎么用对。
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"]指令速查
| Instruction | Meaning | Creates a layer |
|---|---|---|
| FROM | Base image | — |
| WORKDIR | Set working dir (creates it) | Yes |
| COPY / ADD | Copy files in (ADD also untars URLs) | Yes |
| RUN | Execute at build time | Yes |
| ENV / ARG | Runtime env / build-time arg | Yes |
| EXPOSE | Document the port (does not publish) | Yes |
| CMD | Default command, overridable | No |
| ENTRYPOINT | Fixed executable, args appended | No |
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、targetRUN 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.最小可用
Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]指令速查
| 指令 | 含义 | 产生新层 |
|---|---|---|
| FROM | 基础镜像 | — |
| WORKDIR | 设置工作目录 | 是 |
| COPY / ADD | 复制文件(ADD 还能解压/下载) | 是 |
| RUN | 构建时执行 | 是 |
| ENV / ARG | 运行时环境变量 / 构建参数 | 是 |
| EXPOSE | 声明端口(不发布) | 是 |
| CMD | 默认命令,可被覆盖 | 否 |
| ENTRYPOINT | 固定入口,参数追加 | 否 |
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"]记住一条:ENTRYPOINT 是「这个镜像就是干这个的」,CMD 是「默认参数」。两者都用 exec 数组形式,别用 shell 形式——shell 形式会包一层 /bin/sh -c,导致 PID 1 不是你的进程,收不到 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"]最终镜像只包含 JRE 和 jar,编译工具链不进生产镜像。体积从 700MB 降到 180MB 左右,攻击面也小得多。
缓存怎么命中
Dockerfile
# 好:先只拷依赖清单,代码变了也不影响这一层缓存
COPY package*.json ./
RUN npm ci
COPY . .
# 坏:一改代码,依赖就要重装
COPY . .
RUN npm ci常用技巧
.dockerignore排除node_modules、.git、targetRUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*写在一条里- 用非 root 用户运行:
USER app - 需要编译工具但不想留在镜像里,就用多阶段
RUN apt-get update 和 RUN apt-get install 分成两条会踩缓存坑:update 层命中缓存时,install 拿到的是过期的索引,导致装不上包。