未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用范围与待维护事项

本文是需补全的开发测试示例,非可直接部署的生产方案。须自行提供数据库初始化 SQL、FastAPI 应用及 /health、nginx.conf、质量测试、Spark/Flink 集群配置与任务代码;ETL 定时调度未实现,depends_on 不保证数据库就绪,追加写入不具幂等性。示例弱口令、数据库高权限账户及公开端口仅限隔离测试;连接串特殊字符、密钥管理和持久化需另行处理。CI 推送还需配置镜像名称与仓库认证;镜像及依赖版本未在本轮运行验证。

Docker 实战场景

abstract 本章要点

  • ETL/ELT 脚本容器化的完整流程
  • 数据服务(API、定时任务)容器化部署
  • 用 Docker 标准化团队开发环境
  • CI/CD 流水线中使用 Docker 运行数据质量检查

场景一:ETL 脚本容器化

背景

你写了一个 Python ETL 脚本,从 MySQL 读数据,清洗后写入 PostgreSQL。现在要:

  1. 打包成可复用的镜像
  2. 支持通过环境变量配置数据源
  3. 配置定时运行
  4. 日志持久化

项目结构

etl-project/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── src/
│   ├── __init__.py
│   ├── etl_main.py
│   ├── extractors/
│   │   └── mysql_extractor.py
│   ├── transformers/
│   │   └── data_cleaner.py
│   └── loaders/
│       └── postgres_loader.py
├── config/
│   └── etl_config.yaml
├── data/
│   ├── input/
│   └── output/
└── logs/

ETL 脚本示例

# src/etl_main.py
import os
import logging
from datetime import datetime
import pandas as pd
import sqlalchemy

# 日志配置(输出到文件和标准输出)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(f'/app/logs/etl_{datetime.now().strftime("%Y%m%d")}.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

def get_db_config():
    """从环境变量读取数据库配置"""
    return {
        'source': {
            'host': os.environ['SOURCE_DB_HOST'],
            'port': int(os.environ.get('SOURCE_DB_PORT', 3306)),
            'user': os.environ['SOURCE_DB_USER'],
            'password': os.environ['SOURCE_DB_PASSWORD'],
            'database': os.environ['SOURCE_DB_NAME'],
        },
        'target': {
            'host': os.environ['TARGET_DB_HOST'],
            'port': int(os.environ.get('TARGET_DB_PORT', 5432)),
            'user': os.environ['TARGET_DB_USER'],
            'password': os.environ['TARGET_DB_PASSWORD'],
            'database': os.environ['TARGET_DB_NAME'],
        }
    }

def run_etl():
    config = get_db_config()
    logger.info("ETL 任务开始")

    # Extract
    source_engine = sqlalchemy.create_engine(
        f"mysql+pymysql://{config['source']['user']}:{config['source']['password']}"
        f"@{config['source']['host']}:{config['source']['port']}"
        f"/{config['source']['database']}"
    )
    df = pd.read_sql("SELECT * FROM orders WHERE date = CURDATE()", source_engine)
    logger.info(f"Extract 完成:{len(df)} 行数据")

    # Transform
    df['amount'] = df['amount'].fillna(0)
    df['created_at'] = pd.to_datetime(df['created_at'])
    logger.info("Transform 完成")

    # Load
    target_engine = sqlalchemy.create_engine(
        f"postgresql://{config['target']['user']}:{config['target']['password']}"
        f"@{config['target']['host']}:{config['target']['port']}"
        f"/{config['target']['database']}"
    )
    df.to_sql('orders_cleaned', target_engine, if_exists='append', index=False)
    logger.info(f"Load 完成:{len(df)} 行写入 DW")

if __name__ == '__main__':
    run_etl()

Dockerfile

# Dockerfile
FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    TZ=Asia/Shanghai

