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应用用 scratchalpine,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的核心架构,那才是真正的重头戏。