容器化基础:Docker核心概念、Dockerfile编写、镜像构建与优化、容器生命周期管理
好,咱们进入第二章。容器化,说白了就是给应用打包,让它能在任何地方跑起来。Docker 就是目前最流行的打包工具。我刚开始接触 Docker 时,觉得它就是个轻量级虚拟机。后来踩了坑才明白——它跟虚拟机完全是两码事。
Docker 核心概念:镜像、容器、仓库
这三个概念,你必须要刻在脑子里。
- 镜像(Image):一个只读的模板。包含运行应用所需的一切——代码、运行时、库、环境变量、配置文件。你可以把它理解成「类」。
- 容器(Container):镜像的运行实例。它是可写的,有生命周期。你可以把它理解成「对象」。
- 仓库(Registry):存放镜像的地方。Docker Hub 是公共仓库,企业一般用 Harbor 或阿里云镜像仓库。
一句话总结:镜像就是菜谱,容器就是炒出来的菜,仓库就是菜谱集。
我个人习惯把镜像和容器的关系比作「程序与进程」。镜像躺在磁盘上,容器跑在内存里。你想想看,一个镜像可以启动多个容器,每个容器互相隔离,互不影响。
Dockerfile 编写:从零开始写一份像样的 Dockerfile
Dockerfile 就是构建镜像的说明书。我见过太多人把 Dockerfile 写成「屎山」——一个层叠一个层,镜像体积动辄 1GB 以上。嗯,这里要注意,每一行指令都会生成一个镜像层,层越多,镜像越大,构建越慢。
先看一个标准的 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 .
EXPOSE 3000
CMD ["node", "server.js"]
这里有几个关键点:
- 选择 Alpine 版本:基础镜像越小越好。Alpine 只有 5MB 左右,比 Ubuntu 的 200MB 香多了。
- 利用构建缓存:把变化不频繁的
package.json先复制,再执行npm install。这样只要依赖没变,这一层就会命中缓存,构建速度飞起。 - 多阶段构建:第一阶段装编译工具、下载依赖,第二阶段只复制产物。最终镜像里没有源码、没有构建工具,干净利落。
我的经验:我曾经接手一个项目,Dockerfile 里直接 FROM ubuntu:latest,然后 apt-get install 装了一堆编译工具,镜像体积 2.3GB。改成多阶段构建后,最终镜像只有 180MB。部署速度从 5 分钟降到 30 秒。
镜像构建与优化:让镜像瘦身
镜像优化,说白了就是「删删删」。我总结了几条铁律:
| 优化策略 | 说明 | 效果 |
|---|---|---|
| 选择最小基础镜像 | Alpine、Distroless、Scratch | 减少 50%-90% 体积 |
| 合并 RUN 指令 | 用 && 连接多条命令 |
减少镜像层数 |
| 清理缓存 | apt-get clean、npm cache clean |
减少 10%-30% 体积 |
| 多阶段构建 | 构建阶段与运行阶段分离 | 减少 60%-80% 体积 |
| 使用 .dockerignore | 排除 node_modules、.git 等 | 减少构建上下文大小 |
为什么会这样?因为 Docker 镜像的每一层都是增量存储。你装了一个包,即使后面删了,它仍然存在于上一层中。所以,删除操作必须在同一层完成。
# 错误写法:删除无效
RUN apt-get install -y build-essential
RUN apt-get remove -y build-essential # 这一层删了,但上一层还在
# 正确写法:同一层内安装并清理
RUN apt-get update && \
apt-get install -y build-essential && \
apt-get remove -y build-essential && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
避坑指南:我曾经在构建 Java 镜像时,把 JDK 装在了基础层,后面想换成 JRE 就删不掉了。最终镜像里残留了完整的 JDK,白白多了 200MB。从那以后,我坚持「用完即弃」的原则,所有临时工具都在同一层处理掉。
容器生命周期管理:启动、停止、重启、删除
容器的生命周期,说白了就是「生老病死」。Docker 提供了完整的命令来管理这个过程。
# 启动容器(前台运行)
docker run -d --name my-app -p 8080:3000 my-image:latest
# 查看运行中的容器
docker ps
# 查看所有容器(包括已停止的)
docker ps -a
# 停止容器
docker stop my-app
# 启动已停止的容器
docker start my-app
# 重启容器
docker restart my-app
# 进入容器内部
docker exec -it my-app /bin/sh
# 查看容器日志
docker logs -f my-app
# 删除容器
docker rm my-app
# 强制删除运行中的容器
docker rm -f my-app
这里有几个实用技巧:
- 使用
--restart策略:生产环境建议--restart=always,容器挂了自动重启。 - 资源限制:用
--memory和--cpus限制容器资源,防止一个容器吃光宿主机。 - 数据持久化:用
-v挂载卷,容器删了数据还在。
核心原则:容器是无状态的。所有需要持久化的数据,必须通过卷或绑定挂载到宿主机。我见过有人把数据库跑在容器里,容器一删,数据全没了——嗯,那场面,相当尴尬。
你想想看,容器生命周期管理其实就三个动作:创建、运行、销毁。但真正生产环境中,我们很少手动操作这些命令。后面章节我们会用 Docker Compose 和 Kubernetes 来编排容器,那才是真正的「自动化管理」。
好,这一章就到这里。记住:镜像要小,容器要轻,管理要自动化。下一章我们聊聊 Docker Compose,如何把多个容器串起来跑。