从踩坑到精通:聊聊 Docker、Compose 与 Dockerfile 的那些事

记得我第一次接触 Docker 时,按照教程敲下 docker run hello-world,看到屏幕上跳出 "Hello from Docker!" 的瞬间,心里既兴奋又懵懂。几年过去,Docker 已成为我日常开发不可或缺的工具,但其中的几个概念曾让我混淆良久——特别是那些名字相似的 "compose" 相关命令。今天,我想用最接地气的方式,聊聊 Docker 生态中这些常被误解的核心概念。

一、Docker:不只是个"集装箱"

Docker 本身是什么?简单说,它是让应用及其依赖环境打包成"集装箱"(容器)的技术。想象你要搬家:传统方式是把家具、电器零散搬运,到了新家再重新组装调试;而 Docker 则是把整个房间原封不动地装进集装箱,运到新地点直接开门就能用。

关键点:Docker 引擎(Docker Engine)是核心,负责创建和管理容器。当你执行 docker builddocker run 等命令时,你是在和 Docker 引擎对话。

二、那个让人困惑的"Compose":历史与现状

1. 老朋友:docker-compose(带横杠的)

这是很多人的"初恋"。docker-compose 是一个独立的 Python 项目,需要单独安装。它的标志性特征是命令中带横杠:

# 老式命令(独立二进制文件)
docker-compose up -d

特点:

  • 配置文件名为 docker-compose.yml(也带横杠)
  • 作为独立工具存在,与 Docker 引擎是"伙伴关系"
  • 在 Docker Desktop 旧版本中默认包含

2. 新伙伴:docker compose(不带横杠的)

2020 年后,Docker 公司将 compose 功能直接集成到 Docker 引擎中,成为原生子命令:

# 新式命令(Docker 的子命令)
docker compose up -d

关键区别:

  • 不再需要单独安装,只要 Docker 版本 >= 20.10 就自带
  • 命令中没有横杠docker compose 而非 docker-compose
  • 配置文件可命名为 compose.yml(推荐)或保留旧名
  • 性能更好,与 Docker 引擎深度集成

真实经验:我在一个项目中因混用两种 compose 导致环境不一致,花了一整天排查。现在我的规则是:新项目一律用 docker compose(无横杠),旧项目保持原样。

3. 如何辨别自己用的是哪种?

# 查看版本信息
docker compose version    # 新版输出类似 Docker Compose version v2.23.0
docker-compose --version  # 旧版输出类似 docker-compose version 1.29.2

简单记忆:新版无横杠,是 Docker 的一部分;旧版带横杠,是独立程序

三、Dockerfile:容器的"菜谱"

如果说容器是做好的菜,Dockerfile 就是详细菜谱。它告诉 Docker 如何一步步构建镜像。

1. 基本结构与原理

Dockerfile 本质是文本文件,每行一条指令。Docker 按顺序执行这些指令,每步生成一个"中间层",最终叠成完整镜像。

# 示例:一个简单的 Node.js 应用
FROM node:18-alpine               # 基础镜像(相当于锅具)
WORKDIR /app                      # 工作目录(厨房操作台)
COPY package*.json ./             # 复制依赖声明
RUN npm install                   # 安装依赖(准备食材)
COPY . .                          # 复制代码
EXPOSE 3000                       # 声明端口
CMD ["node", "server.js"]         # 启动命令(最后烹饪步骤)

关键机制:Docker 使用"层缓存"加速构建。如果某行指令没变,Docker 会复用之前构建的层,跳过重复步骤。这就是为什么我们通常先复制依赖文件再复制代码——避免每次代码改动都重装依赖。

2. 常见指令解析

  • FROM:指定基础镜像,必须是第一条指令(除了 ARG)
  • RUN:执行命令并创建新层,适合安装软件包
  • COPY/ADD:复制文件,ADD 有额外功能(如自动解压)
  • CMD/ENTRYPOINT:容器启动命令,CMD 可被覆盖,ENTRYPOINT 不可
  • ENV:设置环境变量
  • EXPOSE:声明容器监听端口(实际不发布端口,仅文档作用)
  • VOLUME:创建挂载点,用于持久化数据

3. 真实世界的 Dockerfile 陷阱

  1. 缓存失效问题
    早期我把 COPY . . 放在 RUN npm install 前面,结果每次改一行代码都重装所有依赖。教训:按变更频率排序指令,变动少的放前面。

  2. 镜像膨胀
    有次构建出 1.2GB 的 Node.js 镜像,排查发现用了 node:latest 而非 node:alpine。改用 Alpine 基础镜像后,体积降到 180MB。

  3. 安全漏洞
    在 Dockerfile 中用 RUN apt-get update && apt-get install -y ... 时忘了清理缓存,导致镜像包含不必要的包索引。正确做法:

    RUN apt-get update && \
        apt-get install -y --no-install-recommends nginx && \
        rm -rf /var/lib/apt/lists/*
    

四、Compose 文件:多容器应用的"导演"

当应用需要多个容器协作(如 Web 服务 + 数据库 + 缓存),Docker Compose 就登场了。它通过 YAML 文件定义整个应用栈。

# compose.yml(新版命名)
services:
  web:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      DB_HOST: db
  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: example

volumes:
  pgdata:

关键概念

  • services 定义应用组件
  • volumes 声明持久化卷
  • networks 可自定义网络(默认已创建应用网络)
  • 服务间通过服务名直接通信(如 web 容器访问 db 主机名)

调试技巧:当容器启动失败,用 docker compose logs -f web 实时查看日志,比反复重启高效得多。

五、工具选择指南:何时用什么?

  • 单容器实验:直接用 docker run
    例:docker run -p 80:80 nginx

  • 多容器应用:用 docker compose(新版)
    例:Web 应用 + Redis + PostgreSQL 组合

  • 构建自定义镜像:编写 Dockerfile
    例:将 Python 应用打包为可移植镜像

  • CI/CD 流水线:Dockerfile + docker compose 配合使用
    例:GitHub Actions 中构建镜像并测试

结语:从工具到思维

刚学 Docker 时,我执着于命令细节,直到某天在凌晨三点的故障排查中,才真正理解其价值:环境一致性。开发机上能跑的容器,在测试环境、生产环境几乎必然能跑——这种确定性在分布式系统中弥足珍贵。

至于 compose 的横杠之争?不妨这样想:Docker 正在简化自身生态,将常用功能内化。就像手机从需要装各种 APP 到系统自带实用工具,docker compose(无横杠)是未来。但旧项目中的 docker-compose 仍会存在多年,理解两者的区别,能让你在不同环境中游刃有余。

最后分享个小习惯:我在每个项目根目录放两个文件——Dockerfilecompose.yml。前者定义单个服务的构建规则,后者描述整个应用架构。这种组合像乐高积木:既可独立使用,又能拼成复杂系统。当你能随心所欲地拆解和组装这些"积木"时,就真正掌握了 Docker 的精髓。

实用资源

  • Docker 官方文档的"最佳实践"章节比教程更有价值
  • docker system df 定期清理无用镜像/容器,避免磁盘爆满
  • 遇到问题时,docker inspect <container> 是终极调试工具