2、容器化基础:Docker核心概念、镜像与容器、Dockerfile编写最佳实践

好,咱们进入正题。容器化,说白了就是给应用搞一个「随身携带的运行环境」。我最早接触 Docker 是在一个金融项目里,那时候部署环境五花八门,开发说「我这儿跑得好好的」,运维说「我这儿就是起不来」——嗯,这种扯皮事儿,干过的都懂。

Docker 的出现,把「环境一致性」这个老大难问题,直接给抹平了。咱们今天就把 Docker 的核心概念、镜像与容器的关系,以及 Dockerfile 怎么写才专业,一次性讲透。

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

这三个词,你肯定听过。但很多人用了一年,其实没搞明白它们的本质区别。

  • 镜像(Image):一个只读的模板。里面包含了应用运行所需的一切——代码、运行时、系统工具、库、配置。说白了,就是一个「快照」。
  • 容器(Container):镜像跑起来之后的实例。你可以把它理解成一个「轻量级的虚拟机」,但它共享宿主机内核,启动是毫秒级的。
  • 仓库(Registry):存镜像的地方。Docker Hub 是公共的,但金融场景下,我们一般用私有仓库,比如 Harbor。

一句话总结:镜像就是「菜谱」,容器就是「做出来的菜」。菜谱可以反复用,菜吃完了可以再炒一份。

我个人习惯把镜像比作「类」,容器比作「对象」。面向对象编程的朋友,一听就懂。

2.2 镜像与容器的关系:分层存储与写时复制

这里有个关键点,很多人栽过跟头——镜像的分层结构。

Docker 镜像是由一层层只读层叠加起来的。每一层代表一个 Dockerfile 指令。比如 FROM 是一层,RUN apt-get install 又是一层。

容器启动时,Docker 会在镜像层之上,加一个可写层(容器层)。所有对文件的修改,都发生在这个可写层里。

避坑指南:我曾经遇到一个同事,在容器里改了配置文件,然后直接 docker commit 生成新镜像。结果镜像体积暴涨,因为他把整个可写层都打包进去了。正确的做法是改 Dockerfile,重新构建。

为什么会这样?因为「写时复制」机制。容器层只记录差异,不复制整个镜像。所以多个容器可以共享同一个镜像层,节省磁盘和内存。

下面这张图,帮你理清镜像和容器的关系:

镜像与容器分层结构 容器可写层(容器层) 镜像层 3:应用代码与配置 镜像层 2:运行时环境(JDK、Node等) 镜像层 1:基础操作系统(Alpine、Ubuntu) 镜像只读,容器可写 多个容器可共享同一镜像层,节省存储空间 容器删除后,可写层数据丢失(除非挂载卷) 容器 A 容器 B 容器 C

2.3 Dockerfile 编写最佳实践

Dockerfile 怎么写,直接决定了你的镜像质量。我见过太多「能用就行」的 Dockerfile,结果镜像几个 G,构建还慢。下面是我总结的几个核心原则。

2.3.1 选择合适的基础镜像

别上来就 FROM ubuntu:latest。金融场景下,我推荐用 Alpine 或者 distroless 镜像。

基础镜像 大小 适用场景
ubuntu:22.04 ~77 MB 需要完整工具链的调试环境
alpine:3.18 ~5 MB 生产环境,追求小体积
gcr.io/distroless/java17 ~120 MB Java 应用,只包含运行时

注意:Alpine 用的 musl libc,不是 glibc。如果你的应用依赖 glibc 特性(比如某些 Java 或 C++ 程序),可能会出问题。我踩过这个坑,后来老老实实换成了 distroless。

2.3.2 减少镜像层数

每一条 RUNCOPYADD 都会产生一个新层。层数太多,镜像体积大,构建也慢。

我习惯把多个命令用 && 连起来,放在一个 RUN 里:

# 不推荐
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# 推荐
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

2.3.3 利用构建缓存

Docker 构建时,会逐层检查缓存。如果某层没变,就直接用缓存。所以,把「不常变」的指令放前面,「常变」的放后面。

# 先安装依赖(不常变)
COPY package.json package-lock.json ./
RUN npm install

# 再复制源码(常变)
COPY . .

你想想看,如果反过来,每次改一行代码都要重新 npm install,那构建时间就太长了。

2.3.4 多阶段构建

这是我最喜欢的功能。一个 Dockerfile 里可以写多个 FROM,前面的阶段用来编译,后面的阶段只取编译产物。

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

# 第二阶段:运行
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]

最终镜像只有几 MB,没有 Go 编译器,没有源码,只有可执行文件。安全又轻量。

个人经验:在金融项目中,我们要求所有生产镜像必须使用多阶段构建。一来减少攻击面,二来镜像小了,部署也快。曾经有个镜像从 1.2GB 降到了 180MB,运维同事直呼「舒服」。

2.3.5 不要以 root 运行

默认容器内是 root 用户,这很危险。万一被攻破,攻击者就有宿主机 root 权限(虽然 Docker 有 namespace 隔离,但总有漏洞)。

RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser

嗯,这里要注意,有些基础镜像没有 useradd 命令,比如 Alpine 用 adduser。写 Dockerfile 前,先看看基础镜像的文档。

2.4 常用 Docker 命令速查

最后,列几个我每天都会用的命令。新手别死记硬背,多用几次就熟了。

命令 说明
docker build -t myapp:1.0 . 构建镜像
docker run -d -p 8080:80 myapp:1.0 运行容器(后台模式)
docker exec -it container_id sh 进入容器内部
docker logs -f container_id 查看容器日志
docker system prune -a 清理所有未使用的镜像和容器

好了,这一章的内容就到这儿。Docker 的核心概念和 Dockerfile 编写,说白了就是「镜像要小、构建要快、运行要安全」。你把这些原则记牢了,后面讲容器编排的时候,会轻松很多。


公众号:蓝海资料掘金营,微信deep3321