别再手动装环境了!Docker Compose 一键搭建 Airflow + Postgres + Spark 完整数据栈
你的本地数据环境可能是个烂摊子
大多数数据工程师的本地环境能跑起来,但只在他们的机器上,而且得靠运气。你手动装 PostgreSQL,卡住 Python 版本,折腾 Airflow 又怕搞坏别的东西。新同事加入后花三天才能复现你的环境。
这本质不是工程问题,而是工具问题。Docker Compose 就是来解决它的。
Docker Compose 允许你把整个本地数据栈写成代码——服务、网络、卷、环境变量全在一个 compose.yaml 文件里,一条命令就能启动或销毁。不再有“在我机器上能跑”,不再有三天才能入门的噩梦。
本指南覆盖完整流程:Docker Compose 究竟是什么(以及不是什么)、你需要理解的四个核心概念、一个生产级的真实示例栈(包含 Airflow、PostgreSQL、Redis 和 Spark),以及大部分工程师容易踩的坑。
Docker Compose 是什么(以及不是什么)
Docker Compose 是一个编排工具,用于在单台机器上定义和运行多容器 Docker 应用。你写一个 compose.yaml 文件,描述每个服务——用什么镜像、暴露哪些端口、如何连接其他服务、数据存在哪里。
先注意一个关键点:Docker Compose v1 已在 2023 年 7 月结束生命周期。 旧的 docker-compose 命令(带连字符)已经没了。你应该使用 Docker Compose v2,它作为内置 CLI 插件随 Docker 一同提供。如果你在任何脚本或教程里看到 docker-compose,请把它替换成 docker compose(空格,没有连字符)。
另外,version: 字段在 Compose 文件顶部已经正式弃用,不需要写它,直接删掉。
Docker Compose 不是:
- 生产环境里 Kubernetes 的替代品
- 管理分布式多机器部署的工具
- 生产环境下妥善管理机密信息的方案
但用于本地开发、CI 流水线和单机测试环境?它几乎是无敌的。
前置条件
在写 YAML 之前,确保你已具备:
- Docker Desktop (v4.0+) 或 Linux 上的 Docker Engine + docker-compose-plugin
- 至少 8GB 可用内存(数据栈很吃内存)
- 最低 4 个 CPU 核心(Spark 尤其需要)
- 基本的命令行操作能力
运行以下命令确认环境正常:
docker compose version
# 应该显示 v2.24 或更高版本(2026 年)
如果命令失败或显示 v1.x,请先更新 Docker。
四个核心概念
在搭建完整栈之前,你需要理解 Docker Compose 实际管理的四个东西。
服务(Services)
一个服务就是一个运行中的容器。services: 下的每个条目会变成一个或多个容器。在数据工程栈里,服务就是你的数据库、调度器、消息代理、转换工具等。
网络(Networks)
默认情况下,一个 Compose 文件里的每个服务都可以用服务名作为主机名互相通信——不需要 IP 地址,不需要手动 DNS。这是最被低估的特性之一:你的 Airflow scheduler 连接 Postgres 时,主机名直接写 postgres 就行了。
卷(Volumes)
卷让你的数据在容器重启后依然存活。有两种类型:命名卷(由 Docker 管理,推荐用于数据库)和绑定挂载(宿主机的一个文件夹挂到容器里,适合 DAG、脚本和正在编辑的代码)。
环境变量(Environment Variables)
永远不要硬编码凭据到 compose 文件里。始终使用 .env 文件,并通过 ${VARIABLE_NAME} 语法引用变量。.env 文件不放进版本控制,compose.yaml 里就没有敏感信息。
搭建真实数据工程栈
我们来搭建一个实际可用的栈。这套工具覆盖了大部分数据工程工作流所需的内容:
- PostgreSQL — 充当元数据库和数据仓库(端口 5432)
- Redis — 作为 Celery 任务的消息代理(端口 6379)
- Apache Airflow — 工作流调度器(端口 8080)
- Apache Spark — 分布式数据处理(端口 4040 和 7077)
- Adminer — 轻量级数据库管理界面(端口 8085)
第一步:项目结构
先建一个干净的文件夹结构。文件夹混乱会导致挂载点和构建上下文一团糟。
data-eng-local/
├── compose.yaml
├── .env
├── .env.example
├── airflow/
│ ├── dags/
│ ├── logs/
│ ├── plugins/
│ └── config/
├── spark/
│ └── jobs/
├── postgres/
│ └── init/
│ └── 01_create_schemas.sql
└── README.md
两条规则:compose.yaml 放在根目录;永远不要把 .env 提交到 Git——现在就去添加到 .gitignore,免得忘记。
第二步:.env 文件
# .env —— 不要提交到版本控制
POSTGRES_USER=dataeng
POSTGRES_PASSWORD=changeme_local
POSTGRES_DB=warehouse
AIRFLOW_UID=50000
AIRFLOW__CORE__FERNET_KEY=your_fernet_key_here
AIRFLOW__WEBSERVER__SECRET_KEY=your_secret_key_here
REDIS_PASSWORD=redis_local_pass
生成 Fernet 密钥:
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
第三步:compose.yaml 文件
下面是完整的配置。请仔细阅读行内注释——它们解释了设计决策,而不仅仅是语法。
# compose.yaml —— 不需要 version 字段(Compose v2 已弃用)
x-airflow-common: &airflow-common
image: apache/airflow:3.0.4
environment: &airflow-common-env
AIRFLOW__CORE__EXECUTOR: CeleryExecutor
# 连接到 PostgreSQL 元数据库
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${POSTGRES_DB}
# Celery 结果后端也存到 PostgreSQL
AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres/${POSTGRES_DB}
# Celery 消息代理用 Redis
AIRFLOW__CELERY__BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW__CORE__FERNET_KEY}
AIRFLOW__WEBSERVER__SECRET_KEY: ${AIRFLOW__WEBSERVER__SECRET_KEY}
AIRFLOW_UID: ${AIRFLOW_UID}
env_file:
- .env
volumes:
- ./airflow/dags:/opt/airflow/dags # 绑定挂载 —— 修改 DAG 不用重建容器
- ./airflow/logs:/opt/airflow/logs
- ./airflow/plugins:/opt/airflow/plugins
- ./airflow/config:/opt/airflow/config
depends_on:
postgres:
condition: service_healthy # 等 PostgreSQL 健康检查通过后再启动
redis:
condition: service_healthy
services:
# ─── 数据库 ──────────────────────────────────────────────────────────────
postgres:
image: postgres:16
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data # 命名卷 —— 容器重启后数据还在
- ./postgres/init:/docker-entrypoint-initdb.d # 首次启动时自动执行 SQL 脚本
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# ─── 消息代理 ────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine # Alpine 版本,镜像更小功能完整
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# ─── Airflow ──────────────────────────────────────────────────────────────
airflow-init:
<<: *airflow-common
entrypoint: /bin/bash
command:
- -c
- |
airflow db migrate &&
airflow users create \
--username admin \
--password admin \
--firstname Admin \
--lastname User \
--role Admin \
--email admin@example.com
restart: "no" # 只运行一次就退出,不是长服务
airflow-webserver:
<<: *airflow-common
command: webserver
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
airflow-scheduler:
<<: *airflow-common
command: scheduler
restart: unless-stopped
airflow-worker:
<<: *airflow-common
command: celery worker
restart: unless-stopped
# ─── Spark ────────────────────────────────────────────────────────────────
spark-master:
image: bitnami/spark:3.5
environment:
- SPARK_MODE=master
ports:
- "4040:4040" # Spark UI
- "7077:7077" # Spark 主节点端口
volumes:
- ./spark/jobs:/opt/spark-jobs
restart: unless-stopped
spark-worker:
image: bitnami/spark:3.5
environment:
- SPARK_MODE=worker
- SPARK_MASTER_URL=spark://spark-master:7077
- SPARK_WORKER_MEMORY=2G
- SPARK_WORKER_CORES=2
depends_on:
- spark-master
restart: unless-stopped
# ─── 数据库管理界面 ────────────────────────────────────────────────────────
adminer:
image: adminer:latest
ports:
- "8085:8080"
depends_on:
- postgres
restart: unless-stopped
# ─── 命名卷 ────────────────────────────────────────────────────────────────
volumes:
postgres_data: # 由 Docker 管理,down/up 后数据依然存在
redis_data:
第四步:启动栈
# 第一次运行,初始化 Airflow 数据库并创建管理员用户
docker compose up airflow-init
# 然后启动所有服务(后台模式)
docker compose up -d
# 查看所有容器状态
docker compose ps
# 查看某个服务的实时日志(调试很有用)
docker compose logs -f airflow-scheduler
就是这样。所有服务都变绿后,你可以访问:
- Airflow 界面:http://localhost:8080(用户名 admin,密码 admin)
- Spark UI:http://localhost:4040
- Adminer:http://localhost:8085(服务器填写
postgres,用户填写dataeng)
健康检查:大多数人忽略的重要功能
这是大部分教程跳过的地方——但它真的很关键。
没有健康检查,depends_on 几乎没用。默认情况下,Docker 认为容器在进程启动的那一刻就算“已启动”,而不是里面的服务真正就绪。PostgreSQL 需要几秒初始化,Redis 也需要一点时间。如果 Airflow 在它们就绪之前就尝试连接,就会崩溃——然后你会看到一堆混乱的重启循环。
depends_on 里的 condition: service_healthy 解决了这个问题。它告诉 Docker:等那个服务的健康检查通过之后,再启动本服务。配合依赖服务上的 healthcheck 块,你的栈每次都能按正确顺序启动。
# 正确做法
depends_on:
postgres:
condition: service_healthy # ← 关键
没有这个,你就是在靠运气。运气不是策略。
常见错误及如何避免
- 把凭据硬写在 compose.yaml 里——别这么做。用
.env文件,花 30 秒就能设置好,避免不小心把密码提交到公开仓库。 - 使用
docker-compose(v1)而不是docker compose(v2)——老命令已死。从 2023 年中之前的教程复制配置时,要检查这个。 - 在 compose 文件里写
version:——这个字段在 Docker Compose v2 里已经过时,而且会在 Docker Desktop 里触发弃用警告。删掉它。 - 没有固定镜像版本——
postgres:latest是个陷阱。一次升级可能让你的初始化 SQL 失败、连接字符串变化、扩展不存在。始终固定到具体版本,比如postgres:16。 - 忘记把
.env加到.gitignore——真的,先做这个。 - 运行
docker compose down -v时以为只是docker compose down——-v会删除命名卷,意味着你的数据库数据全没了,无法撤销。用这个参数时一定要明确知道后果。
日常操作命令
栈跑起来后,你会经常用到这些命令:
# 查看运行状态和健康检查结果
docker compose ps
# 查看所有服务的实时日志
docker compose logs -f
# 只查看某个服务的日志
docker compose logs -f airflow-worker
# 重启某个服务,不碰其他服务
docker compose restart airflow-scheduler
# 在运行中的容器里执行一次性命令
docker compose exec postgres psql -U dataeng -d warehouse
# 进入容器的 shell 做调试
docker compose exec airflow-webserver bash
# 停止所有服务(保留卷,安全)
docker compose down
# 核武器——停止所有服务并删除所有卷
docker compose down -v
用 Profiles 管理多环境
当你的栈变得复杂时,有个功能值得了解:Docker Compose Profiles 允许你定义只在特定上下文里启动的服务。给服务加上 profiles: [dev] 或 profiles: [monitoring],它只在你显式请求该 profile 时才会运行。
services:
# 这个服务只在运行 docker compose --profile monitoring up 时启动
prometheus:
image: prom/prometheus:latest
profiles:
- monitoring
ports:
- "9090:9090"
这样,你用一个 compose.yaml 就能适应所有环境——本地开发、CI、测试——而不需要维护多个文件。这是让 Compose 比它传统形象更接近生产级能力的特性之一。
工具版本与参考
本指南中使用的工具版本及官方文档:
- Docker Compose v2.24+ — 官方文档:docs.docker.com/compose
- Apache Airflow 3.0.4 — 官方文档:airflow.apache.org
- PostgreSQL 16 — 官方文档:postgresql.org/docs/16
- Redis 7 (Alpine) — 官方文档:redis.io/docs
- Apache Spark 3.5 (Bitnami) — 官方文档:spark.apache.org/docs/3.5.0
进一步阅读:
- Docker Compose 规范:compose-spec.io — 所有 YAML 语法的权威参考
- Apache Airflow 官方 Docker Compose 配置:airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html
- Data Engineering Zoomcamp (DataTalks.Club):涵盖 Docker + Airflow + dbt 的实战课程 — datatalks.club/courses/data-engineering-zoomcamp.html
- freeCodeCamp 2026年3月文章《How to Use Docker Compose for Production Workloads》— 深入讲解了 Profiles、watch mode 和 GPU 支持
总结
设置一个可靠的本地数据工程环境曾经是一整天的痛苦。有了 Docker Compose,这是一次性投资:写好 compose.yaml,提交到仓库,团队里每个人都能通过 docker compose up 获得完全一致的环境。
记住几点:
- 删掉
version:字段——它已弃用 - 始终在
depends_on里用condition: service_healthy - 固定镜像版本,别用
latest - 凭据放在
.env,永远别放 compose 文件里 - 命名卷要起名——不可恢复的数据没什么价值
本指南中的配置只是一个起点。随着栈增长,你还会用到 override 文件(compose.override.yaml)来做环境特定调整,以及 Compose Profiles 来开关监控工具、调试容器或测试数据库。
核心目标是可复现:当团队成员克隆你的仓库并运行 docker compose up 时,他们应该得到和你一模一样的环境。这不是锦上添花,而是严肃数据工程工作流的基础。




