.env该有多少配置、Docker 起不来怎么用原生进程兜底、日志怎么做到"出问题时一个 ID 定位"。
「AI Agent 工程化实战」系列 · 11 项目源码:**Ticnix/weather-travel-recommend-system**基于气象大数据的出行推荐系统,AI Agent 全栈项目。FastAPI + PostgreSQL(TimescaleDB/pgvector/PostGIS) + Redis; LangGraph + MCP + Skill + RAG,对接 DeepSeek API;React/Vue 前后端分离,实现 3D 天气可视化、智能出行穿搭推荐。 (项目仍在更新中)
TL;DR
- 配置只有一个
.env,但必须守住三条线:Key 不进仓库、未配 Key 不阻断启动、容器内的地址归 compose 管。项目用.env(开发)+.env.prod.example(模板)+ compose 的environment覆盖(容器网络地址)三段式解决。 - "上线"应该是一条命令:备份 → 停应用 → 拉镜像 → 启动 → 健康检查 → 失败自动回滚。回滚不需要重新拉镜像——旧版本镜像就在本地,所以是秒级的。
- 日志要能"用一个 ID 串起一次请求":
contextvars生成 request_id、贯穿所有 service 层日志、输出 JSON 单行、指标按路由模板聚合(不是真实 path)。
目录
- 一、先看那条"一条命令上线"的脚本
- 二、配置管理:一个 .env 里的三条线
- 三、Docker 起不来怎么办:原生进程兜底
- 四、日志:一个 request_id 串起一次请求
- 五、CI:坏代码进不了主干
- 六、小结与下一篇
一、先看那条"一条命令上线"的脚本
deploy.sh 的头部注释把设计意图写得很清楚,我直接引用:
<span># deploy.sh —— 一条命令完成上线:</span>
<span># 备份数据库 → 停应用 → 拉取镜像 → 启动 → 健康检查 → 失败自动回滚</span>
<span>#</span>
<span># 用法:</span>
<span># ./deploy.sh # 部署主干最新版(latest)</span>
<span># IMAGE_TAG=sha-4c3abe9 ./deploy.sh # 部署指定版本</span>
<span># IMAGE_TAG=sha-1234567 ./deploy.sh # 同一条命令就是回滚:换个旧 tag 再跑</span>
<span>#</span>
<span># 退出码:</span>
<span># 0 部署成功</span>
<span># 1 部署失败(已自动回滚到上一版本)</span>
<span># 2 部署失败且回滚也失败 —— 需要人工介入,此时数据库已有部署前备份</span>
注意"同一条命令就是回滚" —— 这是个很聪明的设计:不写单独的 rollback 脚本,而是把"部署哪个版本"变成参数。少一个脚本,少一处可能忘记维护的逻辑。
三个值得抄的决策
① 备份放在"停应用之后、换镜像之前"。
<span># 设计说明:</span>
<span># - 备份放在「停应用之后、换镜像之前」:应用停止后数据不再变化,</span>
<span># pg_dump 拿到的是一致性快照;停机窗口本来就等于部署窗口,不额外损失</span>
"应用停止后数据不再变化" —— 这句是备份正确性的前提。如果边写边 dump,拿到的快照可能跨在两次事务中间。
② 备份要校验"真的能用",不只检查文件存在。
<span># 校验备份真的可用:非空 + gzip 完整性。防止「备份了个寂寞」后继续往下走</span>
[ -s <span>"<span>$BACKUP_FILE</span>"</span> ] || fail <span>"备份文件为空,中止部署"</span>
gzip -t <span>"<span>$BACKUP_FILE</span>"</span> || fail <span>"备份文件损坏(gzip 校验未通过),中止部署"</span>
"防止『备份了个寂寞』" —— 磁盘满或 pg_dump 中途挂了,会留下一个 0 字节或截断的文件。不校验的话,你会在最需要它的那一刻才发现它没用。
③ 回滚不重新 pull。
<span># - 回滚不重新 pull:旧版本的镜像在本地一定存在(刚才就是它跑着的),</span>
<span># 所以回滚不需要网络,秒级完成</span>
<span># - 数据库/Redis 容器全程不重启,业务数据卷不受影响</span>
整个流程是这样:
flowchart TB
S["开始部署"] --> A["记录当前版本<br/>.last_deployed_tag"]
A --> B["停应用容器<br/>postgres/redis 不动"]
B --> C["pg_dump 备份<br/>+ 校验 + 清理过期"]
C --> D{"备份可用吗"}
D -->|"否"| X1["中止部署<br/>数据未受影响"]
D -->|"是"| E["拉取新镜像"]
E --> F["启动新版本"]
F --> G{"健康检查<br/>三个入口"}
G -->|"通过"| Y1["写入版本记录<br/>退出码 0"]
G -->|"失败"| H{"有旧版本吗"}
H -->|"无(首次部署)"| X2["如实报告<br/>退出码 2"]
H -->|"有"| I["用旧 tag 重启<br/>不重新拉取"]
I --> J{"再次健康检查"}
J -->|"通过"| Y2["已回滚·退出码 1"]
J -->|"失败"| X2
style Y1 fill:#E1F5EE,stroke:#0F6E56,color:#04342C
style Y2 fill:#FAEEDA,stroke:#854F0B,color:#633806
style X1 fill:#FCEBEB,stroke:#A32D2D,color:#501313
style X2 fill:#FCEBEB,stroke:#A32D2D,color:#501313
style G fill:#E6F1FB,stroke:#185FA5,color:#0C447C
style J fill:#E6F1FB,stroke:#185FA5,color:#0C447C
style D fill:#E6F1FB,stroke:#185FA5,color:#0C447C
style H fill:#E6F1FB,stroke:#185FA5,color:#0C447C
一个小功能:让回滚流程可以被演练
健康检查函数有个"强制失败"开关:
<span># 参数 $1:是否强制判定失败("1"=演练用)。注意只有「新版本」的检查受它影响,</span>
<span># 回滚后的检查永远真实探测——否则演练时无法验证"回滚后服务真的恢复了"。</span>
<span><span>check_health</span></span>() {
<span>local</span> force_fail=<span>"<span>${1:-0}</span>"</span>
...
<span>if</span> [ <span>"<span>$force_fail</span>"</span> != <span>"1"</span> ] && curl -fsS ... ; <span>then</span>
用法是 FORCE_HEALTHCHECK_FAIL=1 ./deploy.sh。
注意括号里那句:强制失败只作用于"新版本"的检查,回滚后的检查永远真实探测。否则演练就只能演练到"我以为回滚成功了"——这半句才是这个功能真正的价值。
而且首次部署失败时,脚本不假装回滚:
<span>if</span> [ <span>"<span>$OLD_TAG</span>"</span> = <span>"未知"</span> ]; <span>then</span>
<span># 首次部署没有"上一版本"可回滚——如实报告,不假装恢复</span>
<span>printf</span> <span>'[deploy][错误] 这是首次部署且健康检查失败,没有旧版本可回滚。\n'</span> >&2
...
<span>exit</span> 2
<span>fi</span>
"如实报告,不假装恢复" —— 退出码 2 的含义就是"需要人工介入",跟退出码 1(已自动恢复)区分开。
二、配置管理:一个 .env 里的三条线
项目的环境变量分三处,各自职责明确:
| 文件 | 职责 | 是否入库 |
|---|---|---|
| `backend/.env` | 本地开发全部配置 | ❌ 被 `.gitignore` 忽略 |
| `backend/.env.prod.example` | 生产模板(**只列会变的敏感项**) | ✅ 入库 |
| `docker-compose.yml` 的 `environment` | 容器网络地址覆盖 | ✅ 入库 |
第一条线:Key 不进仓库
.gitignore 里对应的规则很直接:
.venv/
.env
# 各环境配置(.env.prod / .env.dev 等):敏感值一律不入库,参考 *.example
.env.*
*.log
注释里那句"参考 *.example"是关键的配套动作:光忽略不够,还得给一个模板,否则新人不知道该配哪些项。
模板文件自己就说了这件事:
<span># ============================================================</span>
<span># 生产环境配置示例</span>
<span>#</span>
<span># 使用方式:</span>
<span># 1. 复制本文件为 backend/.env.prod,填入真实值</span>
<span># (.env.* 已被 .gitignore 忽略,真实 Key 不会进入仓库)</span>
<span># 2. 部署时指定:</span>
<span># ENV_FILE=./backend/.env.prod ./deploy.sh</span>
<span># ============================================================</span>
于是 deploy.sh 和 docker-compose.yml 之间就靠 ENV_FILE 这个变量串联:
<span>env_file:</span>
<span># 部署时可指定环境配置:ENV_FILE=./backend/.env.prod ./deploy.sh</span>
<span># 本地开发默认使用 backend/.env</span>
<span>-</span> <span>${ENV_FILE:-./backend/.env}</span>
${ENV_FILE:-./backend/.env} 是 compose 的默认值语法:不传就用开发配置,传了就用生产的。一行搞定两套环境。
第二条线:未配 Key 不阻断启动
.env 里的注释写得非常明确:
<span># ===== DeepSeek(Key 仅存后端,勿提交 git / 勿暴露前端)=====</span>
<span># 请填入你自己的 Key;留空时 AI 相关功能不可用,其余模块不受影响</span>
DEEPSEEK_API_KEY=
README 里也是同一条承诺:
复制
backend/.env并填写需要的 Key(详见下节)。未配置 Key 的功能会自动降级,不会导致启动失败。
这个约定配合第 09 篇的四层降级才有意义 —— 降级做扎实了,"缺 Key"就只是"某个功能不可用",而不是"整个服务起不来"。降级设计在这里从"容错"变成了"可配置性"的基础。
第三条线:容器内的地址归 compose 管
这是最容易出错的一条。.env 里写的是本地开发地址:
DB_URL=postgresql+asyncpg://admin:123456@127.0.0.1:5432/weather_db
REDIS_URL=redis://127.0.0.1:6379/0
但在容器里 127.0.0.1 指的是容器自己。所以 compose 用 environment 段覆盖:
<span>environment:</span>
<span># 容器内需用 compose 服务名互访,覆盖 .env 中本地开发的 127.0.0.1</span>
<span>DB_URL:</span> <span>postgresql+asyncpg://admin:123456@postgres:5432/weather_db</span>
<span>REDIS_URL:</span> <span>redis://redis:6379/0</span>
而 .env.prod.example 里刻意不放这两个值,模板头部解释了原因:
<span># 与开发的区别:只放「会变的敏感配置」。数据库/Redis 连接串</span>
<span># 由 docker-compose.yml 的 environment 段覆盖为容器网络地址,</span>
<span># 不需要(也不应该)写在这里。</span>
"不需要,也不应该" —— 两套环境连的是不同主机,这个差异归编排层管;.env 只管"会变的敏感值"。
三段式的价值在于每一层只有一个职责:模板告诉你缺什么,
.env放你的真实值,compose 管拓扑差异。混在一起就会出现"我明明改了.env为什么不生效"这种无从排查的问题。
三、Docker 起不来怎么办:原生进程兜底
这是很多个人项目的真实处境(我的机器就是缺 CPU 虚拟化支持,Docker Desktop 起不来)。项目有一组 PowerShell 脚本做兜底。
场景一:改一行文案,不想等镜像重建
dev-user.ps1 的注释直指痛点:
# 为什么需要它:改一行文案就要重建镜像 + 重跑测试时,一轮十几分钟。
# vite dev 是热更新——改完保存、浏览器刷新即可,秒级;而且开发模式
# 不启用 Service Worker,不会出现"改了没生效、其实是缓存"的经典坑。
#
# 两个地址的分工:
# 5173 开发用:热更新,改完即见(不跑测试、不用重建镜像)
# 8080 正式用:Docker 镜像里构建好的静态包,只在你验收/发布时才重建
"改了没生效,其实是缓存" —— Service Worker 缓存是前端调试的经典陷阱。注释里点明"开发模式不启用 SW",把这条经验固化了。
脚本还顺手解决了 Node 版本问题:
# 挑一个 Node 20+:优先 PATH,其次工具自带的多版本目录
...
if (-not $node) {
Write-Host '未找到 Node 20+(Vite 8 跑不了 Node 18),请先安装 Node 20 或 22' -ForegroundColor Red
exit 1
}
"Vite 8 跑不了 Node 18" —— 与其让 Vite 抛一个难懂的错,不如启动前先检查版本、给一句人话。
场景二:Celery 在 Windows 上直接跑不起来
这是个高频坑。start_celery.ps1 的解法是多加一个 -P solo:
Start-Process -FilePath ".\venv\Scripts\python.exe" `
-ArgumentList "-m", "celery", "-A", "app.celery_app", "worker", "-l", "info", "-P", "solo" `
-P solo 是 Celery 的单进程执行池。Windows 上默认的 prefork 池依赖 fork(),而 Windows 没有 fork——不加这个参数会直接起不来。
README 里也留了同一句提醒:
<span># Celery worker(Windows 需加 -P solo)</span>
celery -A app.celery_app worker -l info -P solo
场景三:想给别人看,但没服务器
start-public.ps1 用 Cloudflare 免费隧道把本机端口暴露到公网:
# 原理:项目用 docker compose 跑在本机,再通过 Cloudflare 免费隧道
# (cloudflared quick tunnel)把本机端口暴露到公网。
# 无需服务器、无需备案、无需公网IP。
评论区里两个细节体现了安全意识:
# 3. 管理端默认不对外暴露(含 admin 账号,暴露到公网有风险)
# 如需暴露,执行 .\start-public.ps1 -ExposeAdmin
默认安全,需要时显式打开 —— 这是个好默认值。
还有一个探活判据的选择:
# 用真实后端接口探活:前端 nginx 有 SPA 回退,访问 /health 也会返回 200,不能作判据
$r = Invoke-WebRequest -Uri "http://localhost:$UserPort/api/v1/weather/current" ...
"前端 nginx 有 SPA 回退,访问 /health 也会返回 200" —— 这是很隐蔽的一个坑:用前端地址探活会永远"成功",因为任何路径都被 SPA 回退成 index.html 了。必须探一个真实的后端接口。
四、日志:一个 request_id 串起一次请求
第 03 篇讲过 contextvars 做用户隔离。同一个机制在这里用于可观测性——而且这次它解决的问题更直观。
observability.py 开头把三件事说得清清楚楚:
<span>"""可观测性基础设施:请求 ID 贯穿、结构化日志、轻量指标。
三个部件,解决三个问题:
1. **request_id 贯穿**
每个请求生成唯一 ID(或继承上游的 X-Request-ID),通过 contextvars
在整个调用链传播——包括所有 service 层日志。排查问题时「用一个 ID
就能串起一次请求的全部日志」,不用再靠时间戳猜。
2. **结构化日志**
日志输出为 JSON 单行(字段固定:ts/level/logger/msg/request_id +
调用方通过 extra 传入的业务字段),便于日志采集与检索。
3. **轻量指标(/metrics)**
进程内计数器:按路由统计请求数 / 错误数 / 总耗时。
不引入 Prometheus 等依赖——当前「看调用量与错误率」的需求,
几十行代码即可满足,等真有可视化需求时再升级采集方式,对外接口不变。
"""</span>
"不用再靠时间戳猜" —— 这句话就是全部动机。没有 request_id 时,排查靠"大概那个时间点附近的日志",有并发请求就混乱了。
怎么做到"所有日志都带 request_id"
关键是 logging.Filter——它能在每条日志落盘前注入字段:
<span>class</span> <span>RequestIdFilter</span>(logging.Filter):
<span>"""把 request_id 注入每条日志记录,供 Formatter 取用。"""</span>
<span>def</span> <span>filter</span>(<span>self, record: logging.LogRecord</span>) -> <span>bool</span>:
record.request_id = request_id_var.get()
<span>return</span> <span>True</span>
这就绕开了"每个 logger.info() 都要手动带 request_id"的麻烦 —— 业务代码完全无感,只要它跑在请求上下文里,日志就自带 ID。
JSON 单行日志:extra 字段走白名单
<span>class</span> <span>JsonFormatter</span>(logging.Formatter):
<span>"""JSON 单行日志。
extra 字段走白名单:业务代码通过 `logger.info("...", extra={"path": ...})`
传业务字段,这里只输出白名单内的键——否则 logging 内部自带的
几十个属性(funcName/lineno/msecs...)会把每行日志撑爆。
"""</span>
_EXTRA_KEYS = (<span>"method"</span>, <span>"path"</span>, <span>"route"</span>, <span>"status"</span>, <span>"duration_ms"</span>,
<span>"task_id"</span>, <span>"task"</span>, <span>"retries"</span>)
"否则会把每行日志撑爆" —— 如果直接把 record.__dict__ 全序列化,每行会带上几十个 logging 内部属性(funcName、lineno、msecs…),日志体积翻好几倍。
有意思的是这个白名单里同时有 task_id / retries——HTTP 请求的字段和 Celery 任务的字段共用一份 formatter,所以第 09 篇那个 task_failure 告警的结构化日志是直接能用的。
指标按"路由模板"聚合,不是真实 path
这是整个模块里我觉得最见功力的一处:
<span># 用路由模板而不是真实 path 聚合指标——</span>
<span># 否则 /news/84、/news/85 会被聚成两条,指标失去意义</span>
route = <span>getattr</span>(request.scope.get(<span>"route"</span>), <span>"path"</span>, request.url.path)
如果没有这一行,/api/v1/news/84、/api/v1/news/85……每一个 ID 都会变成一条独立的指标记录。指标面板上会有几千条"只被访问过一次"的路由,完全失去统计意义。
而 request.scope["route"].path 拿到的是路由模板(/api/v1/news/{news_id}),所以所有同类请求会聚到一条上。
还有个容易忽略的细节——为什么不用 uvicorn 自带的 access log:
<span>def</span> <span>setup_logging</span>() -> <span>None</span>:
<span>"""...
关闭 uvicorn.access:请求日志由我们的中间件输出(带 request_id/耗时/路由),
两份 access log 内容重复只会干扰检索。
"""</span>
logging.getLogger(<span>"uvicorn.access"</span>).disabled = <span>True</span>
配合 compose 里的启动参数:
<span># --no-access-log:请求日志由应用中间件输出(含 request_id/耗时/路由模板),</span>
<span># uvicorn 自带的 access log 与其重复且无 request_id,只会在日志里制造噪音</span>
<span>command:</span> <span>uvicorn</span> <span>app.main:app</span> <span>--host</span> <span>0.0</span><span>.0</span><span>.0</span> <span>--port</span> <span>8000</span> <span>--no-access-log</span>
两处都要动:代码里 disabled = True + 启动参数 --no-access-log。只做一处的话,另一路日志还是会冒出来。
最后还有一个 Windows 专属的小坑:
<span># Windows 下 stdout/stderr 重定向到文件时默认用本地编码(GBK),</span>
<span># JSON 里的中文会变成一堆问号;Linux 容器本来就是 UTF-8,reconfigure 无副作用</span>
<span>for</span> stream <span>in</span> (sys.stdout, sys.stderr):
<span>if</span> <span>hasattr</span>(stream, <span>"reconfigure"</span>):
<span>with</span> contextlib.suppress(Exception):
stream.reconfigure(encoding=<span>"utf-8"</span>)
"JSON 里的中文会变成一堆问号" —— 而 contextlib.suppress 是必要的防御:不是所有流都支持 reconfigure。
五、CI:坏代码进不了主干
ci.yml 的三个层次,注释里表述得很精炼:
<span># 分三个层次,由快到慢、由便宜到贵:</span>
<span># 1. lint 几秒钟 —— 格式、未使用变量、明显的坏味道</span>
<span># 2. test 几分钟 —— 442 个用例(后端 379 + 前端单测 41 + E2E 由本地/部署后跑)</span>
<span># 3. build 几分钟 —— 四个镜像能不能构建出来</span>
<span>#</span>
<span># 为什么 lint 和 test 拆成不同 job:</span>
<span># lint 只要 10 秒,test 要 2 分钟。拆开后 lint 失败会立刻反馈,</span>
<span># 不用等测试跑完;而且两者并行执行,总耗时取较慢的那个。</span>
"lint 失败会立刻反馈" —— 把快的放前面单独成 job,不是为了省时间(并行跑总时长取最慢的那个),而是为了让人更快知道最蠢的那类错误。
发布和回滚的闭环
<span>release:</span>
<span>needs:</span> [<span>backend-lint</span>, <span>backend-test</span>, <span>frontend</span>]
<span>if:</span> <span>github.event_name</span> <span>==</span> <span>'push'</span>
<span>permissions:</span>
<span>contents:</span> <span>read</span>
<span>packages:</span> <span>write</span>
needs 保证"测试没过的代码不配被发布"。然后标签策略是回滚能力的基础:
<span># 标签策略(可回滚的关键):</span>
<span># 应用镜像 → latest(始终指向主干最新)+ sha-<短hash>(与提交一一对应)</span>
<span># 回滚时只要用旧提交的 sha-xxx tag 重新执行 deploy.sh 即可。</span>
<span># postgres 镜像随 PG 版本走(pg17),不随应用版本变化,用固定 tag。</span>
sha-<短hash> 是回滚的锚点。 只有 latest 的话,你无法回到"上一个具体版本"。
几个实用的 CI 细节
① 失败的日志要打进注解区,因为完整日志需要登录。
<span># 失败时把首尾日志输出成注解:pytest 的"用法错误"(exit 4) 会把原因</span>
<span># 写在最前面,而"测试失败"的信息在最后面,两头都取才够定位。</span>
<span>head</span> <span>-12</span> <span>pytest-output.log</span> <span>|</span> <span>while</span> <span>IFS=</span> <span>read</span> <span>-r</span> <span>line;</span> <span>do</span>
<span>echo</span> <span>"::error::[头部] $line"</span>
<span>done</span>
"两头都取" —— 这个观察很准:pytest 退出码 4(用法错误)的信息在最前面,而用例失败的信息在最后面。只 tail 会漏掉前者。
② 连发多次推送时,取消上一次没跑完的流水线。
<span>concurrency:</span>
<span>group:</span> <span>ci-${{</span> <span>github.ref</span> <span>}}</span>
<span>cancel-in-progress:</span> <span>true</span>
<span># 好处:省额度,且不会出现"旧提交的检查结果覆盖新提交"的困惑</span>
③ 前端的 pre-commit 钩子不能用 npm 命令。
<span># 用本地 Python 脚本而不是直接写 npm 命令,原因见脚本注释:</span>
<span># Windows 下 npm 实际是 npm.cmd,pre-commit 按字符串找可执行文件会找不到。</span>
"npm 实际是 npm.cmd" —— pre-commit 按可执行文件名查找,找不到 .cmd。这类平台差异只能靠踩坑积累。
六、小结与下一篇
可复用清单
| 场景 | 做法 |
|---|---|
| 环境配置 | 三段式:`.env.example` 模板 + `.env` 真实值(gitignore)+ compose 覆盖拓扑差异 |
| 敏感值 | `.gitignore` 里忽略,但**必须给模板文件**,否则新人不知道配什么 |
| 缺配置 | 未配 Key 应**降级**而非阻断启动(前提是降级做扎实了) |
| 上线脚本 | 备份 → 停应用 → 拉镜像 → 启动 → 健康检查 → 失败自动回滚 |
| 备份时机 | 放在**停应用之后**,数据不再变化时 dump 才是一致性快照 |
| 备份校验 | 非空 + 压缩完整性,**别只检查文件存在**——防"备份了个寂寞" |
| 回滚 | 不重新拉镜像(旧镜像在本地),秒级完成;换 tag 就是回滚 |
| 演练 | 提供一个"强制失败"开关,但**回滚后的检查必须真实探测** |
| 难懂的错误 | 启动前检查前置条件(Node 版本等),给一句人话而不是让框架抛异常 |
| Windows + Celery | worker 必须加 `-P solo`(默认 prefork 依赖 `fork()`) |
| 前端探活 | 别探前端地址——SPA 回退会让任何路径都返回 200,要探真实后端接口 |
| 请求日志 | `contextvars` + `logging.Filter` 自动注入 request\_id,业务代码无感 |
| JSON 日志 | `extra` 字段走白名单,不要全序列化(logging 内部属性会撑爆日志) |
| 指标聚合 | 用**路由模板**(`/news/{id}`)而不是真实 path,否则指标全是长尾 |
| access log | 代码里 disable + 启动参数 `--no-access-log`,**两处都要改** |
| 镜像标签 | `latest` + `sha-<短hash>` 双标签,后者是回滚的锚点 |
| CI 分层 | 快的单独成 job 先反馈;失败日志打注解(完整日志要登录才能看) |
下一篇预告
运维这条线还能再往下走一层:数据库迁移与数据演化。项目用了 Alembic,但表结构里还有 TimescaleDB 超表、PostGIS 地理字段、pgvector 向量列这些"非标准 DDL"——它们怎么跟迁移脚本配合?改字段时历史数据怎么办?以及"测试库为什么每次都要重建"这个决定的真实代价。要的话我接着写,还是这个体量。
参考资料
- Docker Compose 官方文档 · Environment variables ——
${VAR:-default}默认值语法、env_file与environment的优先级关系 docs.docker.com/compose/env… - Cloudflare 官方文档 · TryCloudflare —— 免账号的临时隧道(quick tunnel),域名每次变化 developers.cloudflare.com/cloudflare-…
- Celery 官方文档 · Workers 的
--pool参数 ——solo池的适用场景(Windows 无fork()) docs.celeryq.dev/en/stable/u… - Python 官方文档 ·
contextvars——ContextVar.set/reset与 asyncio 的上下文隔离 docs.python.org/3/library/c… - Python 官方文档 ·
logging.Filter—— 在日志落盘前修改LogRecorddocs.python.org/3/library/l… - GitHub Actions 官方文档 · Workflow 语法 ——
concurrency取消并发运行、needs依赖、::notice::与::error::注解 docs.github.com/en/actions/… - 项目源码(仍在更新中) —— Ticnix/weather-travel-recommend-system github.com/Ticnix/weat…
可复用清单式总结,适合想给个人或小团队项目补上部署、回滚与可观测性短板的开发者,尤其推荐 Docker 起不来、排查日志靠猜时间戳的同学对着逐条落地。