# Docker Compose 本地环境标准化:5 个配置避免"我机器上能跑"

新成员配环境半天跑不起来、数据库版本不一致导致 SQL 报错、本地数据在 docker compose down 后全部丢失。这些问题本质上都是同一个原因:本地环境没有标准化。本文给出 5 个关键配置和一个开箱即用的模板。

技术栈: Docker Compose v3.8 + MySQL 8.0 + Redis 7 + Python 3.12


1. 镜像版本锁定:latest 是定时炸弹

问题

yaml 复制代码
# 每次 docker compose pull 可能拉取到不同版本
services:
  db:
    image: mysql        # 隐含 :latest
  redis:
    image: redis

latest 标签不保证可复现性。今天拉的是 MySQL 8.0.35,下周拉的可能变成 8.1。团队成员在不同时间 docker compose pull,得到的镜像版本不同,产生以下问题:

  • SQL 语法差异 :8.0 的 GROUP BY 宽松模式在 8.1 默认严格模式,已有查询直接报错
  • 生产环境不一致 :生产环境通常锁定具体版本,本地用 latest 意味着你测试的 MySQL 和生产跑的版本可能差一个主版本
  • 回滚困难:镜像 tag 被覆盖后无法回退到之前测试过的版本

修复

yaml 复制代码
services:
  db:
    image: mysql:8.0.35       # 精确到补丁版本
  redis:
    image: redis:7.2-alpine   # Alpine 版本体积减少 80%
  app:
    image: python:3.12.2-slim # slim 比完整版小 800MB

推荐组合(2026.07)

服务 镜像 拉取大小 说明
Python python:3.12-slim ~150MB 生产部署也用 slim,保持一致
MySQL mysql:8.0 ~500MB 锁定主版本即可,8.0.x 内部无 breaking
Redis redis:7-alpine ~30MB 对开发环境完全够用
PostgreSQL postgres:16-alpine ~250MB 16 是当前 LTS
Nginx nginx:1.27-alpine ~40MB 静态资源 + 反向代理

原则:主版本号必须写,小版本按需锁定。 如果服务之间强依赖特定 API 行为(如 MySQL 窗口函数),精确到补丁版本。


2. 数据库初始化:别再手动建表了

问题

团队里每个开发者都要手动执行 CREATE DATABASECREATE TABLEINSERT 种子数据。结果是:

  • 同一个表在不同人本地字段不一致
  • 种子数据不同导致测试结果不一样
  • 新人第一天浪费在配环境上

修复:利用容器初始化机制

MySQL 官方镜像的 docker-entrypoint-initdb.d 目录会在容器首次启动 时自动执行其中的 .sql.sh 文件,按文件名字母序执行。

yaml 复制代码
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-devpass}
      MYSQL_DATABASE: myapp
    volumes:
      - ./docker/mysql/init:/docker-entrypoint-initdb.d
      - mysql_data:/var/lib/mysql

目录结构:

bash 复制代码
docker/mysql/init/
├── 01-schema.sql      # 建表
├── 02-seed.sql        # 测试数据
└── 03-fixtures.sql    # 复杂 fixture

01-schema.sql

sql 复制代码
CREATE TABLE IF NOT EXISTS users (
    id CHAR(36) PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
    id CHAR(36) PRIMARY KEY,
    user_id CHAR(36) NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    status ENUM('pending', 'paid', 'shipped') DEFAULT 'pending',
    FOREIGN KEY (user_id) REFERENCES users(id)
);

02-seed.sql

sql 复制代码
INSERT INTO users (id, name, email) VALUES
    ('a0000001', '张三', 'zhangsan@example.com'),
    ('a0000002', '李四', 'lisi@example.com');

INSERT INTO orders (id, user_id, amount, status) VALUES
    ('b0000001', 'a0000001', 299.00, 'paid'),
    ('b0000002', 'a0000002', 159.50, 'pending');

执行逻辑:容器初次启动时,MySQL 自动按 010203 顺序执行。已有数据的 volume 不会重复执行。


3. 环境变量管理:三类变量的隔离策略

