1. 为什么我又折腾了一个自托管部署平台
第一次接触 Vercel 那种「push 代码就自动上线、每个分支都有独立预览地址」的体验时,我确实被惯坏了。后来手上项目变多,有些是公司内网服务,有些是客户要求数据必须落在自己机房,还有些纯粹是我自己想省点托管费,于是「把 Vercel 的体验搬回自己服务器」这件事就反复被提上日程。Openship 就是我在这个背景下挖到的一个自托管部署平台,它的定位很直接:你有一台自己的服务器,装好 Docker,它帮你把 Git 仓库到线上服务的整条链路管起来,包括构建、部署、域名、HTTPS、回滚这些琐事。
这篇文章不是官方文档的搬运,而是我把它从零跑起来、踩了几个坑、又拿它部署了两三个真实项目之后的一份实操记录。我会讲清楚它到底解决了什么问题、核心链路是怎么设计的、Docker 和 OpenResty 在里面各扮演什么角色、以及那些文档里不会写但实际一定会遇到的坑。适合谁看?如果你已经会用 Docker、懂一点 Linux 和 Nginx/OpenResty,又想给自己的项目搞一套「私有版 Vercel」,那这篇基本可以照着抄。如果你连 Docker 都没装过,建议先把 Docker 基础过一遍再回来,不然中间很多环节会卡住。
先说结论性的判断:Openship 这类自托管部署平台的价值,不在于它比 Vercel 功能更强,而在于控制权。代码、构建产物、运行环境、数据全在你自己手里,代价是你得自己维护服务器、自己处理证书续期、自己扛住流量。这笔账划不划算,取决于你的项目性质,而不是平台本身好不好。
2. Openship 到底是个什么东西
2.1 一句话拆解它的核心能力
把 Openship 拆开看,它本质上是一个「Git 驱动的部署编排器」。你给它一个仓库地址,它负责拉代码、识别项目类型、跑构建命令、把产物打成镜像或静态文件、再通过反向代理暴露出去。整个过程和你熟悉的 Vercel 工作流几乎一致:连仓库、配环境变量、点部署、拿域名。
它和纯 CI/CD 工具(比如 Jenkins、GitLab CI)的区别在于抽象层级。Jenkins 给你的是「流水线原语」,你得自己写每一步;Openship 给你的是「部署原语」,构建、发布、路由、证书这些它已经封装好了,你只需要填几个关键参数。这也是为什么它敢说自己「把 Vercel 的体验搬过来」——它卖的是开箱即用的部署体验,不是无限灵活的流水线。
2.2 它和 Vercel、Netlify 的本质差异
很多人第一反应是「这不就是个开源的 Vercel 吗」。功能层面确实像,但底层逻辑完全不同。Vercel 是托管服务,你的代码跑在它的边缘网络上,构建也在它的机器上完成;Openship 是自托管,所有东西都跑在你自己那台机器上。
这个差异带来几个直接后果。第一,构建资源由你决定,你服务器 CPU 强、内存大,构建就快,反之就慢,没有平台帮你弹性扩容。第二,网络位置由你决定,用户访问速度取决于你服务器所在机房和带宽,而不是全球边缘节点。第三,数据合规由你掌控,这对很多有数据落地要求的场景是刚需。第四,成本模型变了,从按量付费变成固定服务器成本,流量小的项目反而更贵,流量大的项目可能省很多。
所以别把 Openship 当成「免费 Vercel」,它更像是「用一台服务器换一套部署体验」。想清楚这个前提,后面的取舍就顺了。
2.3 技术栈里 Docker 和 OpenResty 的分工
Openship 的运行时底座是 Docker,这点很关键。每个部署单元最终都会变成一个或多个容器,构建在容器里跑,运行也在容器里跑,隔离性和可复现性都靠它。你服务器上装好 Docker 和 Docker Compose,基本就满足了运行前提。
反向代理层用的是 OpenResty,也就是 Nginx 加 Lua 的那套组合。为什么不用裸 Nginx?因为动态路由、证书自动签发、按域名分流这些逻辑,用 Lua 在 OpenResty 里处理比反复改 Nginx 配置文件优雅得多。你访问服务时看到的 443、80 端口,背后就是 OpenResty 在转发。顺带提一句,很多人搜「403 forbidden openresty」就是因为代理配置里目录权限或 root 路径写错了,这个后面排查章节会细讲。
3. 部署前的环境准备与关键决策
3.1 服务器规格怎么选才不踩坑
这是最容易被低估的一步。Openship 本身不重,但你的项目构建可能很吃资源。我的经验是:构建峰值内存至少按项目最重的那次构建来估。一个中等规模的 Node 前端项目,构建时 Node 进程吃到 1.5G 到 2G 内存很常见,如果你服务器只有 2G 内存,构建大概率被 OOM Killer 干掉,表现为「构建莫名其妙失败,日志里啥也没有」。
我的推荐配置分三档。个人玩具项目、纯静态站,2 核 2G 够用,但要开 swap。中小型全栈项目,2 核 4G 是舒适区。要跑多个项目、还带数据库的,直接上 4 核 8G,别省这个钱。磁盘方面,Docker 镜像和构建缓存很占空间,40G 起步,建议 80G 以上,否则跑几个月就得手动清镜像。
提示:如果你在 Windows 上用 Docker Desktop 做本地测试,遇到「virtualization support not detected」或「failed to start because virtualization support wasn't detected」,先去 BIOS 里把虚拟化(VT-x / AMD-V)打开,这是最常见的原因,和 Openship 本身无关。
3.2 Docker 安装与几个必调参数
Linux 上装 Docker 现在很省事,官方脚本一把梭:
curl -fsSL https://get.docker.com | sh sudo systemctl enable --now docker装完别急着往下走,有几个参数必须调。第一,日志驱动和大小限制。Docker 默认的 json-file 日志会无限增长,跑久了能把磁盘吃满。在/etc/docker/daemon.json里加上:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }第二,镜像加速。国内拉镜像慢是常态,配好镜像源能省大量时间。第三,把当前用户加进 docker 组,否则每条命令都要 sudo:
sudo usermod -aG docker $USER改完记得重新登录生效。这里有个高频坑:很多人改完没重登,然后一直报 docker 权限错误,以为是配置没生效,其实是会话没刷新。
3.3 域名与证书的前置规划
Openship 要接管域名和 HTTPS,所以你得提前把 DNS 规划好。我的做法是准备一个泛解析记录,比如*.deploy.example.com指向服务器 IP,这样每个项目分配一个子域名,不用每次手动加解析。证书方面,Openship 一般走 ACME 自动签发,前提是 80 端口能被外部访问到,用于域名验证。
这里有个容易忽略的点:如果你的服务器前面还有一层云厂商的负载均衡或 CDN,80 端口的验证请求可能到不了 Openship。要么把验证路径透传,要么改用 DNS 验证方式。我一开始就是卡在这,证书一直签不下来,排查半天才发现是上游把 80 端口拦了。
4. 核心链路拆解:从 Git 到线上服务
4.1 构建阶段:识别项目类型与产物
Openship 拿到仓库后,第一件事是判断这是个什么项目。常见类型有静态站、Node 服务、Python 服务、Dockerfile 项目等。判断逻辑一般基于仓库里的标志文件,比如有package.json就当 Node 项目,有Dockerfile就优先用 Dockerfile 构建。
这一步的实操要点是构建命令和环境变量。以 Node 项目为例,构建命令通常是npm ci && npm run build,注意用ci而不是install,前者严格按 lock 文件装依赖,可复现性更好。环境变量分两类:构建时变量和运行时变量。前端项目里NEXT_PUBLIC_或VITE_开头的变量是在构建时被「烧」进产物的,改了必须重新构建;后端服务的数据库连接串这类是运行时注入的,改了重启即可。搞混这两类,会出现「我明明改了配置怎么没生效」的经典问题。
4.2 镜像与容器:部署单元的落地
构建完成后,产物会被打成镜像或直接作为静态文件挂载。Dockerfile 项目的流程最直观:docker build出镜像,docker run起容器。非 Dockerfile 项目,Openship 会用内置的模板生成镜像,比如 Node 服务用官方 node 基础镜像加你的产物。
这里我强烈建议自己写 Dockerfile,哪怕平台能自动生成。原因有三:一是多阶段构建能把镜像从几百兆压到几十兆,二是你能精确控制基础镜像版本,避免某天平台模板升级导致构建行为变化,三是排查问题时你对自己的镜像结构心里有数。一个典型的多阶段 Node 构建长这样:
FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000 CMD ["node", "dist/server.js"]4.3 路由与代理:OpenResty 怎么把请求送对地方
容器起来后监听的是内部端口,外部访问要靠 OpenResty 转发。Openship 会为每个部署生成一段路由配置,把域名映射到对应的容器端口。这块的核心是域名到服务的映射关系和证书绑定。
实际使用中,我建议给每个项目分配独立子域名,而不是用路径前缀区分。原因很简单:路径前缀会让前端资源引用、Cookie 作用域、CORS 配置都变复杂,子域名则天然隔离。OpenResty 处理子域名转发时,server_name写具体域名,proxy_pass指向容器地址即可。如果你遇到 403 forbidden,八成是root指向的目录权限不对,或者index文件不存在,先查这两处。
4.4 发布与回滚:怎么做到「一键切回上一版」
这是自托管平台最容易被做砸的地方。好的发布流程应该是原子切换:新版本容器先起来、健康检查通过、再把流量切过去,旧容器保留一段时间以便回滚。Openship 的思路基本是这个,但具体实现依赖它怎么管理容器和路由。
我的实操建议是:永远保留最近两到三个版本的镜像,别让清理策略把旧镜像全删了。回滚时如果镜像还在,切回去就是秒级的事;如果镜像没了,就得重新构建,那回滚就变成了「重新部署一个旧 commit」,体验差很多。另外,数据库迁移这类有状态操作要单独考虑,回滚代码不等于回滚数据,这点必须提前想清楚。
5. 完整实操:从零部署一个真实项目
5.1 服务器初始化与 Openship 安装
假设你有一台干净的 Ubuntu 22.04 服务器,第一步装 Docker,按前面 3.2 的步骤来。装完验证:
docker --version docker compose version两个命令都有输出,说明环境 OK。接着拉 Openship 的部署文件。这类自托管平台通常提供 docker compose 方式安装,大致流程是下载 compose 文件和配置模板,改几个关键变量,然后docker compose up -d。
关键变量一般包括:管理后台的访问域名、管理员账号密码、数据存储路径、以及 ACME 证书用的邮箱。数据存储路径一定要挂到宿主机上,别用容器内的匿名卷,否则容器重建数据就没了。我一般挂到/opt/openship/data这种固定路径,方便备份。
5.2 接入 Git 仓库与首次部署
进管理后台,添加 Git 仓库。这里有两种方式:公开仓库直接填地址,私有仓库需要配访问凭证。私有仓库建议用部署密钥或访问令牌,别用账号密码,令牌权限也尽量收窄到只读。
添加仓库后,Openship 会让你选分支、配构建命令、填环境变量。第一次部署我建议先用最简单的项目试水,比如一个纯静态 HTML 站,确认整条链路通了,再上复杂项目。这样出问题时变量少,好排查。
首次部署的日志要盯紧。构建阶段看依赖装没装上、构建命令有没有报错;运行阶段看容器起没起来、健康检查过没过;路由阶段看域名解析对不对、证书签没签下来。这三段任何一段出问题,表现都是「部署失败」,但原因完全不同。
5.3 环境变量与密钥管理
环境变量是部署里最容易出事的地方。我的原则是:敏感信息只放运行时变量,绝不进代码仓库。数据库密码、API 密钥、JWT secret 这些,全部通过平台的环境变量功能注入。
还有个细节:环境变量里的特殊字符要转义。比如密码里带$或#,在某些 shell 解析场景下会被当成变量或注释,导致实际注入的值和你以为的不一样。我踩过一次,数据库密码里有个$,结果连接一直失败,排查半天才发现是变量被提前展开了。解决办法是用单引号包裹,或者干脆避免在密码里用这些字符。
5.4 绑定域名与 HTTPS 生效
域名绑定分两步:DNS 解析和平台配置。DNS 那边加一条 A 记录或泛解析指向服务器 IP,平台这边填上域名,等证书签发。证书签发通常几十秒到几分钟,取决于 ACME 服务的响应速度。
验证 HTTPS 是否生效,别只看浏览器绿锁,用命令行更靠谱:
curl -I https://your-app.example.com看返回头里有没有正常的 200,以及证书信息对不对。如果返回 403,回到 4.3 那节查 OpenResty 配置;如果连接超时,查防火墙和安全组有没有放行 443。
6. 常见问题与排查技巧实录
6.1 构建失败类问题速查
构建失败是最常见的一类,表现是日志里报错然后流程中断。我整理了一张速查表,覆盖我实际遇到过的几种:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 构建进程被杀死,无明确报错 | 内存不足触发 OOM | 加 swap 或升配,看dmesg有无 oom 记录 |
| 依赖安装超时 | 镜像源慢或网络不通 | 换镜像源,检查服务器出网 |
| 构建命令找不到 | 基础镜像里没这个命令 | 换基础镜像或自己写 Dockerfile |
| 构建成功但产物为空 | 构建输出目录配错 | 核对dist/build等输出路径 |
OOM 这个特别隐蔽,因为日志里往往只有一句「Killed」,没有任何堆栈。判断方法是在构建时另开一个终端跑dmesg -T | tail,如果看到 oom-killer 相关记录,基本就实锤了。
6.2 运行与网络类问题排查
容器起来了但访问不通,这类问题排查顺序很重要。我的习惯是从内到外:先进容器看服务本身通不通,再看容器端口映射,最后看 OpenResty 转发。
# 进容器 docker exec -it <container> sh # 容器内自测 curl localhost:3000容器内能通、外部不通,问题就在代理层或防火墙。容器内就不通,问题在应用本身,看应用日志。这个顺序能帮你快速定位问题在哪一层,避免瞎猜。
6.3 证书与域名类问题
证书签不下来,最常见三个原因:80 端口不通、DNS 没生效、域名被上游拦截。排查时先dig一下域名看解析对不对,再curl一下 80 端口看能不能通。如果服务器前面有 CDN 或负载均衡,记得验证请求要能透传到 Openship。
还有个坑是证书续期失败。ACME 证书一般 90 天有效期,自动续期依赖定时任务。如果续期任务因为某种原因挂了,某天你会突然发现网站证书过期。建议加个监控,证书剩余天数少于 15 天就告警。
6.4 我踩过的几个真实坑
第一个坑是磁盘被镜像撑满。跑了两三个月,某天部署突然失败,一查磁盘 100%。原因是旧镜像和构建缓存没清理。解决办法是配定期清理策略,docker system prune要慎用,会删掉所有未使用资源,最好用带过滤条件的版本,保留最近几个版本。
第二个坑是环境变量改了没生效。前端项目改了VITE_变量,重新部署还是旧值。原因是这类变量在构建时被烧进产物,必须触发重新构建,光重启容器没用。这个坑我踩了不止一次,后来养成习惯:改前端环境变量,一定手动触发一次完整构建。
第三个坑是回滚时发现旧镜像没了。有次线上出问题想回滚,结果清理策略把旧镜像删了,只能重新构建旧 commit,多花了好几分钟。从那以后我把镜像保留数量调到 5,宁可多占点磁盘。
7. 自托管部署的取舍与我的实际体会
用了一段时间 Openship,我对「自托管部署平台」这件事有了更清醒的认识。它确实能把 Vercel 那套体验搬回来,但搬回来的只是体验,不是 Vercel 背后的基础设施。你的构建速度取决于你的服务器,你的可用性取决于你的运维水平,你的访问速度取决于你的机房。这些是自托管的固有代价,不是 Openship 的锅。
反过来说,如果你需要数据落地、需要成本可控、需要完全掌控运行环境,那这套方案的价值就体现出来了。我现在把几个内部工具和客户项目都放在上面,日常 push 代码自动部署,体验和用 Vercel 差别不大,但数据全在自己手里,心里踏实。
最后分享一个我一直在用的小技巧:给每个项目配一个健康检查端点,比如/healthz,返回 200 就行。Openship 发布时可以用它判断新版本是否真的起来了,避免把流量切到一个起不来的容器上。这个端点实现成本极低,但能挡掉很多「部署成功但服务不可用」的尴尬情况。另外,服务器上的 Docker 和 Openship 本身也要定期更新,安全补丁这东西,平时不觉得,出事就晚了。