第 4 章:Dockerfile 与镜像
学习目标
- 编写多阶段 Dockerfile
- 掌握 13 个核心指令
- 减少镜像体积、加速构建
- 避开缓存失效和 root 运行的坑
一、最小 Dockerfile(Node 示例)
dockerfile
# 基础镜像
FROM node:20-alpine
# 工作目录(不存在会自动建)
WORKDIR /app
# 先复制依赖清单,利用缓存
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源码
COPY . .
# 暴露端口(只是声明,实际靠 -p 映射)
EXPOSE 3000
# 容器启动命令
CMD ["node", "server.js"]构建并跑:
bash
docker build -t myapp:1.0 . # . 表示当前目录的 Dockerfile
docker run -d -p 3000:3000 myapp:1.0⚠️ 坑 1:
FROM node拉的是node:latest,别用 latest。固定到具体版本(node:20.11-alpine),否则构建不可复现。
二、13 个核心指令
dockerfile
FROM ubuntu:22.04 # 基础镜像,必备
LABEL maintainer="me@example.com" # 标签(新版用 LABEL,不推荐 MAINTAINER)
WORKDIR /app # 后续指令的工作目录
COPY src/ dst/ # 本地文件复制(推荐)
ADD src.tar.gz /dst/ # 远程/解压(慎用,缓存差)
RUN apt update && apt install -y curl # 构建时执行命令
ENV NODE_ENV=production # 设置环境变量
ARG VERSION=1.0 # 构建参数(--build-arg 传)
EXPOSE 8080 # 声明端口(只是文档)
VOLUME ["/data"] # 声明匿名卷挂载点
USER nginx # 切换用户(root → 普通)
CMD ["nginx", "-g", "daemon off;"] # 容器启动命令(可被 docker run 覆盖)
ENTRYPOINT ["python"] # 入口(更难覆盖)⚠️ 坑 2:
RUN命令多行写合并成一层,否则每行都是一层镜像:dockerfile# 错 RUN apt update RUN apt install -y curl # 对:&& 连接 + 清理缓存 RUN apt update && apt install -y --no-install-recommends \ curl vim git \ && rm -rf /var/lib/apt/lists/*
三、CMD vs ENTRYPOINT
dockerfile
# CMD:可被覆盖
CMD ["node", "app.js"]
docker run myapp # 跑 node app.js
docker run myapp node test.js # 覆盖成 node test.js
# ENTRYPOINT:更难覆盖(必须 --entrypoint)
ENTRYPOINT ["python"]
CMD ["app.py"]
docker run myapp # 跑 python app.py
docker run myapp manage.py shell # 跑 python manage.py shell(--entrypoint 不变)JSON 数组语法 vs shell 语法:
dockerfile
# 对(推荐):不经过 shell,信号能传到进程
CMD ["nginx", "-g", "daemon off;"]
# 错:nginx 跑在 shell 里,不会接收 SIGTERM,docker stop 要 10 秒才退
CMD nginx -g "daemon off;"⚠️ 坑 3:
docker stop发SIGTERM,容器主进程必须接收并优雅退出。否则强制等待 10 秒后才SIGKILL。JSON 语法才能让信号穿透。
四、多阶段构建(瘦身神器)
构建产物 vs 运行产物分开。Java 应用必备:
dockerfile
# 阶段 1:编译
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /src
COPY . .
RUN ./mvnw -B -ntp package -DskipTests
# 阶段 2:运行(更小的镜像)
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /src/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]体积对比:
| 镜像 | 大小 |
|---|---|
eclipse-temurin:17-jdk | ~470MB |
eclipse-temurin:17-jre | ~280MB |
| 多阶段构建后 | ~280MB |
⚠️ 坑 4:多阶段构建一定要加
AS 别名,否则--from=builder引用不到前一阶段。
五、镜像缓存优化
Dockerfile 每一层缓存,改一行下面的都失效:
dockerfile
FROM node:20-alpine
WORKDIR /app
# 先复制依赖清单(几乎不变)
COPY package*.json ./
RUN npm ci # 这一层会因为 package.json 变化才重跑
# 再复制源码(变频繁)
COPY . . # 源码变了,不会让 npm ci 重跑
CMD ["node", "server.js"].dockerignore 排除不必要文件,和 .gitignore 一样重要:
.git
node_modules
dist
*.log
.env
README.md⚠️ 坑 5:
COPY . .把node_modules一起复制进去,镜像巨大。先.dockerignore排除再加RUN npm ci重建。
六、用普通用户跑(安全)
默认 root 跑容器,一旦逃逸就是宿主机 root。生产环境必须降权:
dockerfile
FROM eclipse-temurin:17-jre
# 建用户
RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app
WORKDIR /app
COPY --chown=app:app target/*.jar app.jar
USER app # 切换
ENTRYPOINT ["java", "-jar", "app.jar"]验证:
bash
docker run --rm myapp id # 输出 uid=1000(app),不是 root⚠️ 坑 6:
USER app之前所有文件属主是 root。COPY 时加--chown才能让普通用户写。
七、健康检查
dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1查看状态:
bash
docker ps # STATUS 列显示 healthy / unhealthy
docker inspect --format='{{.State.Health.Status}}' myapp⚠️ 坑 7:
HEALTHCHECK命令必须在镜像里有curl或wget。Alpine 镜像默认没有,先RUN apk add --no-cache curl。
八、镜像导入导出
bash
# 保存
docker save -o myapp.tar myapp:1.0
# 加载
docker load -i myapp.tar
# 历史(看每层体积)
docker history myapp:1.0九、本章小结
| 指令 | 作用 |
|---|---|
FROM | 基础镜像 |
COPY / RUN | 复制文件 / 执行命令 |
CMD / ENTRYPOINT | 启动命令 |
| 多阶段 | 编译 + 运行分离,瘦身 |
.dockerignore | 排除构建不需文件 |
USER | 降权运行 |
动手练习
- 给自己的 Spring Boot 项目写 Dockerfile,多阶段(JDK 编 → JRE 跑),最终镜像比 JDK 镜像小
- 加
.dockerignore,验证COPY . .不再复制.git - 用
docker history看每层体积,合并 RUN 减少层数 - 加
HEALTHCHECK,启动后用docker ps看 STATUS 变healthy
下一章:第 5 章:Docker Compose →