新成员配环境半天跑不起来、数据库版本不一致导致 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 DATABASE、CREATE TABLE、INSERT 种子数据。结果是:
- 同一个表在不同人本地字段不一致
- 种子数据不同导致测试结果不一样
- 新人第一天浪费在配环境上
修复:利用容器初始化机制
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 自动按 01 → 02 → 03 顺序执行。已有数据的 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 clone → cp .env.example .env.local → docker compose up -d → 跑起来了。
2026 年 7 月 | 约 2800 字 | 预计阅读 8 分钟