3. 容器化部署实战:Dockerfile优化、镜像构建缓存、容器资源限制、数据持久化陷阱
容器化部署,说白了就是把应用连同它的运行环境一起打包。听起来简单,但坑是真不少。我见过太多团队,Dockerfile 写得像流水账,镜像几个 G 大,部署上去动不动 OOM。这一章,我把这几年踩过的坑和总结的经验,一次性倒给你。
核心要点:容器化部署不是「把代码塞进镜像就完事」。Dockerfile 的每一行、镜像的每一层、容器的每一个参数,都藏着性能和安全的关键。
3.1 Dockerfile 优化:从「能用」到「高效」
先看一个反面教材。我接手过一个项目,Dockerfile 长这样:
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y python3 python3-pip
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
CMD ["python3", "app.py"]
有什么问题?问题大了去了。
- 基础镜像太肥:
ubuntu:latest几百兆,换成python:3.9-slim能瘦到 100M 左右。 - 层数过多:每个
RUN都产生一层。我建议把apt-get和pip合并到一层。 - 没有清理缓存:
apt-get install后的/var/cache/apt还在镜像里,白白占用空间。 - COPY 时机不对:把整个项目复制进去,再装依赖。你想想看,只要代码改一行,整个
pip install缓存就失效了。
优化后的版本:
FROM python:3.9-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY . .
CMD ["python3", "app.py"]
这里用了多阶段构建。第一阶段只装依赖,第二阶段只复制产物。最终镜像小,而且依赖层不会因为代码变动而失效。
我的习惯:Dockerfile 里,把「不常变的内容」放在前面(比如系统包、Python 依赖),把「经常变的内容」放在后面(比如业务代码)。这样能最大化利用构建缓存。
3.2 镜像构建缓存:别让缓存成为摆设
Docker 构建镜像时,每一层都会缓存。如果某层没变,就直接用缓存。但很多人没搞明白缓存的失效规则。
举个例子:
COPY package.json /app/
RUN npm install
COPY . /app/
只要 package.json 没变,npm install 这层就会命中缓存。但如果把 COPY . /app/ 写在 npm install 前面,那每次代码变动都会导致 npm install 重新执行——哪怕依赖没变。
为什么会这样?因为 Docker 检查的是「输入文件的内容」。你把整个目录复制进去,任何文件改动都会让这层缓存失效,后续所有层都得重跑。
我曾经踩过的坑:有一次构建镜像花了 15 分钟,排查发现是 COPY . /app/ 放在了前面,导致每次构建都重新下载所有 npm 包。调整顺序后,构建时间降到了 30 秒。
缓存优化的几个原则:
- 先复制依赖描述文件(
package.json、requirements.txt、go.mod等),再装依赖。 - 后复制业务代码。
- 利用
.dockerignore排除不需要的文件(node_modules、.git、__pycache__等),减少缓存失效概率。 - 多阶段构建:把编译环境和运行环境分开,编译阶段的缓存失效不影响最终镜像。
3.3 容器资源限制:别让一个容器拖垮整个宿主机
默认情况下,容器可以占用宿主机的全部资源。你想想看,如果某个容器内存泄漏,或者某个进程突然吃满 CPU,其他容器怎么办?
我见过一个生产事故:一个 Java 应用没设置内存限制,GC 时把宿主机内存吃光,导致同一台机器上的其他服务全部 OOM 被 kill。从那以后,我要求所有容器必须设置资源限制。
Docker 的资源限制参数:
| 参数 | 作用 | 示例 |
|---|---|---|
--memory |
限制最大内存 | --memory=512m |
--memory-swap |
限制内存+交换分区总和 | --memory-swap=1g |
--cpus |
限制 CPU 核心数 | --cpus=1.5 |
--cpu-shares |
设置 CPU 权重(相对值) | --cpu-shares=512 |
--pids-limit |
限制进程数 | --pids-limit=100 |
我建议:生产环境至少设置 --memory 和 --cpus。Java 应用还要额外设置 JVM 参数(-Xmx),让 JVM 知道容器的内存上限,避免 JVM 申请超过容器限制的内存。
举个例子,启动一个 Java 容器:
docker run -d \
--name my-java-app \
--memory=512m \
--memory-swap=512m \
--cpus=1 \
-e JAVA_OPTS="-Xmx400m -Xms400m" \
my-java-image
这里 --memory-swap=512m 表示禁用交换分区(因为内存和交换总和等于内存上限)。为什么禁用交换?因为容器 OOM 时,Docker 会直接 kill 进程,而不是让它在交换分区里苟延残喘——那样性能更差。
3.4 数据持久化陷阱:容器重启后数据去哪了?
容器是短暂的。一旦删除,容器内的所有数据都会消失。这是新手最容易犯的错误。
我遇到过好几次:开发人员在容器里跑数据库,容器重启后数据全没了,然后跑来问我「为什么数据丢了?」——嗯,因为数据写在容器层,容器没了,数据自然也没了。
解决方案:使用 Volume 或 Bind Mount。
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Volume | 由 Docker 管理,存储在 /var/lib/docker/volumes/ |
数据库、配置文件等需要持久化的数据 |
| Bind Mount | 直接映射宿主机目录 | 开发环境热更新、日志收集 |
| tmpfs Mount | 存储在内存中 | 敏感数据(如密码文件) |
使用 Volume 的示例:
docker run -d \
--name mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
mysql:8.0
这样,即使容器被删除,数据还在 Volume 里。下次启动时挂载同一个 Volume,数据就回来了。
注意:Bind Mount 在 macOS 和 Windows 上性能较差(因为文件共享机制)。生产环境优先使用 Volume。另外,不要把宿主机的重要目录(如 /etc、/var)直接挂载给容器,安全风险太大。
3.5 知识体系总览
下面这张图,把容器化部署的核心知识点串起来了:
这张图把四个核心知识点串起来了。你从「容器化部署」出发,沿着箭头走,每个分支都有对应的子知识点。我个人习惯在写 Dockerfile 时,脑子里先过一遍这张图——镜像怎么优化、缓存怎么利用、资源怎么限制、数据怎么持久化。想清楚了再动手,能省不少事。
最后说一句:容器化不是银弹。它解决了环境一致性问题,但也带来了新的复杂度。把 Dockerfile 写好、资源限制设好、数据持久化做好,你的容器化之路才能走得稳。