问题

环境变量分为三类,混在一起就会出问题:

类别 示例 特点 错误做法
环境差异 数据库地址、Redis URL 本地/测试/生产各不相同 写死在代码里
敏感密钥 API Key、DB 密码 绝对不能入 Git 放在 docker-compose.yml
功能开关 Debug 模式、日志级别 开发开/生产关 生产忘了关 Debug

修复:分层管理

docker-compose.yml --- 只声明变量,不写值:

yaml 复制代码
services:
  app:
    env_file:
      - .env.local
    environment:
      - DATABASE_URL=mysql://root:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
      - REDIS_URL=redis://redis:6379
      - DEBUG=${DEBUG:-false}   # 默认 false,安全兜底

.env.local --- 本地敏感值,不入库:

bash 复制代码
MYSQL_PASSWORD=local_dev_only
MYSQL_DATABASE=myapp_dev
DEBUG=true
API_KEY=sk-local-test-key

.env.example --- 提交到仓库的模板:

bash 复制代码
MYSQL_PASSWORD=请替换为你的本地密码
MYSQL_DATABASE=myapp_dev
DEBUG=false
API_KEY=请填入你的API密钥

代码层 --- 启动即校验:

python 复制代码
import os
from functools import lru_cache

@lru_cache()
def get_config() -> dict:
    """应用启动时调用一次,缺变量直接报错"""
    required = ["DATABASE_URL", "REDIS_URL"]
    missing = [k for k in required if k not in os.environ]
    if missing:
        raise RuntimeError(f"缺少环境变量: {', '.join(missing)}")
    return {
        "database_url": os.environ["DATABASE_URL"],
        "redis_url": os.environ["REDIS_URL"],
        "debug": os.environ.get("DEBUG", "false").lower() == "true",
    }

os.environ["KEY"](不加默认值)而非 os.environ.get("KEY", "default")------缺少必需变量时启动即报错,比运行时出 bug 好排查一万倍。


4. 启动顺序控制:depends_on 不够用

问题

yaml 复制代码
app:
  depends_on:
    - db
    - redis

depends_on 只保证"db 容器先启动",不保证"MySQL 服务已就绪"。容器启动 ≠ 服务可用。MySQL 从容器启动到端口监听通常需要 5-15 秒,这期间应用连接会报 Connection refused

修复:healthcheck + condition

yaml 复制代码
services:
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 3s
      timeout: 3s
      retries: 10
      start_period: 10s    # 留给 MySQL 初始化的时间

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 3s
      retries: 5

  app:
    depends_on:
      db:
        condition: service_healthy   # 关键:等服务真正 ready
      redis:
        condition: service_healthy

Healthcheck 参数说明:

参数 含义 建议值
interval 检查间隔 3s
timeout 单次检查超时 3s
retries 失败重试次数 MySQL: 10, Redis: 5
start_period 启动宽限期 MySQL: 10-30s

补充方案:应用层重试

healthcheck 解决了 docker-compose 层面的问题,但代码里建议也加一层重试作为双保险:

python 复制代码
import time
import mysql.connector
from mysql.connector.errors import DatabaseError

def wait_for_db(max_retries: int = 30, interval: float = 1.0):
    """阻塞直到数据库可用,或超时抛出异常"""
    for i in range(max_retries):
        try:
            conn = mysql.connector.connect(
                host="db", user="root",
                password=os.environ["MYSQL_PASSWORD"],
            )
            conn.close()
            return
        except DatabaseError:
            time.sleep(interval)
    raise RuntimeError(f"数据库 {max_retries}s 内未就绪")

5. 数据持久化:volume 配置的三层防御

问题

yaml 复制代码
# docker compose down → 数据全丢
services:
  db:
    image: mysql:8.0
    # 没有 volumes ------ 数据在容器内,容器删了就没了

更危险的是:docker compose down -v 会删除匿名卷。一句命令,全团队开发数据归零。

修复:三层防御

第一层:命名卷

yaml 复制代码
services:
  db:
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:
    driver: local

第二层:external: true

