# Fully Booked - Docker 化部署，使用 DigitalOcean 和 Cloudflare

Company: Fully Booked
Service: DevOps 工程
Period: 2023-04 - 2025-06
Tech: Docker, DigitalOcean, Cloudflare, DNS, SSL Certificates, Caddy
Web: https://fully-booked.uk/
Canonical: https://webanion.com/zh/portfolio/fully-booked-dockerized-deployment-with-digital-ocean-and-cloudflare

Fully Booked 以五个 Docker 服务的形式上线，部署在 Cloudflare 后面的一台 DigitalOcean droplet 上，让一个很小的团队也能运营一个托管定金的市场平台。一个仓库构建所有镜像，一个 compose 文件和一个 Makefile 把发布简化为 pull、build 和 up，Caddy 代理负责证书和安全头，Playwright 在服务器上渲染每一份报价单和发票的 PDF。由于各服务从一开始就相互独立，后来无需重建就迁移到了自托管的 Kubernetes 集群。

## 完整故事

这套基础设施让一个收取定金的交易平台，由一个小团队运行五项服务，而且不出意外。

### 托管定金的交易平台不能宕机

<p>Fully Booked 收取客户的定金，向运营商发放收入，并送达双方都依赖的各种文件。在这里，宕机可不只是不方便：无法付款的客户会去别家预订，打不开报价单的运营商会丢掉这单生意。</p><p>整个平台由五项服务组成，包括客户网站、运营商网站、管理后台、文档网站和一个后端，由一个非常小的团队开发和维护。部署方式必须简单到可以手动运行，又稳固到值得信赖。</p>

### 五项服务，一个代码仓库，一条命令

<p>每项服务都从同一个代码仓库构建为独立的 Docker 镜像。四个网站被构建成轻量的静态服务器，NestJS 后端承载 GraphQL API，MongoDB 和上传的文件则存放在持久化卷上。一个 compose 文件把它们连接到同一个私有网络中，一个 Makefile 把每次发布简化为 pull、build 和 up 三步。</p>

### Cloudflare 在前，外加两个刻意的例外

<p>Cloudflare 托管每个子域名的 DNS，并为公开网站提供代理，负责缓存、证书和防护。有两条记录特意保持直连：一条是运营商网站，好让 Apple 读取那个让链接直接打开应用的文件；另一条是 API，好让 Square 的支付 webhook 顺利到达而不被拦截。</p><p>这两处任何一处出错，要么每份 PDF 里的深度链接都会失效，要么支付无法确认，所以两者都在 DNS 表旁边写明了原因。</p>

### 承担安全工作的反向代理

<p>位于边缘的 Caddy 容器会自动签发证书、压缩响应、重定向所有 www 地址，为 API 添加 strict transport、frame 和 content type 等安全头，并以应用商店要求的 content type 提供应用关联文件。</p>

### 在生产环境中生成文件

<p>每一份报价单、取消信函、发票、预订确认函和司机行程单，都由运行在后端镜像中的 Playwright 在服务器端生成。让无头浏览器在生产容器里成功安装，是整个项目中最难的一环，也正因如此，运营商才能在几秒钟内用手机发出一份精美的 PDF。</p>

### 从一台服务器到一个集群

<p>在最初的几个阶段，这台 droplet 服务器支撑起了整个平台。当每月账单和单台机器的局限开始显现时，同样的镜像被<a href="/portfolio/fully-booked-zero-downtime-migration-to-self-hosted-kubernetes">零停机迁移到自托管的 Kubernetes 集群</a>，之所以能做到，是因为它们从一开始就是作为独立服务构建的。</p><p>对创始人来说，这意味着无需重建、没有中断；对运营商和客户来说，一切照旧，只是系统一直稳定运行。这些服务本身的故事，请见<a href="/portfolio/fully-booked-all-in-one-smart-booking-solution">Fully Booked 的完整故事</a>。</p>
