早报站要真正“可交付”。现在起一套全栈要5条命令、关注启动顺序、操心环境变量——本篇之后:一条
docker compose up,PG + Redis + 后端 + 前端全部就位。
从“我机器上能跑”到“任何机器一条命令能跑”,这是部署的起点,也是开篇词“能上线”四件事的最后一坎。🎯本篇产出:后端/前端Dockerfile + docker-compose.yml(健康检查、依赖顺序、卷持久化)+ 常用编排命令。含代码约130行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | 用Docker Compose一键拉起全栈 |
| 代码行数 | ~130行(Dockerfile + compose) |
| 依赖 | Docker Desktop / Docker Engine + Compose v2+ |
| 核心功能 | 四服务编排 + 健康检查 + 依赖顺序 + 卷持久化 |
| 跑起来的命令 | docker compose up -d --build |
| 核心知识点 | 服务名即主机名、depends_on + healthcheck、volume |
| 做完你能得到 | 任何机器一条命令能跑的全栈应用 |
一、概念课:Compose在解决什么
手工起全栈的痛点(现在的你正在经历):起PostgreSQL → 起Redis → 装依赖 → 跑迁移 → 起uvicorn → 起npm dev——6条命令,顺序不能错,环境变量散在三个终端里,换个机器全部重来。
Compose的做法:把“有哪些服务、谁依赖谁、环境变量是什么、数据存哪里”写进一个compose.yaml(声明式配置),一条命令按依赖顺序全部拉起。
这就是**服务编排(orchestration)**的入门——所有容器编排工具(K8s是重型版)解决的都是同一件事。
📋 四个术语一次认清
| 术语 | 含义 | 类比 |
|---|---|---|
| image(镜像) | 只读模板,含运行时与依赖 | 光盘 |
| container(容器) | 镜像的运行实例 | 用光盘开的机 |
| service(服务) | compose里定义的容器+配置 | 一个机组 |
| volume(卷) | 容器外挂的持久化目录 | 外接硬盘 |
🆕 关于version字段(重要)
🚨
version:字段已废弃。
Compose v2+不再需要它,写了反而会收到警告:the attribute version is obsolete, it will be ignored。新写法直接以services:开头,本篇的compose.yaml就是正确姿势。
二、第0步:两个Dockerfile
🔧 Dockerfile.backend
FROM python:3.13-slim WORKDIR /app # pip 走国内镜像源加速(非国内网络可删除这行) ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple # 依赖先行单独一层:代码改动不用重装依赖 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 后端运行所需的目录逐个拷(见 .dockerignore 排除大目录) COPY alembic.ini ./ COPY app ./app COPY core ./core COPY collector ./collector COPY migrations ./migrations COPY main.py . EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]🔧 frontend/Dockerfile(开发模式,生产Nginx版后续篇章)
FROM node:24-alpine WORKDIR /app COPY package*.json . RUN npm install COPY . . CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]📌 三个细节
| # | 细节 | 说明 |
|---|---|---|
| ① | WORKDIR | 固定工作目录 |
| ② | 依赖先COPY再装 | 利用Docker层缓存——改代码不用重装依赖 |
| ③ | --host 0.0.0.0 | 让容器外可访问(默认只听localhost) |
💡镜像版本选择:
python:3.13-slim是当前稳定Python线,node:24-alpine是Active LTS(Node 26将在2026年10月28日进入Active LTS,届时可升级)。
三、第1步:compose.yaml——核心编排文件
compose.yaml(项目根,注意没有version:字段):
services:db:image:postgres:18-alpineenvironment:POSTGRES_USER:postgresPOSTGRES_PASSWORD:postgresPOSTGRES_DB:dailyvolumes:-pgdata:/var/lib/postgresql# 数据放卷里,容器删了数据还在healthcheck:# 健康检查:pg_isready才算"就绪"test:["CMD-SHELL","pg_isready -U postgres -d daily"]interval:5stimeout:5sretries:10start_period:10s# 给PG启动留缓冲,避免初期误判redis:image:redis:8-alpinehealthcheck:test:["CMD","redis-cli","ping"]interval:5stimeout:3sretries:5backend:build:./backendenvironment:DATABASE_URL:postgresql+psycopg://postgres:postgres@db:5432/dailyREDIS_URL:redis://redis:6379/0# 主机名就是服务名!SECRET_KEY:${SECRET_KEY:-dev-secret}DEEPSEEK_API_KEY:${DEEPSEEK_API_KEY}depends_on:db:condition:service_healthy# 等db健康再启动redis:condition:service_healthy# 等redis健康再启动ports:["8000:8000"]volumes:-uploads:/app/uploads# 上传的封面图持久化frontend:build:./frontendports:["5173:5173"]volumes:pgdata:🎯 四个关键设计(全是新手必踩的坑)
| # | 设计 | 为什么重要 |
|---|---|---|
| ① | 服务名即主机名 | 容器网络里backend访问db用db,不是localhost——同一网络内的服务通过名字互相访问 |
| ② | depends_on + healthcheck双保险 | 只写depends_on不够(它只管启动顺序,不管“就绪”)——db的健康检查通过后backend才启动,否则后端一启动就报“连接被拒” |
| ③ | volume持久化 | pgdata卷挂在PG数据目录——docker compose down后数据还在;down -v才会删(第八节⑤) |
| ④ | 环境变量注入 | DATABASE_URL从本地开发的localhost换成db——同一份配置,宿主机视角和容器视角的“地址”不一样(第八节③) |
💡
start_period的作用:给PG启动留缓冲时间,在这段时间内健康检查失败不计数,避免PG还没起来就被判定为“不健康”。
四、第2步:迁移与种子数据自动化
容器启动后要跑Alembic迁移,再灌种子数据。backend服务的command覆盖:
backend:...command:>sh -c "alembic upgrade head && python seed.py && uvicorn app.main:app --host 0.0.0.0 --port 8000"启动顺序变成:建表 → 种子 → 起服务。Alembic的env.py从settings读DATABASE_URL,容器里注入的环境变量自动生效——迁移配置复用,零改动。
🆕Compose v5.3+的新选择:如果你用的是较新的Compose(v5.3.0,2026年7月发布),可以用**
pre_start生命周期**替代“sh -c串联”——它原生支持“主容器启动前先跑一个短命容器”,比用depends_on service_completed_successfully更干净。入门阶段先用sh -c串联足够,知道有这个方向即可。
五、第3步:一键起 + 常用命令
dockercompose up-d--build# 构建并后台启动(-d不占终端)dockercomposeps# 看四个服务的状态dockercompose logs-fbackend# 跟后端日志dockercompose down# 停止(保留数据卷)dockercompose down-v# 停止并删除数据卷(⚠️ 数据没了)🌐 浏览器访问
| 端 | 地址 | 说明 |
|---|---|---|
| 前端 | http://127.0.0.1:5173 | 页面正常加载 |
| 后端API | http://127.0.0.1:8000/api | 前端调用的地址 |
⚠️注意5173页面的API请求依然是
127.0.0.1:8000——因为浏览器在宿主机上,它看到的“地址”是宿主机视角;只有容器内互相访问时才用服务名。两个视角别混(第七节小结)。
六、验收清单
1. 一条命令:docker compose up-d--build→ 四个服务全部Running2.dockercompose ps显示db / redis / backend / frontend都在,且db/redis为healthy3. 浏览器打开http://127.0.0.1:5173 → 首页正常加载、登录、收藏全通4.dockercompose down → 再docker compose up -d(不带--build)→ 数据还在(pgdata卷生效)5. 把本地已装的PG/Redis停掉,全栈依然工作(全部在容器里)6. 换台“干净机器”(或删除所有旧容器)重跑第1步 → 依然全绿7. 全程无报错后提交Gitgitadd.gitcommit-m"Docker Compose:一键起全栈(PG+Redis+后端+前端)"七、常见报错:这6个,Compose上手的标配(重点!)
①service backend depends on db but db has no healthcheck
🔍 原因:depends_on里写了condition: service_healthy,但db服务没配healthcheck,或格式写错。
✅ 解法:给db补上healthcheck(第3节写法),test用pg_isready最稳。
② backend一启动就报could not connect to server: Connection refused
🔍 原因:两个可能——
- ①
DATABASE_URL里写的是localhost(容器内没有PG!); - ②
depends_on没配condition,backend比db先起。
✅ 解法:地址用服务名db;depends_on加condition: service_healthy。
③port is already allocated(5432已被占用)
🔍 原因:宿主机本地也装了PostgreSQL,端口冲突。
✅ 解法:映射改宿主侧端口5433:5432(容器内不变)——宿主端口冲突改冒号左边,服务名不用动。
④ 前端页面能开,但所有API请求失败
🔍 原因:前端代码里API地址写死了localhost且端口没映射 / 或vite proxy指向了容器内才存在的地址。
✅ 解法:浏览器视角用127.0.0.1:8000(映射过即可);容器视角才用服务名——两个视角的地址表,写代码时贴在显示器上。
⑤docker compose down -v之后数据全没了
🔍 原因:-v会删除声明过的卷——PG数据、上传的图片,全没。
✅ 解法:把down -v当“格式化硬盘”看待,确认要重建才用;日常一律down(不带-v)。
⑥ 改了代码但容器里还是旧版本
🔍 原因:镜像没重新构建(up -d不重建);或后端没挂载代码卷(改了不生效)。
✅ 解法:代码变更 →docker compose up -d --build(重新构建镜像);或开发期用volume挂载源码(bind mount)。
八、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 补全健康检查:给backend也加healthcheck | ⭐⭐ | curl -f localhost:8000/docs |
| 2 | .env化敏感配置:把SECRET_KEY等接入.env文件 | ⭐⭐ | compose自动读取,且.env进gitignore |
| 3 | Makefile化命令:写一个Makefile封装make up/make down/make logs | ⭐⭐⭐ | 团队里“连docker命令都不用记” |
| 4(选做) | bind mount开发:把backend源码挂载进容器(- ./backend:/app) | ⭐⭐⭐ | 体验“改代码即热更新”的开发模式 |
📦 配套代码
两个Dockerfile + compose.yaml + 种子脚本已上传Git(python_daily/):【gitee仓库地址】