容器化基础:Docker核心概念、Dockerfile编写、镜像构建与优化、容器生命周期管理
好,咱们进入第二章。说实话,容器化是整个云原生体系的基石。你想想看,没有容器,微服务跑在哪?CI/CD 怎么搞?环境不一致的问题怎么解决?所以这一章,咱们得把 Docker 吃透。
我个人习惯把 Docker 理解成「轻量级的虚拟机」,但比虚拟机更狠——它共享宿主机内核,启动快,资源占用小。嗯,这里要注意,不是所有场景都适合容器,但绝大多数应用场景,Docker 都是最优解。
2.1 Docker 核心概念:镜像、容器、仓库
这三个概念,说白了就是 Docker 世界的「三驾马车」。我刚开始学的时候,总把镜像和容器搞混。后来一个老大哥跟我说了一句话,我记到现在:镜像就是类,容器就是实例。
- 镜像(Image):一个只读的模板,包含运行应用所需的一切——代码、运行时、库、环境变量、配置文件。你可以把它想象成一个「快照」。
- 容器(Container):镜像的运行实例。你可以启动、停止、删除、暂停它。每个容器都是隔离的,有自己的文件系统、网络、进程空间。
- 仓库(Registry):存放镜像的地方。Docker Hub 是官方的公共仓库,企业一般用 Harbor 或阿里云镜像仓库。
核心关系图(脑补一下):
Dockerfile → 构建 → 镜像 → 推送 → 仓库 → 拉取 → 运行 → 容器
我在项目中遇到过一个问题:团队里有人直接在容器里改配置,然后 commit 成新镜像。这种做法其实很不推荐。镜像应该是不可变的,每次变更都应该通过 Dockerfile 重新构建。
2.2 Dockerfile 编写:从入门到精通
Dockerfile 就是构建镜像的「配方」。写得好,镜像小、构建快、安全;写得烂,镜像几百兆、构建慢、漏洞多。咱们直接看一个实战例子。
2.2.1 一个标准的 Node.js 应用 Dockerfile
# 1. 选择基础镜像
FROM node:18-alpine AS builder
# 2. 设置工作目录
WORKDIR /app
# 3. 先复制依赖文件,利用缓存
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 4. 复制源码
COPY . .
# 5. 多阶段构建:生产镜像
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
# 6. 暴露端口
EXPOSE 3000
# 7. 启动命令
CMD ["node", "dist/index.js"]
为什么这么写?我跟你讲讲我的经验。
- 选择 Alpine 版本:基础镜像从 100MB+ 降到 5MB 左右。我在一个微服务项目里,把所有镜像从 Ubuntu 换成 Alpine,整体存储节省了 70%。
- 先复制 package.json,再 RUN npm install:这是利用 Docker 的层缓存机制。只要 package.json 没变,这一层就不会重新构建,节省大量时间。
- 多阶段构建:builder 阶段装了一堆构建工具,最终镜像只保留运行所需的最小文件。我曾经见过一个 Java 项目,构建镜像 1.2GB,用多阶段构建后只有 180MB。
小技巧:写 Dockerfile 时,尽量把「不常变」的指令放在前面,「常变」的放在后面。这样能最大化利用缓存。
2.2.2 Dockerfile 最佳实践清单
| 实践 | 说明 | 为什么重要 |
|---|---|---|
| 使用 .dockerignore | 排除 node_modules、.git 等 | 减少构建上下文大小,提升构建速度 |
| 尽量使用官方镜像 | 如 node:18-alpine | 安全、稳定、有维护 |
| 不要安装不必要的包 | 只装生产依赖 | 减小镜像体积,减少攻击面 |
| 使用非 root 用户 | RUN addgroup -g 1001 app && adduser -u 1001 -G app app | 提升安全性,防止容器逃逸 |
| 合并 RUN 指令 | 用 && 连接多个命令 | 减少镜像层数 |
避坑指南:我曾经在项目里没写 .dockerignore,结果把整个 node_modules(几百兆)都打进了构建上下文。每次构建都慢得要死,后来排查才发现这个问题。记住:构建上下文越小越好。
2.3 镜像构建与优化
构建镜像很简单,docker build -t myapp:1.0 . 就完事了。但优化镜像,才是真正体现功力的地方。
2.3.1 镜像体积优化
为什么要在乎镜像体积?你想想看,一个 2GB 的镜像,从仓库拉下来要多久?在生产环境,几十个节点同时拉取,网络带宽直接被打满。我见过一个事故,就是因为镜像太大,滚动更新时拉取超时,导致服务中断了 10 分钟。
优化手段主要有这几个:
- 选择最小基础镜像:Alpine、Distroless、Scratch
- 多阶段构建:构建阶段和运行阶段分离
- 清理缓存和临时文件:
RUN apt-get clean && rm -rf /var/lib/apt/lists/* - 使用 --no-install-recommends:安装包时不装推荐依赖
# 一个优化后的 Java 镜像示例
FROM eclipse-temurin:17-jre-alpine AS builder
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
这个例子用了 Spring Boot 的分层提取,把依赖和业务代码分开。这样业务代码变了,只需要重新构建最后一层,前面的层都可以复用缓存。
2.3.2 镜像安全优化
安全这事儿,说大不大,说小不小。但一旦出事,就是大事。
- 定期扫描镜像漏洞:用 Trivy、Clair 等工具
- 不要存储敏感信息:密码、密钥用环境变量或 Secret 管理
- 使用非 root 用户运行:前面提过,这里再强调一遍
- 锁定基础镜像版本:不要用
FROM node:latest,要用具体版本号
我的经验:在一个金融项目中,我们要求所有镜像必须通过 Trivy 扫描,高危漏洞数为 0 才能上线。刚开始很痛苦,但坚持下来后,生产环境再也没有因为镜像漏洞出过问题。
2.4 容器生命周期管理
容器的生命周期,说白了就是「生老病死」。咱们来看看怎么管理。
2.4.1 核心命令速查
| 操作 | 命令 | 说明 |
|---|---|---|
| 创建并启动 | docker run -d --name myapp myapp:1.0 | -d 后台运行 |
| 查看运行中容器 | docker ps | 加 -a 查看所有 |
| 停止容器 | docker stop myapp | 发送 SIGTERM,等待优雅退出 |
| 强制停止 | docker kill myapp | 发送 SIGKILL,直接干掉 |
| 重启容器 | docker restart myapp | 先 stop 再 start |
| 进入容器 | docker exec -it myapp /bin/sh | 调试用,生产环境慎用 |
| 查看日志 | docker logs -f myapp | -f 实时跟踪 |
| 删除容器 | docker rm myapp | 加 -f 强制删除运行中的 |
2.4.2 优雅关闭与健康检查
这里有个坑,我踩过好几次。容器停止时,Docker 会发 SIGTERM 信号,然后等 10 秒(默认),如果进程还没退出,就发 SIGKILL。如果你的应用没有正确处理 SIGTERM,就会导致请求被中断、数据丢失。
正确的做法是:
# 在应用里捕获 SIGTERM 信号
process.on('SIGTERM', () => {
console.log('收到 SIGTERM,开始优雅关闭...');
server.close(() => {
console.log('所有连接已关闭');
process.exit(0);
});
});
另外,健康检查也很重要。Docker 支持三种健康检查方式:
- HEALTHCHECK 指令:在 Dockerfile 里定义
- docker run 的 --health-cmd:启动时指定
- docker-compose 的 healthcheck:编排时定义
# Dockerfile 中的健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
建议:每个容器都应该有健康检查。我在生产环境见过因为容器「假死」(进程还在但服务不可用)导致的问题,健康检查能及时发现并重启容器。
2.4.3 资源限制
容器默认是不限制资源的。你想想看,如果一个容器把 CPU 跑满,其他容器怎么办?所以一定要设置资源限制。
# 限制内存和 CPU
docker run -d \
--name myapp \
--memory="512m" \
--memory-swap="1g" \
--cpus="0.5" \
myapp:1.0
参数说明:
--memory:硬限制,超过这个值容器会被 OOM Kill--memory-swap:内存+交换分区总和,一般设为 memory 的两倍--cpus:限制 CPU 使用率,0.5 表示最多用半个核
避坑指南:我曾经在一个 Java 应用上没设置内存限制,结果 JVM 的堆内存默认是物理内存的 1/4,在 64GB 的机器上直接分配了 16GB,导致其他容器 OOM。后来加了 -XX:MaxRAMPercentage=50.0 和 Docker 的内存限制才解决。
2.5 实战:从零构建一个生产级镜像
好,理论说完了,咱们来点实战。假设你有一个 Python Flask 应用,咱们一步步构建一个生产级镜像。
# 1. 基础镜像
FROM python:3.11-slim AS builder
# 2. 设置工作目录
WORKDIR /app
# 3. 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 4. 复制源码
COPY . .
# 5. 多阶段构建:生产镜像
FROM python:3.11-slim
WORKDIR /app
# 6. 创建非 root 用户
RUN addgroup --system app && adduser --system --ingroup app app
# 7. 复制依赖和源码
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /app /app
# 8. 切换用户
USER app
# 9. 暴露端口
EXPOSE 5000
# 10. 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1
# 11. 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
这个 Dockerfile 涵盖了咱们今天讲的所有要点:
- ✅ 最小基础镜像
- ✅ 多阶段构建
- ✅ 非 root 用户
- ✅ 健康检查
- ✅ 依赖缓存优化
构建并运行:
# 构建镜像
docker build -t flask-app:1.0 .
# 运行容器
docker run -d \
--name flask-app \
--memory="256m" \
--cpus="0.25" \
-p 5000:5000 \
flask-app:1.0
# 查看日志
docker logs -f flask-app
# 测试健康检查
docker inspect flask-app | grep -A 10 "Health"
嗯,到这里,容器化基础的内容就差不多了。你可能会问:「这些我都会了,但怎么跟 CI/CD 结合起来?」别急,下一章咱们就讲 CI/CD 流水线,到时候这些 Docker 知识全都能用上。
记住一句话:镜像要小,构建要快,运行要稳,安全要严。这十六个字,是我做容器化这么多年总结出来的心法。