第二章:容器技术基础

好,咱们进入第二章。说实话,容器技术是整个云原生大厦的基石。你想想看,没有容器,微服务怎么跑?CI/CD 怎么玩?Kubernetes 调度什么?所以这一章,我带你从零开始,把 Docker 这套东西彻底搞明白。

2.1 Docker 安装与配置

安装 Docker 其实不复杂,但不同系统有不同坑。我个人习惯在 Linux 上搞,毕竟生产环境基本都是 Linux。

2.1.1 在 Ubuntu 上安装

官方推荐用 apt 仓库安装,别去下载什么 .deb 包手动装,那是在给自己找麻烦。

# 卸载旧版本
sudo apt-get remove docker docker-engine docker.io containerd runc

# 安装依赖
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release

# 添加 Docker 官方 GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 设置稳定版仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装 Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

装完之后,记得把当前用户加到 docker 组里,不然每次都要 sudo,烦得很。

sudo usermod -aG docker $USER
newgrp docker
注意:加组后要重新登录才能生效。我曾经有个同事,加完组没退出重登,折腾了半天以为装坏了。

2.1.2 验证安装

docker --version
docker run hello-world

看到 "Hello from Docker!" 就说明成了。嗯,这里要注意,如果拉镜像慢,后面我会讲怎么配加速器。

2.1.3 Docker 配置优化

我个人习惯装完先改几个配置,不然用起来会有点别扭。

配置镜像加速器——国内你懂的,不配加速器,拉个 nginx 都能等到你怀疑人生。

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://你的加速器地址.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

配置日志轮转——这个坑我踩过。默认情况下,容器日志会无限增长,直到把磁盘撑爆。我在生产环境见过一个容器日志占了 200GB,那叫一个酸爽。

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

2.2 Dockerfile 编写最佳实践

Dockerfile 怎么写?说白了就是告诉 Docker 怎么把你的应用打包成镜像。但怎么写得好,这里面门道不少。

2.2.1 基础镜像选择

别上来就 FROM ubuntu:latest,那玩意 700MB,你部署一次试试?我建议用 Alpine,才 5MB 左右。

# 不推荐
FROM ubuntu:20.04

# 推荐
FROM alpine:3.18

当然,如果你的应用依赖 glibc,Alpine 可能不太行,那就用 slim 版本。

FROM node:18-slim

2.2.2 分层构建与缓存

Docker 构建镜像是一层一层叠上去的。每一行 RUN、COPY 都会产生一个新层。我见过有人把所有命令写在一行,也有人每步都写一行。都不对。

正确的做法是:把不常变动的指令放前面,频繁变动的放后面。这样能充分利用缓存。

# 好例子
FROM node:18-alpine

WORKDIR /app

# 先复制依赖文件
COPY package.json package-lock.json ./
RUN npm install

# 再复制源码
COPY . .

EXPOSE 3000
CMD ["node", "app.js"]

为什么这样写?因为源码经常改,但 package.json 不常改。如果先复制源码,每次改代码都要重新 npm install,那构建速度就慢得离谱了。

2.2.3 多阶段构建

这个技巧我强烈推荐。说白了就是:一个 Dockerfile 里写多个 FROM,前面的用来编译,后面的用来运行。最终镜像只包含运行所需的东西。

# 第一阶段:编译
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 alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]

你看,最终镜像只有 alpine + 一个二进制文件,可能就 20MB。如果不用多阶段构建,光 golang 镜像就 800MB 了。

核心原则:镜像越小,部署越快,攻击面越小。能删的依赖一个不留。

2.2.4 减少层数

虽然 Docker 层数理论上没限制,但层太多会影响性能。我建议把相关的 RUN 命令合并起来。

# 不推荐
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*

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

为什么要删 apt 缓存?因为那些文件只在安装时需要,运行时不依赖。不删的话,它们会留在镜像里,白白增加体积。

2.3 镜像构建与优化

镜像构建不只是 docker build 那么简单。怎么构建得快、怎么让镜像小、怎么保证安全,都是学问。

2.3.1 构建命令

docker build -t myapp:v1.0 -f Dockerfile.prod .

-t 是打标签,-f 指定 Dockerfile 文件名(默认是 Dockerfile),最后的 . 是构建上下文。

这里有个坑:构建上下文不要放无关文件。Docker 会把整个上下文发给守护进程,如果你在项目根目录执行 docker build,而项目里有 node_modules 或者 .git,那构建速度会慢到你想哭。

解决方案:写个 .dockerignore 文件。

node_modules
.git
*.log
.env
dist

2.3.2 镜像优化技巧

优化项 说明 效果
选择小基础镜像 Alpine / slim 替代 full 体积减少 80%
多阶段构建 编译和运行分离 体积减少 90%
合并 RUN 命令 减少层数 构建更快
清理缓存 apt/yum/npm 缓存 体积减少 10-30%
使用 .dockerignore 排除无关文件 构建更快

2.3.3 镜像安全

别用 root 用户运行容器。这是个基本的安全原则。如果容器被攻破,root 权限意味着攻击者可以为所欲为。

FROM node:18-alpine

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

USER appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .

CMD ["node", "app.js"]
小技巧:可以用 docker scan 命令扫描镜像漏洞。我每次构建完都会扫一遍,特别是生产环境用的镜像。

2.4 Docker Compose

单容器好办,但实际项目哪有只跑一个容器的?前端、后端、数据库、缓存、消息队列...一个个手动 docker run 会疯掉的。这时候就需要 Docker Compose 了。

2.4.1 什么是 Docker Compose

说白了,就是用 YAML 文件定义一组容器,然后一条命令全部启动。我最早接触 Compose 时觉得这玩意就是「一键部署」的雏形。

2.4.2 编写 docker-compose.yml

来个实际例子:一个 Node.js 应用 + Redis + MySQL。

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=mysql
      - REDIS_HOST=redis
    depends_on:
      - mysql
      - redis
    volumes:
      - .:/app
      - /app/node_modules

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: myapp
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  mysql_data:

你看,三个服务,一个文件搞定。每个服务可以指定镜像、端口、环境变量、数据卷、依赖关系。

2.4.3 常用命令

# 启动所有服务
docker compose up -d

# 查看日志
docker compose logs -f

# 停止并删除
docker compose down

# 重新构建并启动
docker compose up -d --build

# 查看运行状态
docker compose ps

-d 是后台运行,不加的话日志会直接打印在终端上,调试时挺有用。

2.4.4 网络与数据持久化

Compose 会自动创建一个网络,所有服务可以通过服务名互相访问。比如 app 服务里连接数据库,直接用 mysql:3306 就行,不用写 IP。

数据持久化用 volumes。我上面例子里的 mysql_data 就是命名卷,数据会保存在宿主机上,容器删了数据还在。

注意:生产环境别用 Compose 的 volumes 做数据备份。Compose 适合开发环境,生产环境建议用 Kubernetes 或者云服务商的管理型数据库。

2.4.5 环境变量管理

敏感信息别硬编码在 docker-compose.yml 里。我习惯用 .env 文件。

# .env 文件
DB_PASSWORD=root123
REDIS_PASSWORD=redis123
# docker-compose.yml 中引用
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

这样 .env 文件可以加到 .gitignore 里,避免密码泄露。

小结

这一章内容不少,从 Docker 安装到 Dockerfile 编写,再到镜像优化和 Compose 编排。说实话,这些是容器技术的核心基本功。你把这些搞透了,后面学 Kubernetes 会轻松很多。

下一章我们讲容器编排,也就是 Kubernetes。到时候你会发现,很多概念其实在 Docker 里已经见过了。