2、容器化基础:Docker核心概念与镜像构建实战
聊到云原生,Docker 是绕不开的第一道坎。我记得刚接触容器化那会儿,觉得这东西不就是个轻量级虚拟机吗?后来踩了不少坑才明白——容器和虚拟机,本质上是两码事。今天咱们就把 Docker 的三个核心概念掰开揉碎,再聊聊镜像构建的那些实战经验。
2.1 镜像、容器、仓库:铁三角关系
这三个概念,说白了就是模板、实例和超市的关系。
- 镜像(Image):一个只读的模板,包含运行应用所需的一切——代码、运行时、系统工具、库、配置。你可以把它理解成操作系统的 ISO 文件。
- 容器(Container):镜像的运行实例。镜像启动后,加上一层可写层,就成了容器。我习惯把容器比作「从镜像这个模具里倒出来的冰棍」。
- 仓库(Registry):存放镜像的地方。Docker Hub 是公共仓库,企业一般用 Harbor 或阿里云 ACR 搭私有仓库。
一句话总结:镜像 build 一次,run 成容器,push 到仓库共享。这是 CI/CD 流水线的基石。
我在项目中遇到过一个问题:同事把镜像和容器搞混了,以为删了容器镜像也没了。结果重新部署时发现镜像还在,但容器日志全丢了。嗯,这里要注意——容器是 ephemeral(临时)的,数据要挂载卷来持久化。
2.2 Dockerfile 最佳实践:从能用→好用
写 Dockerfile 谁都会,但写出「好」的 Dockerfile 需要经验。我总结了 6 条铁律,你照着写基本不会出大问题。
2.2.1 选择合适的基础镜像
别上来就 FROM ubuntu:latest。你想想看,一个 Python 应用需要完整的 Ubuntu 吗?
| 基础镜像 | 大小 | 适用场景 |
|---|---|---|
| ubuntu:22.04 | ~77MB | 需要完整系统工具链 |
| alpine:3.18 | ~5MB | 追求极致镜像体积 |
| python:3.11-slim | ~120MB | Python 应用首选 |
| golang:1.21-alpine | ~300MB | Go 编译环境 |
我个人习惯:生产环境用 -slim 或 -alpine 变体。Alpine 虽然小,但用的是 musl libc,有些 C 扩展会编译失败。遇到这种情况,换 -slim 基本能解决。
2.2.2 分层构建,利用缓存
Docker 的镜像是由一层层只读文件系统堆叠起来的。每一行 RUN、COPY、ADD 都会产生一个新层。构建时,如果某层没变化,Docker 会直接复用缓存。
所以,把不常变动的指令放前面,频繁变动的放后面。比如:
# ❌ 错误写法:每次改代码都要重装依赖
COPY . /app
RUN pip install -r requirements.txt
# ✅ 正确写法:先装依赖,再拷代码
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app
我曾经因为顺序写反了,每次代码提交都要等 5 分钟装依赖。后来改成这样,构建时间从 5 分钟降到了 30 秒。
2.2.3 合并 RUN 指令,减少层数
每多一层,镜像就大一点,拉取就慢一点。把相关的操作合并到一条 RUN 里:
# ❌ 不推荐
RUN apt-get update
RUN apt-get install -y curl vim
RUN apt-get clean
# ✅ 推荐
RUN apt-get update && \
apt-get install -y curl vim && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
小技巧:最后加上 rm -rf /var/lib/apt/lists/* 清理 apt 缓存,能再省几十 MB。Alpine 的话用 rm -rf /var/cache/apk/*。
2.2.4 使用 .dockerignore
你肯定不想把 node_modules、.git、__pycache__ 这些文件打包进镜像吧?写个 .dockerignore:
.git
node_modules
__pycache__
*.md
.env
Dockerfile
.dockerignore
这能显著加快 docker build 的上下文传输速度。我见过一个项目没加 ignore,每次构建要传 2GB 的 node_modules……
2.2.5 多阶段构建
这是我最喜欢的功能。你可以在一个 Dockerfile 里用多个 FROM 语句,前面的阶段用来编译,后面的阶段只保留产物。
# 第一阶段:编译
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
# 第二阶段:运行
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]
最终镜像只有十几 MB,而且没有 Go 编译器、没有源码。安全又轻量。
2.2.6 以非 root 用户运行
默认容器是以 root 运行的,这很危险。如果容器被攻破,攻击者直接有宿主机 root 权限。创建一个普通用户:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
警告:千万别在生产容器里用 root 跑应用。我曾经审计过一个客户的镜像,里面居然有 SSH 服务且 root 密码是 123456……
2.3 镜像构建与优化策略
构建镜像不只是写个 Dockerfile 然后 docker build。真正生产级的构建,要考虑速度、大小、安全性。
2.3.1 构建速度优化
- 利用 BuildKit:
DOCKER_BUILDKIT=1 docker build,支持并发构建和更好的缓存。 - 远程缓存:在 CI 中把缓存推送到仓库,下次构建直接拉取缓存层。
- 按需安装:只装运行时需要的包,不要装
vim、curl这些调试工具。
2.3.2 镜像大小优化
我见过最大的镜像有 2.3GB,里面甚至包含了 GCC 编译器。优化后只有 150MB。怎么做到的?
- 选小基础镜像:Alpine 或 Distroless。
- 清理临时文件:apt 缓存、pip 缓存、npm 缓存,用完就删。
- 多阶段构建:只保留运行产物。
- 压缩层:用
docker-slim工具进一步瘦身。
2.3.3 安全性优化
- 定期扫描漏洞:用 Trivy 或 Clair 扫描镜像。
- 固定基础镜像版本:别用
latest,用alpine:3.18.0这种具体版本。 - 最小权限原则:非 root 用户 + 只读文件系统(
--read-only)。
核心原则:镜像越小,攻击面越小;层数越少,构建越快;用户越普通,容器越安全。
2.4 实战:一个生产级 Dockerfile
最后,给你看一个我目前在用的 Node.js 应用 Dockerfile。它融合了上面所有最佳实践:
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# 运行阶段
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
这个镜像最终大小约 180MB,比直接用 node:18 的 1GB 小了 80%。而且没有源码、没有构建工具、没有 root 权限。嗯,这才叫生产级。
好了,Docker 核心概念和镜像构建就聊到这儿。下一章咱们会深入容器编排,聊聊 Kubernetes 的那些事儿。记住一句话:镜像构建是容器化的起点,也是运维的基石。把这一步做扎实了,后面的路会顺很多。