“在我电脑上是好的”,通常不是一句玩笑,而是环境没有描述清楚。

把项目依赖的服务写进 docker-compose.yml,再配一个 Makefile,新环境从 clone 到跑起来可以缩短到几分钟。

目标:一条命令启动

一个典型的后端项目,本地至少需要:

  • MySQL:持久化数据
  • Redis:缓存和会话
  • 可选:RabbitMQ、MinIO、邮件沙箱

与其在文档里写十几步安装教程,不如把这些服务声明成代码。

目录结构

project/
├── docker-compose.yml
├── .env.example
├── Makefile
└── app/

docker-compose.yml 放在项目根目录,环境变量放在 .env,模板提交到仓库,真实值不提交。

Compose 文件

services:
mysql:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./ops/mysql/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot"]
interval: 5s
timeout: 3s
retries: 20

redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

volumes:
mysql_data:
redis_data:

几个容易忽略的点:

  • healthcheck 很重要:应用启动脚本应该等依赖健康,而不是简单 sleep 10。
  • 数据卷命名:不要随便 down -v,否则本地数据会一起消失。
  • 端口冲突:用 ${MYSQL_PORT:-3306}:3306 把宿主机端口做成可配置项。
  • 初始化脚本只执行一次:MySQL 的 initdb 目录只在数据卷为空时运行。

用 Makefile 收敛命令

把常用操作固定下来,团队里就不需要每个人记一套命令:

.PHONY: up down logs psql redis reset

up:
docker compose up -d --wait

down:
docker compose down

logs:
docker compose logs -f --tail=100

psql:
docker compose exec mysql mysql -uroot -proot app

redis:
docker compose exec redis redis-cli

reset:
docker compose down -v
docker compose up -d --wait

--wait 会等待 healthcheck 通过,比 sleep 可靠得多。

应用容器化:开发阶段先别急

本地开发时,我通常只把基础设施放进 Docker,应用仍然在宿主机上跑。这样可以保留断点调试、热更新和 IDE 的完整体验。

等需要验证“干净环境”时,再用一个 Dockerfile 构建应用镜像:

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

如果确实要把应用也放进 Compose,可以加一个 app 服务,并用 depends_on 加上健康检查条件:

depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy

不要提交的东西

.env
*.log
data/

同时提供 .env.example,把变量名和默认值写清楚。新同事复制一份就能启动。

最后

Docker Compose 的价值不是“用了容器”,而是把环境变成可读、可复现、可版本控制的代码。

当环境不再是口头约定,排查问题才能从“你那边是什么版本”回到真正的代码和数据。

好的开发环境应该像一个好的函数:输入明确,输出可复现。