MySQL 版本不对、Redis 端口被占、Node 版本和 package-lock 打架中间还重装了一次 Python。这种事我经历过太多次了。后来我们团队的做法是不给文档给一份compose.yaml。新人git clone完敲一条命令环境就有了。这篇就把这套东西从零搭一遍。用的是 Docker Compose v2 的最新写法包含几个大多数人没用过但确实省事的功能。环境准备只需要装 Docker Desktop或者 Docker Engine Compose v2 插件别的什么都不用。docker--version# 24.x 以上dockercompose version# v2.x注意是 docker compose中间是空格有个小细节docker-compose带横杠是 v1早就停止维护了。如果你敲出来是这个说明装的是老的 Python 版建议换掉。第一步写 compose.yaml先说一个很多人不知道的变化——version这个顶层字段已经废弃了。新的标准文件名是compose.yaml其次是compose.ymldocker-compose.yml属于旧命名。# compose.yamlservices:api:build:context:.target:develop# 多阶段构建的 dev 阶段ports:-8000:8000environment:DATABASE_URL:postgresqlasyncpg://app:secretdb:5432/appdbREDIS_URL:redis://cache:6379/0depends_on:db:condition:service_healthy# 等健康检查通过再启动不是等容器起来cache:condition:service_starteddevelop:watch:-action:syncpath:./apptarget:/app/app-action:rebuildpath:./pyproject.tomldb:image:postgres:17-alpineenvironment:POSTGRES_USER:appPOSTGRES_PASSWORD:secretPOSTGRES_DB:appdbvolumes:-db_data:/var/lib/postgresql/dataports:-5432:5432# 暴露出来本地 IDE 能直连healthcheck:test:[CMD-SHELL,pg_isready -U app -d appdb]interval:5stimeout:3sretries:5start_period:10scache:image:redis:8-alpinevolumes:-cache_data:/datavolumes:db_data:cache_data:depends_on里的condition: service_healthy是重点。默认的depends_on只保证容器启动顺序不保证里面的服务真的能用了。Postgres 容器起来后要几秒才接受连接如果 api 抢跑就会报连接拒绝——很多人第一次用 Compose 都被这个坑过。第二步写多阶段 Dockerfile开发和生产用同一个 Dockerfile靠target区分FROM python:3.13-slim AS base ENV PYTHONUNBUFFERED1 PIP_NO_CACHE_DIR1 WORKDIR /app COPY --fromghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv # ---- 开发阶段 ---- FROM base AS develop COPY pyproject.toml uv.lock ./ RUN uv sync --frozen COPY . . CMD [uv, run, uvicorn, app.main:app, --reload, --host, 0.0.0.0, --port, 8000] # ---- 生产阶段 ---- FROM base AS prod COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev COPY . . USER 1001 CMD [uv, run, gunicorn, app.main:app, -k, uvicorn.workers.UvicornWorker, -w, 4, -b, 0.0.0.0:8000]开发阶段带--reload和不带--no-dev生产阶段反过来。这两个阶段分开写比在 compose 里用command覆盖更清楚。第三步用 watch 告别手动 buildCompose v2.22 起有了develop.watchv2.23 加了syncrestartv2.32 又加了restart和syncexec。它有三种动作action行为什么时候用sync只同步文件不重启应用自己有热重载uvicorn --reload、nodemonsyncrestart同步后重启容器改配置需要重启才生效比如 nginx.confrebuild重新构建镜像并替换容器依赖清单变了pyproject.toml/package.jsondockercomposewatch# 或者直接dockercompose up--watch它会监控你配的路径改代码就同步进去uvicorn 自己热重载改了依赖清单就自动 rebuild。日常用下来基本告别了手动build那三分钟。有个坑要注意watch 的 sync 需要镜像里有stat、mkdir、rmdir三个命令而且容器里的用户必须对目标路径有写权限。如果用非 root 用户Dockerfile 里COPY记得加--chown否则同步会静默失败COPY --chownapp:app . /app第四步profiles 区分环境单机开发时你可能还想开个 pgAdmin、看看 Prometheus 面板但这些不该是所有人默认启动的。用 profilespgadmin:image:dpage/pgadmin4:latestprofiles:[debug]ports:-5050:80prometheus:image:prom/prometheus:latestprofiles:[monitoring]ports:-9090:9090dockercompose up-d# 只有基础服务dockercompose--profiledebug up-d# 加 pgAdmindockercompose--profiledebug--profilemonitoring up-d没写profiles的服务永远启动写了 profile 的只在激活时才起。比维护三四份 compose 文件清爽得多。第五步别再明文写密码见过太多人的 compose 文件里直接写MYSQL_ROOT_PASSWORD: 123456然后连文件一起提交到 Git。Compose 支持 secretsservices:db:image:postgres:17-alpineenvironment:POSTGRES_PASSWORD_FILE:/run/secrets/db_password# 注意是 _FILE 后缀secrets:-db_passwordsecrets:db_password:file:./secrets/db_password.txt注意 Postgres 用的是POSTGRES_PASSWORD_FILE这种带_FILE后缀的变量不是直接把值传进去。这是镜像本身的约定不是 Compose 的语法换别的镜像要查一下它支持不支持。第六步日志和信号也别漏本地开发环境跑久了最常见的诡异问题不是服务挂了是磁盘满了。Docker 默认的json-file日志驱动不带轮转一个疯狂打日志的服务几天就能写掉几十 GB。给服务加上轮转配置services:api:logging:driver:json-fileoptions:max-size:10mmax-file:3另外两个值得加的小配置api:stop_grace_period:30s# docker compose down 时给它 30 秒优雅退出init:true# 用 tini 做 1 号进程正确回收僵尸子进程init: true在跑 shell 脚本、或者应用会 fork 子进程时很有用。容器里的 1 号进程如果不会wait()回收子进程跑久了会攒一堆僵尸进程占着 PID。加这一行就够了Compose 会自动注入一个轻量的 init。常用命令dockercompose up-d# 起dockercomposeps# 看状态dockercompose logs-fapi# 跟日志dockercomposeexecapibash# 进容器dockercompose up-d--build# 重建dockercompose config# 校验并打印合并后的配置dockercompose down-v# 停掉并删数据卷最后那条down -v慎用-v会连数据卷一起删掉数据库就清空了。想重置环境时很好用但手滑一次就得重来。docker compose config这条值得养成习惯它会把你写的文件、.env、以及include进来的所有内容合并展开打印最终生效的配置。遇到我明明改了配置怎么没生效先跑这条看一眼。几个我实际踩过的坑1. 容器里的 localhost 不是你的电脑。在容器里连localhost:5432一定失败因为那是容器自己。要连宿主机的服务用host.docker.internalMac/WindowsLinux 上这个域名默认不存在得加extra_hosts: - host.docker.internal:host-gateway。2. bind mount 在 Mac/Windows 上很慢。挂一个几万文件的node_modules进去文件 IO 能慢到怀疑人生。解决方案就是用develop.watch的 sync 代替 bind mount或者把依赖目录用匿名卷屏蔽掉volumes:-./app:/app-/app/node_modules# 匿名卷让容器内自己维护3. 改了配置忘了重载。Compose 里改compose.yaml后部分字段比如 ports、environment需要docker compose up -d重新创建容器才生效光restart不行。改完记得up -d或者up -d --force-recreate。最后这套东西的价值不在于用了 Docker在于环境变成了代码。新人来了不用问任何人docker compose up -d就完事换台电脑不用重新配一遍线上出问题排查时本地能和线上跑同一套依赖版本。我们团队现在的标准是README 里超过三行的环境配置说明一律改成一份 compose 文件。环境配置是文档最容易过期的地方写成代码就不会了。