Skip to content
第 4 章 运维 ⏱ 13 分钟阅读

第 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降权运行

动手练习 ​

  1. 给自己的 Spring Boot 项目写 Dockerfile,多阶段(JDK 编 → JRE 跑),最终镜像比 JDK 镜像小
  2. 加 .dockerignore,验证 COPY . . 不再复制 .git
  3. 用 docker history 看每层体积,合并 RUN 减少层数
  4. 加 HEALTHCHECK,启动后用 docker ps 看 STATUS 变 healthy

下一章:第 5 章:Docker Compose →

本站基于 VitePress 构建 · 由 StackHub 团队维护