3、容器化基础:Docker核心概念、Dockerfile编写技巧、镜像仓库管理、容器网络与存储

聊到PaaS平台,容器化是绕不开的基石。说白了,没有Docker,就没有今天这么繁荣的云原生生态。我最早接触Docker还是1.9版本,那时候很多人觉得它就是个轻量级虚拟机。嗯,这个误解到现在还有人信。

这一章,我们就把Docker的核心概念、Dockerfile怎么写、镜像怎么管、网络和存储怎么配,一次性讲透。都是我在生产环境里摸爬滚打出来的经验。

3.1 Docker核心概念:镜像、容器、仓库

这三个概念,你闭着眼睛都得能说清楚。

  • 镜像(Image):一个只读的模板,包含了运行应用所需的一切——代码、运行时、库、环境变量、配置文件。你可以把它理解成Java里的类。
  • 容器(Container):镜像的运行实例。它是可写的,有生命周期。相当于Java里new出来的对象。
  • 仓库(Registry):存放镜像的地方。Docker Hub是官方的,但企业里我们一般用Harbor或自建的。

重要:镜像分层是Docker的精髓。每一层都是只读的,容器层是唯一可写层。这种设计让镜像复用、传输、存储都变得极其高效。

我在项目中遇到过一个问题:有同事把容器当虚拟机用,进去装了一堆东西,然后commit成一个新镜像。结果镜像体积从200MB膨胀到2GB。这种做法,说白了就是反模式。

3.2 Dockerfile编写技巧

Dockerfile写得好不好,直接决定了镜像的质量。我见过太多人把Dockerfile写成了一锅粥。下面是我总结的几个核心技巧。

3.2.1 基础镜像选择

别一上来就FROM ubuntu:latest。你想想看,一个Java应用,用openjdk:11-jre-slim就够了,何必带一堆用不上的工具?

基础镜像 体积 适用场景
ubuntu:20.04 ~200MB 需要完整系统工具链
alpine:3.15 ~5MB 追求极致体积,但注意glibc兼容性
openjdk:11-jre-slim ~200MB Java运行时,去掉了编译工具
golang:1.18-alpine ~300MB Go编译环境,多阶段构建常用

我的习惯:能用slim不用full,能用alpine不用ubuntu。但要注意,alpine用的是musl libc,有些依赖glibc的程序会出问题。我曾经被这个坑过,一个Python的C扩展在alpine上编译了半天。

3.2.2 指令顺序与层缓存

Docker构建时会缓存每一层。如果某层没变,就直接用缓存。所以,把变化最少的指令放在前面

# 不推荐:先COPY代码,再装依赖
FROM node:14-alpine
COPY . /app
RUN npm install

# 推荐:先装依赖,再COPY代码
FROM node:14-alpine
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app

为什么?因为代码经常改,但package.json不常改。这样改代码时,npm install那层缓存还能用,构建速度能快好几倍。

3.2.3 多阶段构建

这是我最喜欢的功能。你想想看,编译环境需要一堆工具,但运行时只需要编译好的二进制。多阶段构建完美解决了这个问题。

# 第一阶段:编译
FROM golang:1.18-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 第二阶段:运行
FROM alpine:3.15
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]

最终镜像只有几MB,而不是几百MB。我在做微服务PaaS平台时,所有服务都用这种方式构建,镜像仓库的存储压力小了很多。

3.3 镜像仓库管理

镜像仓库是PaaS平台的“粮仓”。管理不好,后面全是坑。

3.3.1 仓库选型

  • Docker Hub:公共仓库,适合开源项目。但拉取有速率限制,企业用不现实。
  • Harbor:企业级首选。支持RBAC、镜像复制、漏洞扫描、垃圾回收。我们PaaS平台用的就是它。
  • Nexus:如果团队已经用了Nexus做Maven仓库,可以顺便管镜像。

3.3.2 镜像命名与标签规范

没有规范,仓库就是垃圾场。我建议这样命名:

# 格式:仓库地址/项目名/服务名:版本号-环境
registry.paas.com/order-service/order-api:1.2.3-prod
registry.paas.com/user-service/user-svc:2.0.1-staging

注意:永远不要用latest标签。你根本不知道它指向哪个版本。我曾经排查过一个线上问题,发现两个节点的latest镜像不一样,因为构建时间差了10分钟。从那以后,我强制团队必须用语义化版本号。

3.3.3 垃圾回收与清理

镜像越积越多,磁盘迟早会爆。Harbor支持自动垃圾回收,但需要手动触发或定时任务。我一般这样配:

  • 保留最近10个版本的镜像
  • 超过30天的旧版本自动删除
  • 每周日凌晨执行一次垃圾回收

3.4 容器网络

网络是容器化里最容易出问题的地方。Docker提供了几种网络模式,各有各的用途。

3.4.1 网络模式对比

模式 特点 适用场景
bridge 默认模式,容器通过NAT访问外网 单机多容器通信
host 容器直接使用宿主机网络栈 性能敏感型应用,如Nginx
none 无网络,只有lo接口 安全隔离场景
overlay 跨主机容器通信,需要Swarm或K8s 多机集群

我个人习惯,单机调试用bridge,生产环境用overlay(配合K8s的CNI插件)。host模式虽然性能好,但端口冲突是个大问题。

3.4.2 自定义网络

默认的bridge网络有个问题:容器之间只能通过IP通信,不能通过服务名。自定义bridge网络可以解决这个问题。

# 创建自定义网络
docker network create --driver bridge my-net

# 启动容器时指定网络
docker run -d --name web --network my-net nginx
docker run -d --name app --network my-net myapp

# 现在app容器里可以直接 ping web

这个特性在微服务架构里特别有用。服务发现都不用额外组件,Docker自带的DNS就能搞定。

3.5 容器存储

容器是短暂的,但数据需要持久化。Docker提供了三种存储方式。

3.5.1 存储方式对比

方式 特点 适用场景
Volume 由Docker管理,存在宿主机特定目录 数据库、配置文件等需要持久化的数据
Bind Mount 直接映射宿主机目录到容器 开发环境热更新、日志收集
tmpfs 存在内存中,容器停止后消失 敏感数据、临时缓存

3.5.2 Volume的使用

生产环境我强烈推荐用Volume。它比Bind Mount更安全,也更方便迁移。

# 创建Volume
docker volume create mysql-data

# 使用Volume
docker run -d \
  --name mysql \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=root \
  mysql:8.0

避坑指南:我曾经把MySQL的数据目录用Bind Mount挂载到宿主机的一个普通目录。结果宿主机磁盘满了,MySQL直接崩溃。后来改用Volume,配合定期备份,再也没出过问题。Volume的存储位置由Docker管理,你不需要关心它在哪,只要知道它叫mysql-data就行。

3.5.3 存储驱动选择

Docker支持多种存储驱动,比如overlay2、aufs、devicemapper。现在主流是overlay2,性能好,稳定性高。

如果你还在用aufs或devicemapper,我建议尽快迁移到overlay2。我在一次性能测试中发现,overlay2的IOPS比devicemapper高了30%以上。

好了,这一章的内容就到这里。Docker的基础打牢了,后面讲K8s和PaaS平台编排的时候,你会觉得轻松很多。下一章我们聊聊容器编排的进阶话题。