yaml 复制代码
volumes:
  mysql_data:
    driver: local
    external: true   # docker compose down -v 不会删除此卷

第三层:备份脚本(CI/定时任务里跑)

bash 复制代码
#!/bin/bash
# scripts/backup-dev-db.sh
docker compose exec -T db mysqldump -u root -p"$MYSQL_PASSWORD" myapp \
  > backups/dev_$(date +%Y%m%d_%H%M%S).sql

命名卷 vs 绑定挂载

命名卷 绑定挂载
路径 Docker 管理 (/var/lib/docker/volumes/) 你指定 (./data)
跨平台 ✅ 无路径差异 ❌ Linux/Mac/Windows 路径不同
down -v 行为 会被删除(除非 external:true 不删除本地目录
使用场景 生产环境、不需要看数据文件 开发环境、需要直接查看/编辑

生产级模板(直接使用)

yaml 复制代码
version: "3.8"

services:
  db:
    image: mysql:8.0
    container_name: myapp-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/mysql/init:/docker-entrypoint-initdb.d
      - ./docker/mysql/conf:/etc/mysql/conf.d    # 自定义 MySQL 配置
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 3s
      timeout: 3s
      retries: 10
      start_period: 10s
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 3s
      retries: 5
    networks:
      - app-network

  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: myapp-app
    restart: unless-stopped
    env_file:
      - .env.local
    ports:
      - "${APP_PORT:-8000}:8000"
    volumes:
      - .:/app
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: >
      sh -c "python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
    networks:
      - app-network

volumes:
  mysql_data:
    driver: local
    external: true
  redis_data:
    driver: local
    external: true

networks:
  app-network:
    driver: bridge

使用步骤:

bash 复制代码
# 1. 创建外部卷(防止误删)
docker volume create mysql_data
docker volume create redis_data

# 2. 创建 .env.local
cp .env.example .env.local && vim .env.local

# 3. 启动
docker compose up -d

# 4. 验证
docker compose ps           # 所有服务 status = healthy
docker compose logs app     # 查看应用日志

总结

配置 问题 解决方案 关键代码
版本锁定 latest 不可复现 image: mysql:8.0 锁定主版本,必要时到补丁版
初始化 每人手动建表 docker-entrypoint-initdb.d .sql 文件字母序自动执行
环境变量 密钥硬编码 env_file + os.environ 强制校验 缺变量启动即报错
启动顺序 服务未 ready 就连接 healthcheck + service_healthy start_period 给初始化留时间
数据持久化 down -v 丢数据 external: true 命名卷 三层防御防止误删

把这套模板在新项目落地后,新人第一天就能写代码:git clonecp .env.example .env.localdocker compose up -d → 跑起来了。


2026 年 7 月 | 约 2800 字 | 预计阅读 8 分钟


相关推荐
李燚2 天前
Eino devops 调试系统源码:GraphCompileCallback 怎么工作(第50篇-E36)
数据库·golang·agent·devops·graphql·aiagent·eino
leoZ2312 天前
Skill 技能系统完全指南(四):实战 Skill——DevOps 部署
运维·devops
leeyi4 天前
调试 Agent 的 3 个姿势:日志、断点、可视化(第49篇-E35)
aigc·agent·devops
南宫乘风6 天前
我们用企微机器人 + Claude 搭了个内部 DevOps 助手,替代了 60% 的日常杂活
机器人·claude·devops
love530love6 天前
GPT + Codex CLI + CodexPro 三位一体:AI 编程协作工作流
人工智能·windows·gpt·chatgpt·devops·mcp
姚青&7 天前
持续交付与 DevOps 体系
运维·devops
爱好曙光7 天前
云原生混沌工程实战:基于Litmus的故障注入与弹性测试
云原生·kubernetes·devops·混沌工程·故障注入·litmus·弹性测试
运维大师7 天前
【云原生与DevOps】07-Istio服务网格落地:从试点到全量的踩坑记录
云原生·istio·devops
运维大师7 天前
【云原生与DevOps】08-多云容灾架构设计:跨Region自动切换实践
运维·云原生·devops