DailyUse 项目单机部署实战(Docker + Caddy + Nginx + PowerSync)

基于本机仓库与 ali-dailyuse 服务器真实配置,整理 DailyUse 在阿里云单机上的 Docker、Caddy、Nginx 与 PowerSync 部署实战。

#tech / dev #tech / dev / project / dailyuse #tech / ops / deploy #resource / docker #resource / nginx #resource / power-sync #type / journal #status / growing

[!info] related notes

DailyUse 项目单机部署实战(Docker + Caddy + Nginx + PowerSync)

目标

  • 记录 DailyUse / Memoflow 当前在阿里云单机上的真实部署结构,而不是只写抽象部署原则。
  • 把本机仓库配置、服务器 /opt/dailyuse 目录结构、运行中容器状态、镜像发布链路和排障经验串起来。
  • 让这篇笔记既能当部署操作手册,也能当“Docker + 反向代理 + 同步服务 + 单机编排”的项目实践总结。

前置条件

我这次核对了哪些真实来源

  • 本机项目目录:D:\home\projects\dailyuse
  • 服务器目录:/opt/dailyuse
  • 连接方式:ssh ali-dailyuse

本地仓库里实际读取过的关键文件

  • docker-compose.prod.yml
  • docker-compose.yml
  • Dockerfile.api
  • Dockerfile.web
  • nginx.conf
  • Caddyfile
  • docker/powersync/powersync.yaml
  • tools/docker/publish-images.ps1
  • .github/workflows/docker-deploy.yml
  • .github/workflows/release-please.yml
  • docs/deployment/README.md

服务器上实际核对过的内容

  • /opt/dailyuse 文件列表
  • docker-compose.prod.yml
  • Caddyfile
  • .env.production.local 中的变量键名
  • docker compose ps
  • docker images

说明:

  • 我只读取了线上环境变量的键名,没有把敏感值记入笔记。
  • 这篇笔记讲的是当前真实部署形态,不是以后可能会演进成的理想架构。

步骤

1. 先讲结论:当前生产环境长什么样

当前 DailyUse 的生产环境不是“一个 Nginx 包打天下”,而是下面这套多层结构:

Internet
  |
  v
