--- id: 20260814-docker-postgresql-gitea-same-host-deployment title: Docker Compose、外部 PostgreSQL 与 Gitea 同机部署需要显式拓扑契约 created: 2026-08-14 updated: 2026-08-14 status: promoted scope: global category: deployment confidence: medium last_verified: 2026-08-14 promotion_target: skill projects: - zhixing-system tags: - docker-compose - postgresql - gitea-actions - self-hosted-runner - single-host-deployment --- # Docker Compose、外部 PostgreSQL 与 Gitea 同机部署需要显式拓扑契约 ## Trigger 创建新的个人 Web 项目,并准备继续采用 Docker、PostgreSQL、Gitea Actions 与单机反向代理组合;或者需要从已有项目复制部署文件,但不确定哪些内容是通用骨架、哪些内容绑定当前服务器或业务 Job。 ## Context `zhixing-system` 使用前后端分离源码、Docker Compose 编排和 Gitea Actions 部署。开发环境由 Compose 启动项目专属 PostgreSQL;生产环境复用 1Panel 管理的 PostgreSQL,通过外部 Docker 网络连接。Gitea Runner 在目标 Docker 主机上检出代码、原地构建镜像并更新同一个 Compose 项目,不经过镜像仓库。 这套方案适合低运维成本的单机个人项目,但真正可复用的不是项目名和端口,而是以下契约:开发与生产数据库拓扑分离、生产配置失败即停止、常驻服务与一次性 Job 分离、容器身份与数据卷权限匹配、内部服务不直接暴露公网、部署流程必须验证配置和健康状态。 ## Evidence - 2026-08-14 检查 `zhixing-system/docker-compose.dev.yml`:开发 Compose 包含 `postgres:16-alpine`、数据库健康检查、持久卷、后端和前端热更新卷;`migrate` 与 `market-sync` 放在 `jobs` profile 中。 - 2026-08-14 检查 `zhixing-system/docker-compose.prod.yml`:生产 Compose 不启动 PostgreSQL,后端与 Job 通过外部 `1panel-network` 访问数据库;`ZHIXING_DATABASE_URL` 使用 Compose 的 `${VAR:?message}` 形式禁止回退到开发数据库;Web 仅绑定 `127.0.0.1:8111`。 - 2026-08-14 检查 `zhixing-system/.gitea/workflows/deploy-production.yaml`:`main` push 或手动触发后,执行 checkout、两种 profile 的 Compose 校验、原地 build/up、迁移、Web 健康检查,并在成功或失败时执行 `docker compose ps`。 - 2026-08-14 检查 `zhixing-system/zhixing-server/Dockerfile` 与 `zhixing-system/zhixing-web/Dockerfile`:开发/生产多阶段构建;生产后端使用 UID 10001 非 root 用户;前端以 Nginx 提供静态文件,并在容器启动时注入后端 upstream;两个镜像都包含健康检查。 - Git 提交 `42942af` 将生产 Web 端口从所有地址改为只绑定 loopback,并删除完成使命的 Runner smoke-test workflow。 - Git 提交 `afdc5ca` 将生产内置 PostgreSQL 改为 1Panel 外部 PostgreSQL,增加 Gitea secrets、生产连接串强制校验、Job profile 校验和数据库迁移步骤。 - Git 提交 `eabf102` 证明镜像构建阶段对目录 `chown` 不足以覆盖运行时新建的 named volume;项目增加 root 身份的 `market-data-init` 一次性容器,将卷目录归属修正为 UID 10001。 - Git 提交 `9023e00` 在 Debian 与 Alpine 生产镜像中安装时区数据,同时通过镜像和 Compose 显式统一 `Asia/Shanghai`。 - 使用无敏感值占位连接串实际执行以下四项配置校验,退出码均为 0:开发默认 profile、开发 `jobs` profile、生产默认 profile、生产 `jobs` profile;命令均为 `docker compose --env-file /dev/null -f [--profile jobs] config --quiet`。 - 中央知识库在写入前搜索 `docker`、`compose`、`postgresql`、`gitea` 等关键词,未发现同根因条目。 ## Root cause 已验证: - 开发与生产的 PostgreSQL 拓扑不同。开发依赖 Compose 内部服务名 `postgres`;生产依赖预先存在的外部网络、稳定数据库网络别名和单独管理的凭据。试图让同一份默认连接串覆盖两者,会把开发便利性带入生产并掩盖配置错误。 - Docker 镜像内的目录所有权与运行时 named volume 的所有权不是同一件事。卷首次挂载时可能由 root 初始化,导致非 root 应用无法写入;需要显式的卷初始化或宿主机预置步骤。 - Gitea 同机部署依赖稳定的 `COMPOSE_PROJECT_NAME` 维持容器、网络和 named volume 身份,而不是依赖 Runner 每次 checkout 的绝对路径。 - `depends_on.condition` 只有在依赖服务定义了可用的 healthcheck 或一次性 Job 能正确返回退出码时才有意义。 - 只把生产入口绑定到 loopback,可以让 1Panel/Nginx/Caddy 承担公网 TLS 与路由,避免应用容器端口绕过反向代理直接暴露。 推断: - 当前 Gitea workflow 先 `up -d --build`、后执行迁移。若未来应用启动依赖新 schema,或者迁移包含破坏性变更,这个顺序可能让新容器先进入不兼容状态。新项目必须显式选择“先迁移再切换”或 expand/contract 迁移,而不是机械复制当前顺序。 - 当前 workflow 没有在仓库内表达部署串行化、自动回滚、数据库备份、镜像留档或 Runner 配置。单 Runner、低流量个人项目可能暂时接受,但这些能力不能被误认为已由 Gitea/Compose 自动提供。 - 当前直接在目标主机构建镜像,部署简单,但缺少不可变镜像版本和快速回滚点;当构建变慢、主机增多或可用性要求提高时,应改为构建并推送带提交 SHA 的镜像,再由生产 Compose 拉取指定版本。 ## Preferred action 新建同类型项目时,先复制结构,再替换项目参数,不要直接复制当前业务变量: 1. 建立 `Dockerfile` 的 development/production target;依赖使用 lockfile 固定,生产阶段只包含运行依赖,服务进程使用固定的非 root UID,并为每个容器提供不依赖额外调试工具的健康检查。 2. 保留独立的 `docker-compose.dev.yml` 与 `docker-compose.prod.yml`。开发 Compose 可以内置 PostgreSQL、源码热更新卷和有边界的本地默认凭据;生产 Compose 不应包含开发凭据或隐式数据库 fallback。 3. 生产 PostgreSQL 通过外部网络接入时,显式声明 `external: true`,使用稳定网络别名,不引用易变化的容器实例名;连接串由 Gitea secret 注入,并用 `${DATABASE_URL:?Set ...}` 在配置展开阶段失败。 4. 把 migration、seed、backup、cron task 等一次性命令建模为 Compose profile 下的 Job。Job 通过退出码报告结果,不在常驻 Web 进程内部偷偷执行迁移或调度。 5. named volume 由非 root 服务写入时,增加幂等的 init Job,或在首次部署 runbook 中显式预置 UID/GID;不能只依赖 Dockerfile 中对镜像目录的 `chown`。 6. Web 容器只绑定 `127.0.0.1:`,由宿主机反向代理统一处理公网域名与 TLS;浏览器使用同源 `/api`,Nginx upstream 通过运行时环境变量指向 Compose 后端服务名。 7. Gitea workflow 至少包含:checkout、默认与 Job profile 的 `docker compose config --quiet`、build、迁移策略、`up -d --remove-orphans`、应用级健康检查、`if: always()` 的服务状态输出。固定 `COMPOSE_PROJECT_NAME`,并明确 Runner 必须运行在目标主机且具备所需 Docker 权限。 8. 在第一个会修改 schema 的版本之前确定发布顺序:低停机项目可在维护窗口执行备份、迁移、启动、健康检查;需要连续可用时使用向后兼容的 expand/contract migration。任何顺序都必须提供失败后的数据库与应用回退说明。 9. 为同一生产环境增加部署串行化,并记录可回滚版本。若继续目标主机本地构建,至少保留上一个 Git commit 和镜像 tag;若改用镜像仓库,则以 commit SHA 标记并部署固定 tag。 10. 首次部署 runbook 单独检查:外部网络存在、数据库别名可解析、数据库/用户已创建、密码已 URL 编码、TLS 模式与服务器一致、迁移可执行、volume 权限正确、反向代理只转发 loopback 端口、健康检查能从容器内和代理入口通过。 建议将未来模板参数化为:`PROJECT_NAME`、`COMPOSE_PROJECT_NAME`、`DATABASE_NAME`、`DATABASE_USER`、`DATABASE_HOST_ALIAS`、`EXTERNAL_NETWORK`、`WEB_LOOPBACK_PORT`、`TIMEZONE`、`BACKEND_HEALTH_PATH`。业务专用 token、同步 Job、数据目录和并发参数不进入通用模板。 ## Boundaries - 这套模式面向单机、单环境、低到中等流量的个人项目。多主机、高可用、蓝绿/金丝雀发布或受监管数据不应继续使用目标主机原地构建作为默认方案。 - `1panel-network`、`postgresql`、`8111`、`Asia/Shanghai`、UID 10001 和 `ZHIXING_*` 都是当前项目参数,不是全局约定。 - 生产 PostgreSQL 是否与应用同一台主机不是核心要求;核心是把数据库拓扑、TLS、凭据来源和网络边界显式化。远程数据库不需要 Docker external network。 - 时区是否使用 `Asia/Shanghai` 取决于业务契约。数据库时间戳仍应明确 UTC/带时区语义,不能仅靠容器 `TZ` 推断数据含义。 - healthcheck 只能证明探针覆盖的最小路径可用,不能替代迁移验证、关键业务 smoke test、数据库备份恢复演练和外部反向代理检查。 - 当前证据来自一个项目及其多次演进,尚未在第二个独立项目复用;在新项目完整验证前,不晋升为全局强制模板或自动生成 skill。 ## Failed approaches - 在生产 Compose 中保留内置 PostgreSQL 和开发默认连接串:后来改为外部数据库并用必填变量阻止错误回退。 - 将 Web 端口绑定到所有宿主机地址:后来收敛到 `127.0.0.1`,由反向代理作为唯一公网入口。 - 只在 Dockerfile 中创建并 `chown` 数据目录:运行时 named volume 仍可能由 root 初始化,最终需要单独的 init Job。 - 把一次性 Runner 能力检查长期保留在部署 workflow 集合中:验证完成后删除 smoke-test workflow,避免每次 push 运行无业务价值的检查。 - 把当前部署顺序视为通用答案:迁移与应用切换顺序必须由 schema 兼容契约决定。 ## Promotion record - 2026-08-14:按用户明确请求推广为单文件 Skill:`zhixing-system/bootstrap-docker-postgres-gitea/SKILL.md`。 - 验证:`python3 /Users/yuxuanhui/.codex/skills/.system/skill-creator/scripts/quick_validate.py /Users/yuxuanhui/bcc-github/quant-project/zgnb/zhixing-system/bootstrap-docker-postgres-gitea`,结果为 `Skill is valid!`。 - 当前仍只有 `zhixing-system` 的实战证据;Skill 保留单机个人项目边界和未验证项提示。待第二个项目复用并验证首次部署、升级、回退和数据库恢复后,再考虑提升为 `patterns/` 下的跨项目规范模式。