通过本地代理和 Cloudflare Tunnel,把内网自建 GitLab 暴露成 Linear Cloud 可以访问的 GitLab URL。
目标链路:
Linear Cloud -> 公网 HTTPS 域名 -> Cloudflare Tunnel -> Docker proxy -> 内网 GitLab
内网 GitLab -> Linear webhook URL
这个项目不实现自定义 Linear 集成。它只负责让内网 GitLab 看起来像一个公网可访问的 self-hosted GitLab,然后 Linear 继续使用官方 GitLab 集成流程。
- 运行一个 Bun HTTP GitLab 代理。
- 将所有请求转发到
UPSTREAM_BASE_URL。 - 将 GitLab 返回中的内网 URL 改写成
PUBLIC_BASE_URL,覆盖Location、Link、JSON、HTML 和纯文本响应。 - 通过 Docker Compose 运行 Bun proxy 和 Cloudflare Tunnel。
- 不保存 GitLab token。Linear 会把
PRIVATE-TOKEN发给代理,代理只负责转发给 GitLab。
开始前需要准备:
UPSTREAM_BASE_URL:这台机器或 Docker 容器能访问的内网 GitLab 地址,例如https://gitlab.internal.example.com。PUBLIC_BASE_URL:给 Linear 访问的公网 HTTPS 地址,例如https://gitlab-linear.example.com。CLOUDFLARED_TUNNEL_NAME:Cloudflare Tunnel 名称,例如linear-gitlab-sync。CLOUDFLARED_TUNNEL_ID:由cloudflared tunnel create生成。CLOUDFLARED_CREDENTIALS_FILE:cloudflared tunnel create生成的 tunnel credential JSON 绝对路径。- 给 Linear 使用的 GitLab token:建议使用
apiscope。token 所属用户必须能访问要接入的 GitLab 项目。
安装依赖:
brew install cloudflared
docker --version
docker compose version
bun --version创建本地配置:
cp .env.example .env编辑 .env:
NODE_ENV=development
HOST=127.0.0.1
PORT=3100
HOST_PORT=3100
LOG_LEVEL=info
UPSTREAM_BASE_URL=https://gitlab.internal.example.com
PUBLIC_BASE_URL=https://gitlab-linear.example.com
CLOUDFLARED_TUNNEL_ID=
CLOUDFLARED_TUNNEL_NAME=linear-gitlab-sync
CLOUDFLARED_CREDENTIALS_FILE=/absolute/path/to/.cloudflared/<tunnel-id>.json
CLOUDFLARED_CONFIG_PATH=cloudflared/config.yml
ALLOWED_IPS=登录 Cloudflare 并创建 named tunnel:
cloudflared tunnel login
cloudflared tunnel create linear-gitlab-sync创建命令会输出 tunnel ID,并生成 credential 文件,通常在:
~/.cloudflared/<tunnel-id>.json
把生成的值写回 .env:
CLOUDFLARED_TUNNEL_ID=<tunnel-id>
CLOUDFLARED_CREDENTIALS_FILE=/absolute/path/to/.cloudflared/<tunnel-id>.json创建 DNS route。这里使用 PUBLIC_BASE_URL 的 hostname,不要带 https://:
cloudflared tunnel route dns linear-gitlab-sync gitlab-linear.example.com生成本地 Cloudflare Tunnel 配置:
bun run cloudflared:write-config启动 Docker Compose:
bun run docker:up验证服务:
bun run docker:ps
curl http://127.0.0.1:3100/healthz
curl https://gitlab-linear.example.com/healthz验证 GitLab token 是否能通过代理:
curl -i \
-H "PRIVATE-TOKEN: <gitlab-token>" \
https://gitlab-linear.example.com/api/v4/user预期结果是 HTTP/2 200。
在 Linear 中:
- 打开 GitLab integration 设置。
- 选择 self-hosted GitLab。
- GitLab URL 填公网域名,例如
https://gitlab-linear.example.com。 - 填入带
apiscope 的 GitLab token。 - 完成 Linear 的连接流程。
连接成功后,Linear 会给出 webhook URL 和 webhook secret。
在 GitLab 中:
- 打开 Project 或 Group 的 webhook 设置。多个项目建议使用 Group webhook。
- 粘贴 Linear 给出的 webhook URL。
- 粘贴 Linear 给出的 webhook secret。
- 启用这些 triggers:
Push events、Comments、Merge request events、Pipeline events。 - 保持 SSL verification 开启。
- 保存 webhook。
GitLab 配置 webhook 通常需要 Maintainer 或更高权限。
创建一个 Linear issue,然后创建一个 GitLab branch 或 merge request,标题里包含 Linear issue ID:
ABC-123 test GitLab integration
创建 merge request 后,Linear issue 页面应该出现 GitLab 活动。merge request 合并后,Linear 可能会按集成设置把 issue 移到完成状态。
启动或更新:
bun run docker:up查看状态:
bun run docker:ps查看日志:
bun run docker:logs重启:
bun run docker:restart停止:
bun run docker:downTunnel credential JSON 是敏感文件,不要提交到 Git。仓库已经忽略:
cloudflared/config.yml
cloudflared/*.json
cloudflared/*.pem
.env
如果只是停止后重新启动 tunnel:
bun run docker:down
bun run docker:up不需要修改凭证。
如果本地 credential JSON 被删除,但 tunnel 仍然存在:
cloudflared tunnel login
cloudflared tunnel token --cred-file /absolute/path/to/.cloudflared/<tunnel-id>.json <tunnel-name-or-id>然后更新 .env:
CLOUDFLARED_CREDENTIALS_FILE=/absolute/path/to/.cloudflared/<tunnel-id>.json重新生成配置并重启:
bun run cloudflared:write-config
bun run docker:up如果要重新创建 tunnel:
cloudflared tunnel delete <old-tunnel-name-or-id>
cloudflared tunnel create linear-gitlab-sync
cloudflared tunnel route dns linear-gitlab-sync gitlab-linear.example.com然后把新的 tunnel ID 和 credential 路径写入 .env,重新生成配置并启动:
bun run cloudflared:write-config
bun run docker:up如果更换 Cloudflare 账号:
cloudflared tunnel login
cloudflared tunnel create linear-gitlab-sync
cloudflared tunnel route dns linear-gitlab-sync gitlab-linear.example.com公网域名所在的 DNS zone 必须在新的 Cloudflare 账号里。如果域名还在旧账号,要么迁移 zone,要么换用新账号里的域名。
检查 Docker 是否在运行:
docker compose ps检查 Cloudflare 是否能访问 proxy:
curl https://gitlab-linear.example.com/healthz检查 GitLab 是否收到 token:
curl -i \
-H "PRIVATE-TOKEN: <gitlab-token>" \
https://gitlab-linear.example.com/api/v4/personal_access_tokens/self检查 token 是否能列出项目:
curl -i \
-H "PRIVATE-TOKEN: <gitlab-token>" \
"https://gitlab-linear.example.com/api/v4/projects?membership=true&per_page=1&pagination=offset"这两条都应该返回 200。
如果 Linear 能连接 GitLab,但 webhook 没有更新 issue,去 GitLab webhook 的 delivery 记录里查。这个方向是:
内网 GitLab -> Linear 给出的 webhook URL
这条链路不经过本代理。
- 不要把 GitLab token 写进本项目的
.env。GitLab token 应该配置在 Linear 中。 - 不要在 GitLab 公网域名前面加 Cloudflare Access 登录页,否则 Linear 无法直接调用 GitLab API。
- 建议给 Linear 单独创建 GitLab bot 或 service account。
- Cloudflare tunnel credential JSON 不要提交到 Git。
ALLOWED_IPS可以限制访问来源,但建议先确认 Linear GitLab integration 请求实际使用的来源 IP 后再开启。
当前可提交文件不应包含真实 GitLab 域名、真实 Cloudflare 域名、本地绝对路径、tunnel ID、credential JSON 或 token。
发布到公开 GitHub 前建议检查:
git grep -n -E "your-company|internal-domain|real-domain|tunnel-id|token|secret" HEAD
git status --short如果仓库历史里曾经提交过真实域名、tunnel ID 或本地路径,不要直接 push 整个历史。可以选择:
- 用
git archive HEAD导出当前干净快照再创建新仓库。 - 或重写 Git 历史,把敏感内容从所有历史提交中移除。