# 安装数据库客户端依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq-dev \
    gcc \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 先装依赖(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再复制代码
COPY src/ ./src/
COPY config/ ./config/

# 创建日志和数据目录
RUN mkdir -p /app/logs /app/data/input /app/data/output

# 声明挂载点
VOLUME ["/app/logs", "/app/data"]

CMD ["python", "src/etl_main.py"]
# requirements.txt
pandas==2.1.4
sqlalchemy==2.0.25
pymysql==1.1.0
psycopg2-binary==2.9.9
loguru==0.7.2
pyyaml==6.0.1

Compose 配置(一次性运行,定时调度需另配)

# docker-compose.yml
services:

  # 数据库服务(开发测试用)
  mysql-source:
    image: mysql:8.0
    container_name: mysql-source
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: source_db
    volumes:
      - ./init-sql/source:/docker-entrypoint-initdb.d
    networks:
      - etl-net

  postgres-target:
    image: postgres:16
    container_name: postgres-target
    environment:
      POSTGRES_USER: dw
      POSTGRES_PASSWORD: dw123
      POSTGRES_DB: data_warehouse
    volumes:
      - ./init-sql/target:/docker-entrypoint-initdb.d
    networks:
      - etl-net

  # ETL 服务(执行一次后退出;每天运行需另配 cron 或调度系统)
  etl:
    build: .
    container_name: etl-job
    environment:
      SOURCE_DB_HOST: mysql-source
      SOURCE_DB_USER: root
      SOURCE_DB_PASSWORD: root123
      SOURCE_DB_NAME: source_db
      TARGET_DB_HOST: postgres-target
      TARGET_DB_USER: dw
      TARGET_DB_PASSWORD: dw123
      TARGET_DB_NAME: data_warehouse
    volumes:
      - ./logs:/app/logs
      - ./data:/app/data
    networks:
      - etl-net
    depends_on:
      - mysql-source
      - postgres-target
    # 任务跑完就退出,不需要 restart
    restart: "no"

networks:
  etl-net:
    driver: bridge
# 手动触发 ETL
docker compose run --rm etl

# 开发时,修改代码后重新构建并运行
docker compose run --rm --build etl

# 查看 ETL 日志
cat ./logs/etl_$(date +%Y%m%d).log

场景二:数据 API 服务容器化

背景

用 FastAPI 写了一个数据查询 API,需要容器化部署,连接 PostgreSQL 数据仓库。

# Dockerfile
FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/

EXPOSE 8000

# 用 uvicorn 运行 FastAPI
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
# docker-compose.yml
services:

  api:
    build: .
    container_name: data-api
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      DB_URL: postgresql://reader:reader123@postgres:5432/data_warehouse
      REDIS_URL: redis://:redis123@redis:6379/0
    depends_on:
      - postgres
      - redis
    networks:
      - api-net
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5).close()"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:16
    container_name: postgres
    environment:
      POSTGRES_USER: reader
      POSTGRES_PASSWORD: reader123
      POSTGRES_DB: data_warehouse
    volumes:
      - pg-data:/var/lib/postgresql/data
    networks:
      - api-net

  redis:
    image: redis:7-alpine
    container_name: redis
    command: redis-server --requirepass redis123
    networks:
      - api-net

  # Nginx 反向代理(可选;需提供 nginx.conf,HTTPS 和负载均衡需另配证书、端口及上游)
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api
    networks:
      - api-net

networks:
  api-net:
    driver: bridge

volumes:
  pg-data:

场景三:团队开发环境标准化

问题背景

团队有 5 个数据工程师,每人用不同配置的 Mac,经常出现"在你机器上能跑"的问题。

解决方案:一个 Compose 文件搞定所有依赖

# docker-compose.dev.yml
# 统一的开发环境,所有人用这个文件启动依赖服务

services:

  mysql:
    image: mysql:8.0
    container_name: dev-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: dev123
      MYSQL_DATABASE: dev_db
    ports:
      - "3306:3306"
    volumes:
      - dev-mysql-data:/var/lib/mysql
      - ./dev-fixtures/mysql:/docker-entrypoint-initdb.d   # 开发测试数据
    networks:
      - dev-net

  redis:
    image: redis:7-alpine
    container_name: dev-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    networks:
      - dev-net

  kafka:
    image: bitnami/kafka:3.7
    container_name: dev-kafka
    restart: unless-stopped
    ports:
      - "9092:9092"
    environment:
      KAFKA_CFG_NODE_ID: 0
      KAFKA_CFG_PROCESS_ROLES: controller,broker
      KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
      KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
      ALLOW_PLAINTEXT_LISTENER: "yes"
    networks:
      - dev-net

networks:
  dev-net:
    driver: bridge

volumes:
  dev-mysql-data:

配合 Makefile 让操作更简单:

# Makefile
.PHONY: dev-up dev-down dev-reset

# 启动开发环境
dev-up:
	docker compose -f docker-compose.dev.yml up -d
	@echo "开发环境已启动!"
	@echo "MySQL: localhost:3306 (root/dev123)"
	@echo "Redis: localhost:6379"
	@echo "Kafka: localhost:9092"

# 停止开发环境
dev-down:
	docker compose -f docker-compose.dev.yml down

# 重置开发环境(清除所有数据)
dev-reset:
	docker compose -f docker-compose.dev.yml down -v
	docker compose -f docker-compose.dev.yml up -d
	@echo "开发环境已重置"

# 查看日志
dev-logs:
	docker compose -f docker-compose.dev.yml logs -f

# 进入 MySQL
mysql-shell:
	docker exec -it dev-mysql mysql -uroot -pdev123 dev_db

新成员入职,一条命令搞定环境:

# 克隆项目
git clone https://github.com/company/data-platform.git
cd data-platform

# 启动开发环境
make dev-up

# 开始写代码

场景四:CI/CD 中使用 Docker

