Files
zhuangang/topics/devops/docker/01-image-dockerfile.md
T

4.8 KiB
Raw Blame History

id, title, type, domain, stack, tags, level, est_hours, related, week, order
id title type domain stack tags level est_hours related week order
devops.docker.image-dockerfile 镜像和 Dockerfile:分层缓存、多阶段构建、非 root、信号处理 lesson devops
docker
Dockerfile
多阶段构建
镜像分层
构建缓存
distroless
basic 3
devops.docker.compose-network-volume
devops.docker.interview
3
6
1

镜像和 Dockerfile:分层缓存、多阶段构建、非 root、信号处理

解释说明

  • 镜像 vs 容器:镜像是只读的模板(像“类”),容器是镜像运行起来的实例(像“对象”),容器在镜像之上加一个可写层。
  • 分层:Dockerfile 中 RUN、COPY、ADD 每条指令产生一层。构建时如果某一层的输入没变,就直接用缓存;某一层变了,它后面的层全部重新构建。所以把变化少的放前面(安装依赖),变化多的放后面(复制源码)。
  • 多阶段构建:第一阶段用带完整工具链的镜像编译,第二阶段只复制编译产物到很小的运行镜像。Go 编译成静态二进制后,运行镜像可以是 distroless 甚至 scratch,体积从几百 MB 降到十几 MB,攻击面也小。
  • .dockerignore:排除 .git、node_modules、本地数据,减少构建上下文、避免把密钥打进镜像。
  • 非 root 运行:USER 指定普通用户,容器被攻破时影响更小。
  • PID 1 和信号:ENTRYPOINT ["/api"](exec 形式)让程序直接成为 PID 1,能收到 docker stop 发出的 SIGTERM 做优雅退出;ENTRYPOINT /api(shell 形式)会让 /bin/sh 成为 PID 1,信号传不到程序,最后被强制 kill。
  • CMD vs ENTRYPOINT:ENTRYPOINT 是固定执行的程序,CMD 是默认参数(运行时可以覆盖)。

常用命令:docker build -t api:1.0 .、docker run -d -p 8080:8080 --name api api:1.0、docker logs -f api、docker exec -it api sh(distroless 没有 shell)、docker images、docker history api:1.0(看每层大小)。

案例

Go 服务最初用 FROM golang 单阶段构建,镜像 800MB+,每次改一行代码都要重新下载依赖。改成:先 COPY go.mod go.sum 再 go mod download(依赖层可缓存)→ 再复制源码编译 → 运行阶段用 distroless,镜像十几 MB,代码改动时构建只需重新编译。

Python(FastAPI)服务同理:先复制 requirements.txt / pyproject.toml 安装依赖,再复制源码;用 python:3.12-slim 作为基础镜像。

代码示例

Go 服务的多阶段 Dockerfile(完整示例见 examples/go-api-compose):

# syntax=docker/dockerfile:1
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download                 # 依赖层:go.mod 不变就用缓存
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]                 # exec 形式:能收到 SIGTERM

Python FastAPI 服务(示意,按项目实际的依赖文件调整):

FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Go 程序里配合优雅退出:

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go srv.ListenAndServe()
<-ctx.Done()                                   // 收到 docker stop 的 SIGTERM
shutdownCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)                      // 等正在处理的请求完成

易错点

  • COPY . . 放在安装依赖之前:每次改代码都重新安装依赖,构建很慢。
  • 把密钥写进 Dockerfile 或复制进镜像:镜像的每一层都能被查看,删掉的文件也还在之前的层里。密钥通过运行时环境变量或密钥管理注入。
  • 使用 latest 标签:不知道实际跑的是哪个版本,无法回滚。用版本号或 Git 提交哈希。
  • 服务监听 127.0.0.1:容器外访问不到,要监听 0.0.0.0。
  • shell 形式的 ENTRYPOINT:收不到 SIGTERM,每次停止都要等 10 秒后被强杀,正在处理的请求被中断。
  • CGO 编译的二进制放进 scratch / distroless static:缺少 libc 运行不起来,要 CGO_ENABLED=0 或换带 libc 的基础镜像。

面试常问

见 Docker 面试题:镜像和容器的区别、分层与缓存、多阶段构建、如何减小镜像体积、CMD 和 ENTRYPOINT。