Caddy :80/:443
  |
  +--> APP_DOMAIN          -> web:80 (容器内 Nginx)
  |                           |
  |                           +--> /        -> 前端静态资源
  |                           +--> /api/*   -> api:3000
  |
  +--> POWERSYNC_DOMAIN    -> powersync:8080
                                |
                                +--> postgres:5432

api:3000
  |
  +--> postgres:5432
  +--> redis:6379
  +--> ai-service:8100

watchtower
  |
  +--> 轮询业务镜像 prod-latest 并自动更新

这套结构里有三个很容易混淆的点:

  1. 公网入口不是 Nginx,而是 Caddy。
  2. 前端容器内部仍然有一层 Nginx。
  3. PowerSync 走独立子域名,不和主站 /api 共用一条入口。

所以 DailyUse 当前生产部署,不是简单的“前端 + 后端 + 数据库”,而是:

  • Caddy 做 TLS 终止和外层域名入口
  • web 容器内 Nginx 做静态资源和 /api/ 反代
  • api 做业务逻辑
  • powersync 做实时同步
  • postgres + redis 做基础设施
  • watchtower 做镜像自动更新

2. 服务器上真实存在什么

远端 /opt/dailyuse 当前能看到这些关键文件和痕迹:

  • docker-compose.prod.yml
  • Caddyfile
  • nginx.conf
  • .env
  • .env.production
  • .env.production.local
  • docker/
  • api.tar
  • web.tar
  • db.tar
  • redis.tar
  • dailyuse-infra-images.tar
  • backups/

这说明当前服务器不是一台“完全纯净、只靠在线拉镜像”的机器,而是保留了明显的离线镜像导入痕迹

  • 早期或故障时曾通过 api.tar / web.tar / db.tar / redis.tar 直接导入镜像。
  • 后续又整理出 dailyuse-infra-images.tar 这种基础设施离线镜像包。

补一个容易误判的点:

  • /opt/dailyuse 里虽然也有 nginx.conf,但按照当前 Dockerfile.web + docker-compose.prod.yml 的组合,web 实际生效的是镜像内 /etc/nginx/nginx.conf
  • 也就是说,服务器目录里的这份 nginx.conf 更像部署留档或历史残留,而不是当前运行时唯一真源。

这很重要,因为它直接决定了这套部署经验不能只写成:

docker compose pull && docker compose up -d

而必须把“国内网络 / 服务器拉镜像不稳 / 离线兜底”写进去。

3. 当前运行中的真实容器

远端当前运行中并且健康的服务包括:

  • dailyuse-postgres-1
  • dailyuse-redis-1
  • dailyuse-ai-service-1
  • dailyuse-api-1
  • dailyuse-powersync-1
  • dailyuse-web-1
  • dailyuse-caddy-1
  • dailyuse-watchtower-1

几个关键观察:

  • postgres 映射为 127.0.0.1:5432->5432
  • redis 映射为 127.0.0.1:6379->6379
  • caddy 暴露 80/443
  • webapi 都不直接暴露到公网
  • watchtower 已经长期运行,不是纸面规划

这意味着当前生产环境的边界设计是:

  • 基础设施只对宿主机本地可见
  • 公网只有 Caddy
  • Docker bridge network 承担容器间互联

这正是典型的“单机容器编排 + 外层反向代理入口”模型。

4. 生产编排文件到底怎么组织

当前真正的生产编排入口是仓库根目录的:

  • docker-compose.prod.yml

而不是开发环境的:

  • docker-compose.yml

两者分工很明确:

docker-compose.yml

这是本地基础设施编排,主要面向:

  • dev profile
  • test profile

它解决的是:

  • 开发时本地 Postgres / Redis / PowerSync
  • 测试时轻量临时数据库

docker-compose.prod.yml

这才是生产单机部署主文件,里面编排了:

  • postgres
  • redis
  • ai-service
  • api
  • powersync
  • web
  • caddy
  • watchtower

所以从知识组织上看:

  • docker-compose.yml 更偏 开发基础设施
  • docker-compose.prod.yml 更偏 生产运行时

这也是为什么部署笔记必须基于生产编排文件,而不是只基于开发 compose。

4.1 从配置文件到运行时的一一对应

如果只背“有个 compose、有个 Nginx、有个 Caddy”,很快就会混。
更稳的理解方式是:把每个文件和它在运行时承担的角色一一对应起来。

  • docker-compose.prod.yml
    • 定义生产服务清单、depends_on、健康检查、volume、端口暴露和 Docker network
    • 对应知识:Docker 在这个项目里承担的是“编排运行时”,不只是打镜像
  • Caddyfile
    • 定义公网域名入口、TLS 证书申请、APP_DOMAIN -> web:80POWERSYNC_DOMAIN -> powersync:8080
    • 对应知识:正向代理与反向代理 里的反向代理入口层
  • nginx.conf
  • Dockerfile.api
    • 定义 API 运行时镜像、健康检查、启动前迁移
    • 对应知识:镜像不是源码压缩包,而是“可直接运行的交付物”
  • Dockerfile.web
    • 定义 web 镜像如何把编译产物和 nginx.conf 打进一个纯静态站点容器
    • 对应知识:前端生产部署可以是“静态文件容器 + 反代”,不一定要 Node 常驻
  • docker/powersync/powersync.yaml
    • 定义 PowerSync 如何连接 Postgres、如何按用户切 bucket、如何校验客户端 JWT
    • 对应知识:Power Sync 在生产里是同步平面,而不是临时开发工具

5. 为什么这套部署同时用了 Caddy 和 Nginx

这是当前项目最值得讲清楚的一点。

外层:Caddy

Caddyfile 当前做的事情很集中:

  • 监听 APP_DOMAIN
  • 反代到 web:80
  • 监听 POWERSYNC_DOMAIN
  • 反代到 powersync:8080
  • 自动申请和续期 HTTPS 证书
  • 在入口层补一部分安全头

可以把它理解成:

Caddy = 公网 HTTPS 入口 + 域名级反向代理

内层:web 容器中的 Nginx

Dockerfile.web 会把根目录的 nginx.conf 复制进镜像:

  • /etc/nginx/nginx.conf

这层 Nginx 负责:

  • 前端静态资源分发
  • sw.jsindex.htmlsite.webmanifest 的缓存策略
  • 静态资源长缓存
  • SPA 的 try_files $uri $uri/ /index.html
  • /api/ 转发到 api:3000

可以把它理解成:

web(Nginx) = 前端静态站点容器 + API 内网入口

为什么不是 Caddy 直接把 /api 转给 api

因为当前项目选择的是:

主域名 -> Caddy -> web(Nginx)
                     ├─ /      -> 静态资源
                     └─ /api/* -> api

好处是:

  • 主站与 API 保持同源
  • 前端不需要额外面对跨域问题
  • Web 层可以统一处理静态资源缓存和 SPA 路由

代价是:

  • 代理链多了一层
  • /api/* 的排障要同时看 Caddy -> web(Nginx) -> api

这正好对应 Nginx 笔记里的“反向代理”知识:
Nginx 不只是静态资源服务器,也在这里承担了内层流量路由器

6. Nginx 配置在这个项目里具体承担什么

根目录 nginx.conf 的几个关键职责非常明确。

如果你想看这份配置的逐段解析,包括:

  • 为什么 web -> api 显式用 proxy_http_version 1.1
  • gzip 压缩到底压哪些内容、不压哪些内容
  • resolver 127.0.0.11 和变量形式 proxy_pass 为什么要一起出现
  • sw.jsindex.html、静态资源为什么走不同缓存策略

直接看:DailyUse 项目中的 Nginx 配置解析

6.1 静态资源服务

root /usr/share/nginx/html;
index index.html;

这是标准的静态资源容器写法,和 Nginx 里讲的“静态文件服务器”角色完全对应。

6.2 SPA 路由回退

location / {
    try_files $uri $uri/ /index.html;
}

这正是单页应用部署常见的模式。
刷新 /user/profile 不会 404,就是因为回退到了 index.html

这个知识点直接对应:

6.3 API 反向代理

location /api/ {
    set $api_upstream http://api:3000;
    proxy_pass $api_upstream;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_buffering off;
}

这段非常值得记,因为它把多个 Nginx 知识点串到一起了:

  • proxy_pass
  • proxy_http_version 1.1
  • X-Forwarded-*
  • WebSocket 升级头
  • proxy_buffering off
  • 长读写超时

这不是“为了会配置而配置”,而是和 DailyUse 的实际 API / 流式交互需求强绑定。

6.4 缓存策略

sw.jsindex.htmlsite.webmanifest 和静态资源走了不同缓存策略。

这说明当前前端部署不是“能跑就行”,而是在考虑:

  • Service Worker 更新
  • HTML 壳层及时刷新
  • 静态资源长缓存与 immutable

6.5 gzip 压缩策略

当前 nginx.conf 明确开启了:

  • gzip on
  • gzip_vary on
  • gzip_proxied any
  • gzip_comp_level 6

并且主要压缩:

  • text/css
  • application/json
  • application/javascript
  • image/svg+xml
  • 字体相关类型

但不会对 .png/.jpg/.gif 这类已经压缩过的位图图片重复 gzip。
这意味着当前 web 容器不只是“把文件发出去”,而是在承担一层面向前端产物的带宽优化。

这正对应:

7. Docker 在这个项目里不是“装个容器”这么简单

DailyUse 的 Docker 实践有三层。

7.1 镜像构建层

本地和 CI 都会构建三类业务镜像:

  • dailyuse-api
  • dailyuse-web
  • dailyuse-ai-service

相关脚本与工作流:

  • tools/docker/publish-images.ps1
  • .github/workflows/docker-deploy.yml

它们不是两套互不相干的方案,而是同一条发布链路的两个入口:

  • 本地脚本适合手工发版、补发镜像、显式控制 tag
  • GitHub Actions 适合正式 tag 驱动的标准发布

publish-images.ps1 里几个值得记的细节:

  • 默认 tag 形如 v<version>-prod.<utc时间戳>-<gitsha>
  • 会先跑 pnpm nx build api
  • 会再跑 pnpm nx build web --configuration=production
  • -Push 会把业务镜像推到 ACR,并额外打一个 prod-latest
  • 如果不加 -SkipEnvUpdate,会顺手把 env 文件里的 API_TAG / WEB_TAG / AI_SERVICE_TAG 更新成新 tag

7.2 运行时编排层

生产用 docker-compose.prod.yml 编排整套服务。

所以这里的 Docker 不是只用来“打包后端”,而是承担:

  • 基础设施容器编排
  • 业务容器编排
  • 网络隔离
  • volume 持久化
  • 健康检查

7.3 发布与回滚层

镜像 tag、prod-latest、离线 tar 包、Watchtower 自动更新,共同构成发布链路。

这说明项目里的 Docker 已经从“开发工具”上升到了:

部署与发布的核心运行时

8. API 镜像的真实启动方式透露了什么

Dockerfile.api 里最终启动命令是:

sh ./scripts/run-migrations.sh && node --import tsx dist/main.js

这意味着:

  1. 容器启动时会先跑迁移
  2. 然后才真正启动 API 服务

这对部署很关键,因为它带来两个实际影响:

  • api 起不来时,不能只怀疑业务代码,也要怀疑迁移脚本
  • api 的健康检查延迟不能太激进,否则迁移阶段可能被误判失败

这也是为什么 docker-compose.prod.yml 里给 api 设了:

  • start_period: 40s

这类细节只有把 Dockerfile、compose 和真实运行状态放在一起看,才会意识到它的重要性。

9. Web 镜像的真实职责

Dockerfile.web 非常“纯”:

  • 基础镜像:nginx:1.27-alpine
  • 删除默认 conf.d/default.conf
  • 复制 nginx.conf
  • 复制 dist/apps/web

它说明一件事:

web 镜像本质上就是“构建好的前端静态产物 + 一层定制 Nginx”

这和很多“Node 直接 serve 前端”或“把前端和 API 打在一起”的方案不同。

当前项目显式选择了:

  • 前端静态层独立
  • API 独立
  • 由 Nginx 做内层反代

这是一个更接近生产环境的前后端分层部署方式。

10. PowerSync 在这套部署里的位置

当前项目不是把 PowerSync 当成“一个可有可无的开发辅助服务”,而是生产结构中的正式一环。

docker-compose.prod.ymldocker/powersync/powersync.yaml 可以看出:

  • 它连接的是生产 Postgres
  • 依赖逻辑复制
  • hostname / port / database / username / password 明确配置连接
  • 用同步规则把按用户隔离的数据下发到客户端

这和 Power Sync 里的概念完全对上:

Postgres 逻辑复制 -> PowerSync Service -> 客户端 SQLite

对 DailyUse 来说,它的意义不是“提升一点体验”,而是支撑:

  • 本地优先
  • 多端同步
  • Desktop / Web 数据一致性

所以从项目部署视角看,PowerSync 不是边缘服务,而是架构中心的一部分。

11. 当前发布链路到底是什么

当前发布不是“手工上服务器构建源码”,而是镜像发布链路。

主线是:

  1. main 分支推进
  2. release-please 管理版本与 release PR
  3. 创建正式 tag
  4. docker-deploy.yml 构建并推送镜像到阿里云 ACR
  5. 服务器通过 docker compose pull / Watchtower 获取新镜像

本地也有一套手工发布脚本:

  • tools/docker/publish-images.ps1

它会:

  • 读取 env 文件
  • pnpm nx build api
  • pnpm nx build web --configuration=production
  • 构建 api / web / ai-service
  • 可选更新 env 中的 tag
  • 可选推送并额外打 prod-latest

这说明当前项目的部署实践已经不只是“会写 Dockerfile”,而是把:

  • 本地脚本
  • CI workflow
  • 镜像仓库
  • 服务器自动更新

串成了完整链路。

12. 为什么这套项目保留了离线镜像兜底

这是 DailyUse 当前部署经验里最实战的一点。

服务器目录里保留了:

  • api.tar
  • web.tar
  • db.tar
  • redis.tar
  • dailyuse-infra-images.tar

这说明真实线上环境曾经遇到过,或者仍然防范着:

  • Docker Hub 拉取不稳定
  • 基础镜像拉取慢
  • 国内网络环境导致服务器直接拉镜像失败

所以这套部署不能只写成“标准云原生流程”,还必须写上:

本地 pull -> docker save -> scp -> 服务器 docker load

这个点和旧笔记直接关联:

但现在可以更进一步总结成一句话:

DailyUse 的真实部署策略是“业务镜像优先走 ACR,基础设施镜像必要时离线导入”,而不是纯在线拉取。

13. 当前环境变量透露了哪些结构信息

线上 .env.production.local 里实际能看到这些变量家族:

  • 镜像与版本:
    • REGISTRY
    • IMAGE_NAMESPACE
    • API_TAG
    • WEB_TAG
    • AI_SERVICE_TAG
  • 基础设施:
    • DB_*
    • REDIS_*
  • 应用认证:
    • JWT_SECRET
    • SERVICE_SECRET
  • 域名与入口:
    • APP_DOMAIN
    • API_DOMAIN
    • POWERSYNC_DOMAIN
  • Web / API:
    • CORS_ORIGIN
    • VITE_API_BASE_URL
    • PROXY_TARGET_URL
  • PowerSync:
    • POWERSYNC_URL
    • POWERSYNC_PRIVATE_KEY
    • POWERSYNC_PUBLIC_KEY_N
    • POWERSYNC_PUBLIC_KEY_E
    • POWERSYNC_KEY_ID

这说明当前生产环境不是“只有几个数据库账号和一个域名”那么简单,而是已经形成了:

  • 镜像版本治理
  • 域名分层
  • 服务间鉴权
  • 同步服务公私钥配置

这也是为什么部署笔记必须和项目架构知识互相引用,而不能单独孤立存在。

14. 实际部署时推荐怎么做

结合当前仓库和线上状态,推荐执行顺序是:

  1. 确认本地构建产物和镜像可用
  2. 推送业务镜像到 ACR
  3. 必要时把基础设施镜像打成 tar 上传
  4. 登录服务器,进入 /opt/dailyuse
  5. 检查 .env.production.localdocker-compose.prod.ymlCaddyfile
  6. docker compose -f docker-compose.prod.yml --env-file .env.production.local up -d
  7. docker compose ps 和 logs 验证所有健康检查
  8. 再决定是否交给 Watchtower 自动更新

这里最重要的经验不是命令本身,而是:

  • 不要把“构建、推送、运行、自动更新、回滚”混成一个步骤
  • 每一步都要有清晰边界和验证点

14.1 本地发布业务镜像

如果这次是手工发版,而不是等 CI 跑正式 tag,那么本地入口就是:

pwsh ./tools/docker/publish-images.ps1 -EnvFile .env.production.local -Push

这个命令的真实含义不是“推个镜像”这么简单,而是:

  1. 先构建 apiweb 的生产产物
  2. 再构建 api / web / ai-service 三个业务镜像
  3. 推送到 .env.production.local 指定的 REGISTRY/IMAGE_NAMESPACE
  4. 额外更新 prod-latest

如果你想发一个固定版本,而不是自动生成时间戳 tag,可以显式指定:

pwsh ./tools/docker/publish-images.ps1 -EnvFile .env.production.local -Tag v0.3.0-prod.manual -Push

14.2 服务器侧拉起生产服务

业务镜像进入 ACR 之后,服务器侧的核心动作只有三类:

  1. 确认配置文件
  2. 确认基础设施镜像是否已就位
  3. 用生产 compose 拉起服务

典型命令:

ssh ali-dailyuse
cd /opt/dailyuse
docker compose -f docker-compose.prod.yml --env-file .env.production.local pull
docker compose -f docker-compose.prod.yml --env-file .env.production.local up -d
docker compose -f docker-compose.prod.yml --env-file .env.production.local ps

如果服务器对 Docker Hub 拉取不稳定,就不要在这一步死磕在线拉取,而是回到前面说的离线路线:

本地 pull -> docker save -> scp -> 服务器 docker load -> compose up -d

14.3 推荐的验证顺序

不要一上来只看浏览器能不能打开。
更稳的验证顺序是:

  1. docker compose ps,先确认健康检查状态
  2. api 日志,确认迁移已经跑完
  3. caddy 日志,确认 HTTPS 证书和入口层没问题
  4. 再看浏览器首页和 /api/* 是否正常
  5. 最后验证 PowerSync 是否能建立同步链路

常用命令:

docker compose -f docker-compose.prod.yml --env-file .env.production.local logs --tail=100 api
docker compose -f docker-compose.prod.yml --env-file .env.production.local logs --tail=100 caddy
docker compose -f docker-compose.prod.yml --env-file .env.production.local logs --tail=100 powersync

14.4 什么时候交给 Watchtower

watchtower 已经是当前线上真实运行的一部分,但更稳妥的习惯是:

  • 首次部署或大改配置时,先手工 pull + up -d + 验证
  • 确认镜像、配置、域名、证书都正常后,再让 Watchtower 接手后续 prod-latest 更新

这样做的原因是:
自动更新适合“已经稳定的发布链路”,不适合“首次起站时一边排障一边自动滚动”。

15. 这篇笔记和旧笔记的关系

这篇不是为了替代所有旧笔记,而是做项目级总整合。

分工建议:

换句话说:

  • 旧笔记回答“某一步怎么做”
  • 这篇回答“整个 DailyUse 生产部署到底是怎么运转的”

验证

  • 这篇笔记里的部署结构,能和当前本机 docker-compose.prod.ymlnginx.confCaddyfile 对应上。
  • 这篇笔记里的运行状态,能和服务器当前 docker compose ps 对应上。
  • 这篇笔记里的知识引用,能把 Docker、Nginx、PowerSync、镜像仓库和 DailyUse 旧部署笔记串起来。

常见问题

为什么不是只更新 dailyuse-api-deployment

  • 因为这次内容已经超出了“API 镜像部署”边界,涉及整套单机生产结构。

为什么要强调 Caddy 和 Nginx 是两层

  • 因为当前项目确实就是两层代理职责分离,不讲清楚就会误判入口和排障链路。

为什么这篇要写离线镜像 tar

  • 因为服务器目录里真实存在这些文件,说明这不是假设,而是实际运维经验的一部分。
创建于 2026/5/7 更新于 2026/5/27