GitHub Actions 中运行数据质量检查

# .github/workflows/data-quality.yml
name: Data Quality Check

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  data-quality:
    runs-on: ubuntu-latest

    services:
      # CI 环境中启动 MySQL
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test123
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - uses: actions/checkout@v4

      # 在 Docker 容器中运行测试
      - name: Run data quality checks
        run: |
          docker run --rm \
            --network host \
            -e DB_HOST=127.0.0.1 \
            -e DB_PASSWORD=test123 \
            -v ${{ github.workspace }}:/app \
            python:3.11-slim \
            sh -c "pip install -r /app/requirements.txt && python /app/tests/data_quality_test.py"

      # 构建镜像并推送
      - name: Build and push Docker image
        if: github.ref == 'refs/heads/main'
        run: |
          docker build -t $IMAGE_NAME:${{ github.sha }} .
          docker tag $IMAGE_NAME:${{ github.sha }} $IMAGE_NAME:latest
          docker push $IMAGE_NAME:latest

本地 CI 模拟(act 工具)

# 安装 act(本地运行 GitHub Actions)
brew install act

# 本地运行 CI 流水线
act push

场景五:大数据任务的容器化

Spark 任务提交到 Docker 集群

# 构建包含业务代码的 Spark 镜像
cat > Dockerfile.spark <<EOF
FROM bitnami/spark:3.5
USER root
COPY spark-jobs/ /opt/spark/jobs/
RUN pip install delta-spark==3.0.0 boto3==1.34.0
USER 1001
EOF

docker build -f Dockerfile.spark -t my-spark:latest .

# 启动 Spark 集群
docker compose up -d spark-master spark-worker

# 提交任务
docker run --rm \
  --network spark_default \
  my-spark:latest \
  spark-submit \
    --master spark://spark-master:7077 \
    --conf spark.executor.memory=2g \
    /opt/spark/jobs/daily_aggregation.py \
    --date 2024-01-15
# 构建 Flink 任务镜像
cat > Dockerfile.flink <<EOF
FROM flink:1.19-scala_2.12
COPY target/my-flink-job.jar /opt/flink/jobs/
EOF

# 构建包含任务 JAR 的镜像
docker build -f Dockerfile.flink -t my-flink:latest .

# 提交任务到已启动的 Flink 集群
docker run --rm \
  --network flink_default \
  my-flink:latest \
  flink run \
    --jobmanager jobmanager:8081 \
    --class com.company.MyFlinkJob \
    /opt/flink/jobs/my-flink-job.jar

实战技巧汇总

技巧一:使用环境变量文件管理多环境

# .env.dev
DB_HOST=localhost
DB_PASSWORD=dev123
API_URL=http://localhost:8080

# .env.prod
DB_HOST=prod-db.internal
DB_PASSWORD=xxx_secure_xxx
API_URL=https://api.company.com

# 指定用于 Compose 插值的环境变量文件启动
# 容器接收变量还需在 Compose 中用 environment 引用,或配置服务级 env_file
docker compose --env-file .env.dev up -d
docker compose --env-file .env.prod up -d

技巧二:开发模式下实时热重载

# docker-compose.dev.yml 的 app 服务
app:
  build: .
  volumes:
    - ./src:/app/src    # 挂载源码,改代码自动生效
  command: uvicorn src.main:app --reload --host 0.0.0.0 --port 8000
  # --reload 参数:代码变化自动重启服务

技巧三:容器内调试

# 在运行中的容器里临时安装调试工具
docker exec -it my-container bash
apt-get install -y vim curl netcat-openbsd

# 测试网络连通性
docker exec my-app nc -zv mysql 3306
# 输出:Connection to mysql 3306 port [tcp/mysql] succeeded!

# 查看容器的 DNS 解析
docker exec my-app nslookup mysql


info 前置知识


掌握检查

  • [ ] 能把一个 Python ETL 脚本打包成 Docker 镜像,并通过 cron 或调度系统定时运行
  • [ ] 理解多环境配置的最佳实践:用 .env.dev 和 .env.prod 分离配置,docker compose --env-file 切换环境
  • [ ] 能用 docker compose exec 在运行中的容器内执行调试命令(如进入 MySQL 执行 SQL)
  • [ ] 知道如何用 Docker 快速搭建一个新同事的开发环境:克隆仓库 → docker compose up -d → 开始开发
  • [ ] 了解 GitHub Actions 中如何用 Docker 运行数据质量检查(CI 中的容器化应用)

实践练习

进阶题:把你现有的一个 Python 数据处理脚本容器化:(1) 写 Dockerfile;(2) 写 docker-compose.yml 包含脚本容器 + MySQL 依赖;(3) 用 Compose 一键启动,验证脚本能正常访问 MySQL;(4) 用 .env 文件管理数据库连接配置,不在代码中硬编码。


下一步