2. 容器基础回顾:Docker核心概念、镜像与容器、Dockerfile最佳实践
好,咱们正式开始容器编排之前,我得先带大家把Docker的老底儿翻一翻。你可能会想:“Docker嘛,谁不会?” 但说实话,很多人在生产环境里翻车,恰恰是因为基础概念没吃透。这一章,咱们就聊聊那些“你以为你懂,其实未必”的核心点。
2.1 Docker核心概念:镜像、容器、仓库
这三个词,是Docker世界的基石。我见过不少新手,把镜像和容器混着用,结果排查问题的时候一脸懵。
- 镜像(Image):一个只读的模板。它包含了运行应用所需的一切——代码、运行时、库、环境变量、配置文件。说白了,镜像就是一个“打包好的操作系统快照”。
- 容器(Container):镜像的运行实例。你可以把它理解成一个“轻量级的虚拟机”,但它共享宿主机的内核。容器是可写的,你可以在里面改文件、装软件,但一旦删除,所有改动就没了。
- 仓库(Registry):存放镜像的地方。Docker Hub是公共的,但企业里更多用私有的,比如Harbor。
核心区别一句话:镜像是“菜谱”,容器是“炒出来的菜”。菜谱可以反复用,菜吃完就没了(除非你commit成新镜像)。
我个人习惯把镜像比作“类”,容器比作“对象”。面向对象编程里,类定义属性方法,对象是具体的实例。这么一想,是不是清晰多了?
2.2 镜像与容器的生命周期管理
咱们来点实际的。怎么拉镜像、跑容器、看日志、进容器?这些命令你得烂熟于心。
2.2.1 镜像操作
# 拉取镜像
docker pull nginx:1.21
# 查看本地镜像
docker images
# 删除镜像
docker rmi nginx:1.21
# 构建镜像(后面细讲)
docker build -t my-app:v1 .
嗯,这里要注意:docker rmi 之前,得确保没有容器在用这个镜像。否则会报错。我曾经在CI/CD流水线里忘了这茬,导致构建失败,排查了半天才发现是镜像被容器占用了。
2.2.2 容器操作
# 运行容器(前台)
docker run -it --name my-nginx nginx:1.21 /bin/bash
# 运行容器(后台)
docker run -d --name my-nginx -p 8080:80 nginx:1.21
# 查看运行中的容器
docker ps
# 查看所有容器(包括已停止的)
docker ps -a
# 进入容器
docker exec -it my-nginx /bin/bash
# 查看日志
docker logs -f my-nginx
# 停止容器
docker stop my-nginx
# 启动已停止的容器
docker start my-nginx
# 删除容器
docker rm my-nginx
为什么要有 -it 和 -d 的区别?-it 是交互式终端,适合调试;-d 是后台守护进程,适合生产。你想想看,生产环境里谁会一直盯着终端看?
小技巧:用 docker run --rm 可以在容器停止后自动删除。适合临时测试,省得手动清理一堆废弃容器。
2.3 Dockerfile最佳实践
Dockerfile是构建镜像的“配方”。写得好,镜像小、构建快、安全;写得烂,镜像臃肿、漏洞多。我见过最夸张的Dockerfile,一个镜像2个G,里面全是没用的依赖。
2.3.1 基础镜像选择
别一上来就用 ubuntu:latest。那玩意儿太大了。我建议用 alpine 或者 slim 版本。
| 基础镜像 | 大小 | 适用场景 |
|---|---|---|
| ubuntu:22.04 | ~77MB | 需要完整包管理器的场景 |
| debian:bullseye-slim | ~80MB | 兼容性好,比ubuntu小一点 |
| alpine:3.16 | ~5MB | 极致精简,但注意glibc兼容性 |
| scratch | 0MB | 纯静态编译的Go/C应用 |
我个人习惯:Go应用用 scratch 或 alpine,Java应用用 eclipse-temurin:17-jre-alpine,Python应用用 python:3.11-slim。
2.3.2 分层构建与缓存利用
Dockerfile的每一行指令都会生成一个层。层是只读的,构建时会缓存。所以,把不常变动的指令放前面,频繁变动的放后面。
# 不好的写法
FROM node:18-alpine
COPY . /app
RUN npm install
RUN npm run build
# 好的写法
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
为什么好?因为 package.json 不常变,而源码经常改。这样 npm install 那层就能复用缓存,构建速度能快好几倍。我曾经在一个微服务项目里,用这个技巧把构建时间从5分钟降到了30秒。
2.3.3 多阶段构建
上面那个例子其实就是多阶段构建。第一阶段(builder)装编译工具、下载依赖、编译代码;第二阶段(最终镜像)只拷贝编译产物。这样最终镜像里没有源码、没有编译工具,又小又安全。
# Go应用的多阶段构建
FROM golang:1.20 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
FROM scratch
COPY --from=builder /app/myapp /myapp
EXPOSE 8080
CMD ["/myapp"]
你看,最终镜像只有 myapp 这个二进制文件,连shell都没有。攻击者想进容器搞破坏?门儿都没有。
2.3.4 安全与权限
默认情况下,容器以root用户运行。这很危险。如果容器被攻破,攻击者就有宿主机root权限(虽然有一些限制,但风险依然很大)。
# 创建非root用户
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
CMD ["node", "app.js"]
警告:千万别在生产容器里用root!我曾经审计过一个客户的镜像,里面居然有SSH服务,还允许root密码登录。这简直是给黑客留后门。
2.3.5 减少层数与清理缓存
每个 RUN 指令都会生成一层。尽量合并命令,减少层数。同时,记得清理包管理器的缓存。
# 不好的写法
RUN apt-get update
RUN apt-get install -y curl vim
RUN apt-get clean
# 好的写法
RUN apt-get update && \
apt-get install -y curl vim && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
为什么要 rm -rf /var/lib/apt/lists/*?因为 apt-get update 下载的包列表缓存,在运行时根本用不到。留着只会让镜像变大。
2.4 避坑指南:我踩过的那些坑
最后,分享几个我亲身经历的血泪教训。
- 坑一: 用
latest标签。有一次我依赖的第三方镜像更新了latest,结果API不兼容,线上直接挂了。从那以后,我所有镜像都指定具体版本号,比如nginx:1.21.6。 - 坑二: 忽略
.dockerignore。构建上下文里如果有node_modules或.git,会被一起打包发送给Docker daemon,构建慢得要死。加个.dockerignore文件,排除无用文件。 - 坑三: 容器里写日志到本地文件。容器一旦重启,日志就丢了。正确做法是输出到标准输出(stdout/stderr),然后用日志收集工具(如Fluentd、Filebeat)统一采集。
好了,这一章的内容就这些。Docker基础看似简单,但细节决定成败。下一章,咱们聊聊Kubernetes的核心架构,那才是真正的重头戏。