自建服务部署与运维经验:一些踩坑与总结(模板)

部署经验总结(Traefik + Docker Compose + 域名访问 + 共享中间件)

这篇文章总结了本仓库这次连续部署多个服务(dockhandvaultwardenCheckmateMiniflux(rss))的经验、踩坑点、排查手法,以及我个人的部署习惯(也是本仓库后续建议遵循的“默认做法”)。

目标与底线

  • 只允许域名访问:服务不对外暴露宿主机端口,避免 IP:端口 绕过认证/暴露管理面。
  • 统一入口:对外只开 80/443,由 Traefik 统一反代、签发证书、加安全头。
  • 共享基础中间件:MongoDB / Redis / PostgreSQL 这类通用组件集中维护、统一复用,避免每个应用重复起一套浪费资源。
  • Never break userspace(不破坏现有服务):所有变更优先考虑是否会影响已在线服务(路由冲突、端口冲突、网络冲突、认证方式冲突)。

目录组织与“我的部署习惯”

  • 每个应用一个目录:例如 dockhand/vaultwarden/Checkmate/v2/(Miniflux)。
  • 每个应用目录下有一个可直接运行的 docker-compose.yml:避免依赖上游仓库示例分散在 contrib/ 里不好维护。
  • 共享中间件单独一个项目:新增 middlewares/,集中跑 postgres/mongodb/redis
    • 共享中间件只加入内部网络:对外不暴露任何端口
    • 应用通过 external network 方式加入共享网络获取 DB/缓存能力。
  • Traefik 统一网络:所有需要被 Traefik 访问的业务容器都加入 traefik_proxy 外部网络。
  • 不滥用文件路由:优先使用 Docker labels(docker provider)。file provider 只用于少数“非容器化目标”或特殊转发。

标准部署模板(核心套路)

1)禁止宿主机端口暴露

对外服务统一做法:

  • expose:(仅容器网络可见)替代 ports:(宿主机可见)。
  • 由 Traefik 通过 label 路由到容器端口。

这能一次性解决:

  • 端口冲突(例如宿主机 3000 已被别的服务占用)
  • IP:端口绕过域名(安全风险)

2)Traefik labels(域名路由 + TLS + 安全头)

常规标签集合(示意):

  • traefik.enable=true
  • traefik.http.routers.<name>.rule=Host(`miniflux.example.me`)
  • traefik.http.routers.<name>.entrypoints=websecure
  • traefik.http.routers.<name>.tls.certresolver=letsencrypt
  • traefik.http.routers.<name>.middlewares=security-headers@file
  • traefik.http.services.<name>.loadbalancer.server.port=<container_port>

3)多网络容器必须指定 Traefik 网络

当容器同时加入:

  • traefik_proxy(给 Traefik 用)
  • infra_backend(给数据库/缓存用)

Traefik 可能选错 IP(选到它不在的网络),导致 504 Gateway Timeout

解决:在该容器 labels 增加:

  • traefik.docker.network=traefik_proxy

本次 Checkmate 就是典型案例:加了这条后,504 立即消失。

共享中间件(middlewares 项目)

设计原则

  • 高内聚低耦合:中间件只提供网络服务,不掺杂业务逻辑。
  • 最小权限:默认不对外暴露端口,只在 Docker 内部网络给应用用。
  • 数据持久化统一管理:卷在 middlewares 项目里集中维护(便于备份/迁移)。

经验要点

  • 共享网络使用固定名字(例如 infra_backend),各应用 external: true 加入。
  • 应用连接字符串里使用共享网络的服务名(例如 postgres/mongodb)。
  • 不同应用之间尽量使用不同的 DB / schema / user,避免互相污染数据。

应用级注意点(本次踩坑记录)

Dockhand(容器管理面,风险极高)

  • 必须禁用宿主机端口映射,只允许域名 + Traefik 访问。
  • 必须加认证:用 Traefik basicAuth 中间件保护。
  • 不建议再叠加复杂逻辑,越简单越不容易出事故。

Vaultwarden(Bitwarden 兼容服务)

这是最容易“自作聪明搞坏客户端”的服务:

  • 不要对整个站点加 BasicAuth:官方客户端/浏览器插件会直接不可用。
  • 正确做法:仅保护 /admin(管理后台):
    • //api/identity 等客户端路径必须无 BasicAuth 阻挡。
  • 遇到“插件能打开但登录失败”,先看:
    • Cloudflare 是否给 API 下发 challenge(见下文)
    • /api 是否被客户端当作 ServerConfig(必要时在 Traefik 做精确重写到 /api/config

Checkmate(多网络 + Mongo)

  • 共享 Mongo 后,容器加入多个网络会触发 Traefik 选错网络:
    • 直接导致 504
    • traefik.docker.network=traefik_proxy 一把解决

Miniflux(miniflux.example.me)

  • Miniflux 只支持 PostgreSQL,不需要 Mongo/Redis(除非你额外跑 RSSHub、Browserless 等)。
  • HEAD / 返回 405 是可接受的(GET 正常即可),不要把它误判成服务挂了。

Cloudflare 的坑:客户端/插件集体“无法登录”

这次出现过两次同源问题:

  • Vaultwarden 浏览器插件:请求被 Cloudflare challenge 拦截 → 返回 403
  • Reeder(Google Reader API):请求被 Cloudflare challenge 拦截 → 返回 403

共同特征:

  • 响应头出现:server: cloudflare + cf-mitigated: challenge
  • 请求根本到不了 Traefik/应用日志

解决策略(推荐优先级):

  • A:该子域名切 DNS only(灰云):最稳,不需要客户端做任何配合。
  • B:保留橙云,但对该 Host/路径跳过挑战
    • 至少放行:/api/identity/reader/api/0/accounts/ClientLogin 等 API 路径

经验结论:

  • “能在浏览器里打开 ≠ 客户端能用”:浏览器能做人机验证,客户端做不了。

排错手册(从快到慢)

1)先判定“请求到没到服务器”

  • 如果 Cloudflare 403 challenge:请求没到服务器,先处理 Cloudflare/WAF。
  • 如果 Traefik 404/504:请求到 Traefik,但路由/网络/后端不可达。
  • 如果后端 4xx/5xx:请求到应用,查应用日志与配置。

2)Traefik 侧排查

  • 看 access log:该 Host 的请求落到了哪个 router/service、返回码是多少。
  • 多网络容器:优先怀疑 traefik.docker.network 缺失。
  • 同域名多路由来源:优先怀疑 file-provider 与 docker-provider 冲突(删掉旧 file 路由)。

3)应用侧排查

  • docker logs <container> 看启动是否成功、是否监听端口、是否连上 DB。
  • 检查是否误用了 ports: 导致端口冲突/暴露。

最终安全清单(上线前必做)

  • 宿主机无业务端口暴露(只保留 80/443)
  • 管理面必须有认证(dockhand、vaultwarden /admin 等)
  • 数据库/缓存不对公网暴露
  • Traefik 路由无冲突(同 Host 不重复定义 file-provider + docker-provider)
  • Cloudflare 不对 API 下发 challenge(客户端/插件/第三方集成会挂)

Comments