DailyUse 项目单机部署实战(Docker + Caddy + Nginx + PowerSync)
基于本机仓库与 ali-dailyuse 服务器真实配置,整理 DailyUse 在阿里云单机上的 Docker、Caddy、Nginx 与 PowerSync 部署实战。
[!info] related notes
- 相关 MOC: Daily Use MOC, CI/CD MOC
- 前置概念: Docker, Nginx, Power Sync, 正向代理与反向代理
- 相关实践: DailyUse的api端部署, DailyUse 项目中的 Nginx 配置解析, dailyuse的docker镜像构建, dailyuse的Docker镜像推送, dailyuse的Docker镜像拉取, ACR(容器镜像服务)
- 相关部署: PostgreSQL 部署
DailyUse 项目单机部署实战(Docker + Caddy + Nginx + PowerSync)
目标
- 记录 DailyUse / Memoflow 当前在阿里云单机上的真实部署结构,而不是只写抽象部署原则。
- 把本机仓库配置、服务器
/opt/dailyuse目录结构、运行中容器状态、镜像发布链路和排障经验串起来。 - 让这篇笔记既能当部署操作手册,也能当“Docker + 反向代理 + 同步服务 + 单机编排”的项目实践总结。
前置条件
我这次核对了哪些真实来源
- 本机项目目录:
D:\home\projects\dailyuse - 服务器目录:
/opt/dailyuse - 连接方式:
ssh ali-dailyuse
本地仓库里实际读取过的关键文件
docker-compose.prod.ymldocker-compose.ymlDockerfile.apiDockerfile.webnginx.confCaddyfiledocker/powersync/powersync.yamltools/docker/publish-images.ps1.github/workflows/docker-deploy.yml.github/workflows/release-please.ymldocs/deployment/README.md
服务器上实际核对过的内容
/opt/dailyuse文件列表docker-compose.prod.ymlCaddyfile.env.production.local中的变量键名docker compose psdocker 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 并自动更新
这套结构里有三个很容易混淆的点:
- 公网入口不是 Nginx,而是 Caddy。
- 前端容器内部仍然有一层 Nginx。
- PowerSync 走独立子域名,不和主站
/api共用一条入口。
所以 DailyUse 当前生产部署,不是简单的“前端 + 后端 + 数据库”,而是:
Caddy做 TLS 终止和外层域名入口web容器内Nginx做静态资源和/api/反代api做业务逻辑powersync做实时同步postgres + redis做基础设施watchtower做镜像自动更新
2. 服务器上真实存在什么
远端 /opt/dailyuse 当前能看到这些关键文件和痕迹:
docker-compose.prod.ymlCaddyfilenginx.conf.env.env.production.env.production.localdocker/api.tarweb.tardb.tarredis.tardailyuse-infra-images.tarbackups/
这说明当前服务器不是一台“完全纯净、只靠在线拉镜像”的机器,而是保留了明显的离线镜像导入痕迹:
- 早期或故障时曾通过
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-1dailyuse-redis-1dailyuse-ai-service-1dailyuse-api-1dailyuse-powersync-1dailyuse-web-1dailyuse-caddy-1dailyuse-watchtower-1
几个关键观察:
postgres映射为127.0.0.1:5432->5432redis映射为127.0.0.1:6379->6379caddy暴露80/443web和api都不直接暴露到公网watchtower已经长期运行,不是纸面规划
这意味着当前生产环境的边界设计是:
- 基础设施只对宿主机本地可见
- 公网只有 Caddy
- Docker bridge network 承担容器间互联
这正是典型的“单机容器编排 + 外层反向代理入口”模型。
4. 生产编排文件到底怎么组织
当前真正的生产编排入口是仓库根目录的:
docker-compose.prod.yml
而不是开发环境的:
docker-compose.yml
两者分工很明确:
docker-compose.yml
这是本地基础设施编排,主要面向:
devprofiletestprofile
它解决的是:
- 开发时本地 Postgres / Redis / PowerSync
- 测试时轻量临时数据库
docker-compose.prod.yml
这才是生产单机部署主文件,里面编排了:
postgresredisai-serviceapipowersyncwebcaddywatchtower
所以从知识组织上看:
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:80与POWERSYNC_DOMAIN -> powersync:8080 - 对应知识:正向代理与反向代理 里的反向代理入口层
- 定义公网域名入口、TLS 证书申请、
nginx.conf- 定义前端静态资源缓存、SPA 路由兜底、
/api/到api:3000的内层反代 - 对应知识:Nginx、Nginx 中的 location、root、alias 与 proxy_pass、Nginx 中的 buffering、timeout、cache 与日志排障
- 定义前端静态资源缓存、SPA 路由兜底、
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.js、index.html、site.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.js、index.html、静态资源为什么走不同缓存策略
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_passproxy_http_version 1.1X-Forwarded-*- WebSocket 升级头
proxy_buffering off- 长读写超时
这不是“为了会配置而配置”,而是和 DailyUse 的实际 API / 流式交互需求强绑定。
6.4 缓存策略
sw.js、index.html、site.webmanifest 和静态资源走了不同缓存策略。
这说明当前前端部署不是“能跑就行”,而是在考虑:
- Service Worker 更新
- HTML 壳层及时刷新
- 静态资源长缓存与 immutable
6.5 gzip 压缩策略
当前 nginx.conf 明确开启了:
gzip ongzip_vary ongzip_proxied anygzip_comp_level 6
并且主要压缩:
text/cssapplication/jsonapplication/javascriptimage/svg+xml- 字体相关类型
但不会对 .png/.jpg/.gif 这类已经压缩过的位图图片重复 gzip。
这意味着当前 web 容器不只是“把文件发出去”,而是在承担一层面向前端产物的带宽优化。
这正对应:
7. Docker 在这个项目里不是“装个容器”这么简单
DailyUse 的 Docker 实践有三层。
7.1 镜像构建层
本地和 CI 都会构建三类业务镜像:
dailyuse-apidailyuse-webdailyuse-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
这意味着:
- 容器启动时会先跑迁移
- 然后才真正启动 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.yml 和 docker/powersync/powersync.yaml 可以看出:
- 它连接的是生产 Postgres
- 依赖逻辑复制
- 用
hostname / port / database / username / password明确配置连接 - 用同步规则把按用户隔离的数据下发到客户端
这和 Power Sync 里的概念完全对上:
Postgres 逻辑复制 -> PowerSync Service -> 客户端 SQLite
对 DailyUse 来说,它的意义不是“提升一点体验”,而是支撑:
- 本地优先
- 多端同步
- Desktop / Web 数据一致性
所以从项目部署视角看,PowerSync 不是边缘服务,而是架构中心的一部分。
11. 当前发布链路到底是什么
当前发布不是“手工上服务器构建源码”,而是镜像发布链路。
主线是:
main分支推进release-please管理版本与 release PR- 创建正式 tag
docker-deploy.yml构建并推送镜像到阿里云 ACR- 服务器通过
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.tarweb.tardb.tarredis.tardailyuse-infra-images.tar
这说明真实线上环境曾经遇到过,或者仍然防范着:
- Docker Hub 拉取不稳定
- 基础镜像拉取慢
- 国内网络环境导致服务器直接拉镜像失败
所以这套部署不能只写成“标准云原生流程”,还必须写上:
本地 pull -> docker save -> scp -> 服务器 docker load
这个点和旧笔记直接关联:
但现在可以更进一步总结成一句话:
DailyUse 的真实部署策略是“业务镜像优先走 ACR,基础设施镜像必要时离线导入”,而不是纯在线拉取。
13. 当前环境变量透露了哪些结构信息
线上 .env.production.local 里实际能看到这些变量家族:
- 镜像与版本:
REGISTRYIMAGE_NAMESPACEAPI_TAGWEB_TAGAI_SERVICE_TAG
- 基础设施:
DB_*REDIS_*
- 应用认证:
JWT_SECRETSERVICE_SECRET
- 域名与入口:
APP_DOMAINAPI_DOMAINPOWERSYNC_DOMAIN
- Web / API:
CORS_ORIGINVITE_API_BASE_URLPROXY_TARGET_URL
- PowerSync:
POWERSYNC_URLPOWERSYNC_PRIVATE_KEYPOWERSYNC_PUBLIC_KEY_NPOWERSYNC_PUBLIC_KEY_EPOWERSYNC_KEY_ID
这说明当前生产环境不是“只有几个数据库账号和一个域名”那么简单,而是已经形成了:
- 镜像版本治理
- 域名分层
- 服务间鉴权
- 同步服务公私钥配置
这也是为什么部署笔记必须和项目架构知识互相引用,而不能单独孤立存在。
14. 实际部署时推荐怎么做
结合当前仓库和线上状态,推荐执行顺序是:
- 确认本地构建产物和镜像可用
- 推送业务镜像到 ACR
- 必要时把基础设施镜像打成 tar 上传
- 登录服务器,进入
/opt/dailyuse - 检查
.env.production.local、docker-compose.prod.yml、Caddyfile docker compose -f docker-compose.prod.yml --env-file .env.production.local up -d- 用
docker compose ps和 logs 验证所有健康检查 - 再决定是否交给 Watchtower 自动更新
这里最重要的经验不是命令本身,而是:
- 不要把“构建、推送、运行、自动更新、回滚”混成一个步骤
- 每一步都要有清晰边界和验证点
14.1 本地发布业务镜像
如果这次是手工发版,而不是等 CI 跑正式 tag,那么本地入口就是:
pwsh ./tools/docker/publish-images.ps1 -EnvFile .env.production.local -Push
这个命令的真实含义不是“推个镜像”这么简单,而是:
- 先构建
api和web的生产产物 - 再构建
api / web / ai-service三个业务镜像 - 推送到
.env.production.local指定的REGISTRY/IMAGE_NAMESPACE - 额外更新
prod-latest
如果你想发一个固定版本,而不是自动生成时间戳 tag,可以显式指定:
pwsh ./tools/docker/publish-images.ps1 -EnvFile .env.production.local -Tag v0.3.0-prod.manual -Push
14.2 服务器侧拉起生产服务
业务镜像进入 ACR 之后,服务器侧的核心动作只有三类:
- 确认配置文件
- 确认基础设施镜像是否已就位
- 用生产 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 推荐的验证顺序
不要一上来只看浏览器能不能打开。
更稳的验证顺序是:
- 看
docker compose ps,先确认健康检查状态 - 看
api日志,确认迁移已经跑完 - 看
caddy日志,确认 HTTPS 证书和入口层没问题 - 再看浏览器首页和
/api/*是否正常 - 最后验证 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的api端部署
- 保留为 API 部署入口和镜像部署总览
- dailyuse的docker镜像构建
- 专注镜像构建问题和经验
- dailyuse的Docker镜像推送
- 专注仓库推送
- dailyuse的Docker镜像拉取
- 专注服务器侧拉取和离线导入
- 本文
- 负责把“真实服务器部署结构 + 本地配置 + 理论知识点 + 运行经验”串起来
换句话说:
- 旧笔记回答“某一步怎么做”
- 这篇回答“整个 DailyUse 生产部署到底是怎么运转的”
验证
- 这篇笔记里的部署结构,能和当前本机
docker-compose.prod.yml、nginx.conf、Caddyfile对应上。 - 这篇笔记里的运行状态,能和服务器当前
docker compose ps对应上。 - 这篇笔记里的知识引用,能把 Docker、Nginx、PowerSync、镜像仓库和 DailyUse 旧部署笔记串起来。
常见问题
为什么不是只更新 dailyuse-api-deployment
- 因为这次内容已经超出了“API 镜像部署”边界,涉及整套单机生产结构。
为什么要强调 Caddy 和 Nginx 是两层
- 因为当前项目确实就是两层代理职责分离,不讲清楚就会误判入口和排障链路。
为什么这篇要写离线镜像 tar
- 因为服务器目录里真实存在这些文件,说明这不是假设,而是实际运维经验的一部分。