容器化基础:Docker核心概念与最佳实践
各位同学好,今天我们来聊聊容器化。说实话,Docker 这东西刚出来的时候,我第一反应是「又一个虚拟化技术?」。但真正用上之后才发现,它彻底改变了我们交付软件的方式。我到现在还记得第一次用 Docker 部署微服务时那种「终于不用再配环境了」的畅快感。
好,咱们直接进入正题。这一章我会从三个核心概念讲起,然后重点说说 Dockerfile 怎么写才专业,最后用一个实战技巧——多阶段构建,帮你把镜像体积砍到最小。
一、三大核心概念:镜像、容器、仓库
这三个词你肯定听过,但很多人其实没搞清它们之间的关系。我打个比方你就明白了:
- 镜像(Image):就像是一个 ISO 安装盘。它包含了运行应用所需的一切——代码、运行时、系统库、环境变量。它是静态的,只读的。
- 容器(Container):镜像跑起来之后就是容器。你可以把它理解成一个轻量级的虚拟机实例。它是动态的,可读写的。
- 仓库(Registry):存放镜像的地方。Docker Hub 是官方的公共仓库,就像 GitHub 存代码一样,它存镜像。
一句话总结:镜像就是模板,容器就是实例,仓库就是超市。你从仓库拉镜像,用镜像创建容器,容器里跑你的应用。
我在项目中遇到过不少新人,把容器当虚拟机用,进去装软件、改配置。嗯,这其实是个坏习惯。容器应该是「用完即弃」的,任何修改都应该通过重新构建镜像来实现。
二、Dockerfile 最佳实践
写 Dockerfile 看起来简单,但想写好真得花点功夫。我见过太多「能用就行」的 Dockerfile,结果镜像体积几个 G,构建时间半小时。下面这几条是我这些年总结出来的铁律。
2.1 选择合适的基础镜像
别一上来就用 ubuntu:latest。你想想看,你的应用可能只需要一个 Python 运行时,为什么要带上一整套 Ubuntu 系统?
| 基础镜像 | 大小 | 适用场景 |
|---|---|---|
| ubuntu:22.04 | ~77MB | 需要完整系统工具链 |
| debian:bullseye-slim | ~80MB | 需要 glibc 兼容性 |
| alpine:3.18 | ~5MB | 追求最小体积 |
| scratch | 0MB | 静态编译的 Go/C 应用 |
我个人习惯是:能用 alpine 就用 alpine。但要注意,alpine 用的是 musl libc,有些依赖 glibc 的程序会出问题。我之前有个 Java 应用在 alpine 上跑,结果 JNI 调用直接崩了,排查了半天才发现是 libc 不兼容。
2.2 合理利用构建缓存
Docker 构建镜像时,每一行指令都会生成一个缓存层。如果你把经常变动的指令放在前面,那缓存基本就废了。
# ❌ 错误示范:先复制代码,再安装依赖
COPY . /app
RUN pip install -r requirements.txt
# ✅ 正确做法:先复制依赖文件,再安装,最后复制代码
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app
为什么会这样?因为只要 COPY . /app 里的文件有变动,这一层缓存就失效了,后面的所有层都得重新构建。把依赖安装放在前面,只要 requirements.txt 不变,就能复用缓存。我在 CI 流水线里这么一改,构建时间从 5 分钟降到了 30 秒。
2.3 减少层数,合并指令
每个 RUN、COPY、ADD 都会产生一层。层数太多不仅构建慢,拉取也慢。
# ❌ 不推荐:太多层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# ✅ 推荐:合并成一层
RUN apt-get update && \
apt-get install -y curl vim && \
rm -rf /var/lib/apt/lists/*
小技巧:用 && 连接命令,最后记得清理缓存文件。apt 的 /var/lib/apt/lists/*、pip 的 ~/.cache/pip,这些不删掉都会留在镜像里。
2.4 使用 .dockerignore
这个文件的作用跟 .gitignore 类似。你肯定不想把本地的 node_modules、.git、日志文件都打包进镜像吧?
# .dockerignore 示例
.git
node_modules
*.log
.env
dist/
__pycache__/
我曾经见过一个同事,镜像构建出来 2GB,排查了半天发现他把整个 node_modules 目录(1.5GB)都打包进去了。加了 .dockerignore 之后,镜像直接缩到 200MB。
三、多阶段构建:把镜像瘦到极致
这是我最喜欢的一个技巧。说白了就是:用两个(或多个)阶段来构建镜像。第一阶段负责编译、安装依赖,第二阶段只复制最终产物。
为什么要这么做?因为编译阶段需要很多工具链——gcc、make、各种开发头文件。但这些运行时根本不需要。多阶段构建让你可以「用完就扔」。
# 第一阶段:编译阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
# 第二阶段:运行阶段
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
你看,第一阶段用了 golang:1.21,这个镜像有 800MB 左右。但第二阶段只用了 alpine:3.18,加上编译好的二进制文件,最终镜像可能只有 20MB。
关键点:COPY --from=builder 这个语法,可以从上一个阶段复制文件。你甚至可以跨多个阶段复制,比如第一阶段编译前端,第二阶段编译后端,第三阶段把它们合并。
我在生产环境里用多阶段构建优化过一个 Java 应用。原来的镜像基于 openjdk:11-jdk,大小 600MB。改成多阶段构建后,第一阶段用 maven:3.8 编译,第二阶段用 openjdk:11-jre(只有 200MB)运行。最终镜像 250MB,而且运行时不需要 JDK,只需要 JRE。
注意:多阶段构建虽然好,但别滥用。如果你的应用本身就是解释型语言(Python、Node.js),多阶段构建的效果有限。这时候重点应该放在基础镜像选择和依赖裁剪上。
四、实战总结
好,我们来快速回顾一下这一章的核心要点:
- 镜像、容器、仓库——模板、实例、超市,这个比喻你记住了吗?
- Dockerfile 最佳实践——选小镜像、用缓存、合并指令、加
.dockerignore。 - 多阶段构建——编译和运行分离,镜像体积能砍掉 80% 以上。
说实话,这些技巧看起来简单,但真正坚持做的人不多。我见过太多团队,镜像越堆越大,最后 CI 跑一次要半小时。你想想看,如果每次部署都要等半小时,那还谈什么敏捷开发?
下一章我们会聊聊容器编排,到时候你会看到,镜像大小对 Kubernetes 集群的启动速度影响有多大。嗯,今天就到这里,有问题咱们评论区见。