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,重新构建。
为什么会这样?因为「写时复制」机制。容器层只记录差异,不复制整个镜像。所以多个容器可以共享同一个镜像层,节省磁盘和内存。
下面这张图,帮你理清镜像和容器的关系:
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 减少镜像层数
每一条 RUN、COPY、ADD 都会产生一个新层。层数太多,镜像体积大,构建也慢。
我习惯把多个命令用 && 连起来,放在一个 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