引言
"在我机器上能跑啊"——这句话大概是软件开发史上最经典的甩锅语录。Docker 的出现,从根本上解决了"环境不一致"这个老大难问题。从开发环境到测试环境再到生产环境,Docker 确保你的应用在任何地方都以完全相同的方式运行。
本文将从实际开发者的视角,系统讲解 Docker 的核心概念、Dockerfile 编写技巧、镜像优化、Docker Compose 多容器编排,以及生产部署的最佳实践。
一、Docker 核心概念速览
在开始之前,我们先理清几个最核心的概念:
- 镜像(Image):一个只读的模板,包含了运行应用所需的所有文件、依赖和配置。你可以把它理解为"安装包"。
- 容器(Container):镜像的运行实例。一个镜像可以启动多个容器,就像一个安装包可以安装到多台电脑上。
- Dockerfile:用于构建镜像的脚本文件,定义镜像的内容和构建步骤。
- Docker Compose:用于定义和运行多容器应用的工具,通过一个 YAML 文件配置所有服务。
- Registry:镜像仓库,用于存储和分发镜像。Docker Hub 是最常用的公共 Registry。
二、编写高效的 Dockerfile
一个典型的 Node.js 应用 Dockerfile
# 基础镜像
FROM node:20-alpine AS base
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源码
COPY . .
# 构建应用
RUN npm run build
# 暴露端口
EXPOSE 3000
# 启动命令
CMD ["node", "dist/main.js"]
利用多阶段构建优化镜像体积
多阶段构建(Multi-stage Build)是 Docker 镜像优化的核心技巧。它的思路是:在第一个阶段安装所有构建工具、编译代码,在第二个阶段只复制编译产物,最终镜像不包含构建工具。
# 第一阶段:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 第二阶段:运行
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]
使用多阶段构建后,镜像体积可以从 1GB+ 压缩到 200MB 以内,这对于 CI/CD 的构建速度和容器启动时间都有显著提升。
Dockerfile 最佳实践
- 使用
.dockerignore:排除node_modules、.git、.env等不需要的文件,避免不必要的构建上下文传输 - 合并 RUN 指令:每个 RUN 指令会创建一个新的镜像层,合并相关命令可以减少层数
- 使用特定版本标签:不要使用
FROM node:latest,而是使用FROM node:20-alpine,确保构建可重复 - 合理安排 COPY 顺序:先复制不常变的文件(如
package.json),利用 Docker 的层缓存加速构建 - 使用非 root 用户运行:在 Dockerfile 中创建专用用户,避免容器以 root 权限运行
三、Docker Compose 多容器编排
真实项目中,一个应用通常需要多个服务配合运行——比如 Web 服务器 + 数据库 + 缓存 + 消息队列。Docker Compose 让你可以用一个 YAML 文件定义所有这些服务,然后一条命令启动全部。
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./uploads:/app/uploads
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
volumes:
pgdata:
redisdata:
关键配置说明:
depends_on+healthcheck:确保数据库完全就绪后才启动应用,避免连接失败volumes:持久化数据存储,容器重启后数据不丢失environment:通过环境变量注入配置,而不是硬编码在代码中
四、镜像优化策略
1. 选择正确的基础镜像
基础镜像的选择对最终镜像体积影响巨大。以 Node.js 为例:
node:20:约 1GB,包含完整操作系统和工具链node:20-slim:约 250MB,精简版 Debiannode:20-alpine:约 120MB,基于 Alpine Linux
对于大多数应用,alpine 版本是最佳选择。但要注意 Alpine 使用 musl libc 而非 glibc,某些原生模块可能需要特殊处理。
2. 减少层数
# 不推荐:多层导致镜像变大
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean
# 推荐:合并为一条指令
RUN apt-get update && \
apt-get install -y curl git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
3. 使用 distroless 镜像
Google 的 distroless 镜像只包含你的应用及其运行时依赖,连 shell 都没有。这是最极致的镜像瘦身方案,但也意味着无法在容器内执行 docker exec 调试。
五、生产环境最佳实践
安全
- 绝不将密钥写入镜像:使用 Docker Secrets、环境变量或外部密钥管理服务
- 定期扫描镜像漏洞:使用
docker scout或 Trivy 扫描镜像中的已知漏洞 - 使用非 root 用户:在 Dockerfile 中创建并使用专用用户
- 限制容器资源:使用
--memory和--cpus限制容器资源使用,防止单个容器耗尽宿主机资源
日志
- 将日志输出到 stdout/stderr:不要写日志文件,让 Docker 的日志驱动处理日志收集
- 使用集中式日志系统:如 ELK、Loki、Datadog
健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
健康检查让 Docker 和编排工具(如 Kubernetes、Docker Swarm)能够自动检测并重启不健康的容器。
总结
Docker 已经成为现代软件开发不可或缺的基础设施。掌握 Docker 不仅仅是会写 Dockerfile——更重要的是理解镜像分层、构建缓存、多阶段构建等底层原理,以及在生产环境中如何安全、高效地运行容器。
学习路径建议:
- 先学会用 Docker 运行已有的应用
- 然后学会为你的应用编写 Dockerfile
- 接着用 Docker Compose 编排多服务应用
- 最后学习 Kubernetes 或 Docker Swarm 进行容器编排
容器化不是终点,而是云原生之路的起点。