4.8 KiB
4.8 KiB
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 |
|
|
basic | 3 |
|
|
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。 CMDvsENTRYPOINT: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。