我曾经有一个 deploy_all.sh,300 多行,把所有服务的部署逻辑全塞进去。每次改一个环境变量都要 SSH 进服务器手动改,每次服务挂了都要 pm2 logs 翻日志,每次 Nginx 配置出问题都得 nginx -t 反复试。
这套方案跑了大半年,直到有一天 admin-web 部署时服务器直接卡死——OOM,npm install 把 2GB 内存全吃完了。
那天我决定迁移到 Coolify。
为什么是 Coolify
市面上的选项不少:Vercel、Railway、Render、Fly.io。但我的情况有几个限制:
服务器在国内。 Vercel 部署到国内的速度体验很差,自定义域名还要备案问题。
有多个服务。 后端 API、管理后台、5 个落地页、一个工具站,8 个服务如果每个都用不同平台,成本和心智负担都上去了。
想要自托管。 花钱买了服务器,不想再叠一层 PaaS 费用。
Coolify 是一个开源的自托管 PaaS,本质上是一个 UI 友好的 Docker Compose 管理器,内置了 Traefik 反向代理和 Let's Encrypt 证书。安装一行命令,装完就有一个 Web 面板,可以把 GitHub 仓库直接部署成容器。
对独立开发者来说,这个定位很准:不用运维经验,但控制权在自己手里。
迁移前的架构
迁移前的部署方案大致是这样:
GitHub → SSH 拉代码 → npm install + npm build → PM2 守护进程
↓
Nginx 反向代理
8 个服务跑在同一台 2vCPU / 4GB 的云服务器上,PM2 管理 Node.js 进程,Nginx 做端口映射和 SSL 终止。
这套方案的问题:
- 构建在服务器上跑,内存不够就 OOM
- 没有资源隔离,一个服务吃内存影响所有服务
- 回滚很麻烦,出问题只能重新拉代码重新构建
- 脚本越来越复杂,修复一个 bug 可能引入另一个
Coolify 的部署模型
Coolify 的核心是 Docker 容器,每个服务跑在独立容器里,由 Traefik 统一做反向代理。
对我来说最重要的两个特性:
-
自动 SSL:只要域名解析指向服务器,Coolify 自动申请 Let's Encrypt 证书,自动续期,完全不用操心。
-
Webhook 部署:每个服务有一个 Webhook URL,POST 这个 URL 就触发重新部署。可以很方便地接入 GitHub Actions。
两种部署方式
迁移过程中摸索出两套方案,针对不同的项目特点:
方式 A:Dockerfile 服务器构建
适合轻量项目,比如 Next.js 落地页。Coolify 从 GitHub 拉代码,在服务器上直接跑 docker build。
配置很简单:
- Build Pack 选 Dockerfile
- 指定项目目录和 Dockerfile 路径
- 端口设置为 80(容器内 Nginx 监听的端口)
5 个落地页全部用这个方式,每次 push 代码后点一下 Redeploy,2 分钟内就上线。
方式 B:本地构建 + GHCR
admin-web 用了 UmiJS + Ant Design ProComponents,node_modules 有 800MB,服务器上跑 npm install 就会 OOM。
解决方案:在本地构建 Docker 镜像,推送到 GitHub Container Registry(GHCR),Coolify 只负责 pull 镜像,不做构建。
# 本地运行
bash script/deploy/coolify/build_admin_web.sh
# 脚本内部流程:
# 1. docker build → 本地镜像
# 2. docker push → ghcr.io/guntherlau/admin-web:latest
# 3. curl Coolify Webhook → 触发重部署(拉最新镜像)服务器端的"Dockerfile"只有一行:
FROM ghcr.io/guntherlau/admin-web:latestCoolify 执行"构建"实际上只是 docker pull,服务器零压力。
踩过的坑
这部分才是写这篇文章的主要原因,希望能帮后来者少踩。
1. 不能用 Nixpacks
Coolify 面板提供了 Nixpacks 这个选项,号称自动检测项目类型,不需要写 Dockerfile。国内服务器不能用——构建时需要访问 cache.nixos.org,直接超时。
所有项目必须自己写 Dockerfile,用 Dockerfile 方式部署。
2. Dockerfile 里不能用 heredoc 写配置
在 Dockerfile 里用 heredoc 内联写 Nginx 配置,本地构建没问题,但 Coolify 的 Dockerfile 解析器会把 heredoc 内容当 Docker 指令处理,报各种莫名其妙的错误:
# 这样会出错
RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
server {
listen 80;
...
}
EOF正确做法:把 Nginx 配置写成单独文件,用 COPY 引入:
COPY nginx.conf /etc/nginx/conf.d/default.conf3. Alpine 镜像的 apk 要换源
用 Alpine 基础镜像时,apk add 访问官方源 dl-cdn.alpinelinux.org 会超时。在 Dockerfile 第一行换成阿里云镜像:
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories4. NestJS 构建要强制 NODE_ENV=development
Coolify 会把环境变量注入为构建参数,如果项目配置了 NODE_ENV=production,npm ci 会跳过 devDependencies,导致 NestJS 的 CLI 找不到:
Error: Cannot find module '@nestjs/cli'
在 Dockerfile 里显式覆盖:
RUN NODE_ENV=development npm ci
RUN npm run build5. 先加 Swap 再部署
2-4GB 内存的服务器,npm install 期间内存不够就会被 OOM Killer 杀掉,构建失败但日志里看不到明显报错。
部署前先加 Swap:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# 持久化,重启不丢失
echo '/swapfile none swap sw 0 0' >> /etc/fstab6. 端口必须改成 80
Coolify 默认的 Ports Exposes 是 3000,但 Nginx 容器监听 80 端口。不改的话所有请求都会 502 Bad Gateway。
在 Configuration → Network → Ports Exposes 改成 80。
7. SSL 证书要等 1-5 分钟
部署完成后不要立刻测试 HTTPS,Let's Encrypt 证书申请需要时间。看到 Coolify 面板部署成功,等几分钟再访问。
8. 主域名要同时配置 www 和非 www
在 Coolify 的 Domains 字段用逗号分隔填两个:
https://www.gunlabs.cn,https://gunlabs.cn
只填一个的话,访问另一个会 404。
9. GHCR 镜像要设成 Public
推送到 GHCR 的镜像默认是 Private,服务器拉取时会报 unauthorized 错误,即使服务器有 SSH 权限也不行——GHCR 是独立的权限体系。
手动改成 Public:GitHub → Profile → Packages → 对应包 → Package settings → Change visibility → Public
10. Swap 重启会丢失
手动 swapon 的 Swap 重启后消失。必须写入 /etc/fstab 才能持久化(见第 5 条)。这个坑在服务器重启后才会发现,所以要在加 Swap 的时候一并处理。
迁移后的状态
现在 8 个服务全部跑在 Coolify 上,都是容器化部署:
| 服务 | 部署方式 |
|---|---|
| NestJS API | Dockerfile(服务器构建) |
| Admin 管理后台 | GHCR 镜像 |
| 5 个落地页 | Dockerfile(服务器构建) |
| tools-gunlabs | GHCR 镜像 |
| MySQL 8.0 | Coolify 托管 |
| Redis 7 | Coolify 托管 |
日常更新落地页只需要 push 代码,GitHub Actions 自动触发 Coolify Webhook 重部署。更新 admin-web 跑一个本地脚本,5 分钟内线上更新。
最重要的变化是心智负担降低了。Nginx 配置不用手动维护,SSL 证书不用操心,容器挂了 Coolify 自动重启,日志在面板里直接看。
那个 300 行的 deploy_all.sh 还在仓库里,作为历史文物保留着。
如果你也是用自托管服务器的独立开发者,Coolify 值得试试。安装文档在 coolify.io,整个安装过程不超过 10 分钟。