Janus 是一个自托管的多租户短链服务。租户注册后管理自己的域名与短链,系统为每个已激活域名自动签发并续期 HTTPS 证书,把 域名/短码 的访问重定向到目标 URL。
除跳转外还提供:落地页型短链、访问明细与地理统计、租户级访问处置规则引擎、自定义错误页,以及供脚本调用的 Bearer JWT API。
- 单服务器部署 — Go(Gin + GORM)+ Vue 3 SPA(nginx)+ PostgreSQL 16 + Caddy(on-demand TLS),
docker compose up -d全部跑起来。 - 多租户隔离 — 域名、短链、规则、落地页在租户之间完全隔离;部署者持有平台管理员角色。
- 自带证书 — 平台域名配一条泛解析,每个租户自动获得
<slug>.<平台域名>;自有域名激活后由 Caddy 自动签发 Let's Encrypt 证书。 - 前后端分离 — 前端独立构建为 nginx 镜像,后端镜像内无任何前端文件(ADR-0006)。
Internet → Caddy(80/443, on-demand TLS)
├── / → nginx(前端 SPA)
└── /api/*, /{code} → Go 后端 → PostgreSQL 16
cmd/janus/ # 后端入口(加载配置 → 连库 → 初始化脚本 → worker → HTTP)
internal/ # httpapi(routes/handlers)、rules(规则引擎)、store、domain、geo、rbac…
migrations/init.sql # 唯一 schema 事实来源,启动时自动幂等执行
web/ # 前端 SPA(Vue 3 + Vite + Tailwind 4)
docker/ # Dockerfile(backend / web 两个目标)+ nginx.conf
docker-compose.yml # 开发编排(Postgres + Caddy)
docker-compose.prod.yml, Caddyfile.prod # 生产编排
docs/ # deploy.md、testing.md、adr/
| 工具 | 版本 |
|---|---|
| Docker Desktop(含 Compose v2) | 较新版本 |
| Go | ≥ 1.26 |
| Node.js | ≥ 20.19 |
| pnpm | ≥ 9(仓库锁定 10.x) |
| Caddy CLI | ≥ 2.x,开发流程第 3 步信任本地 CA 需要(生产不需要) |
开发环境基础设施进 Docker(Postgres、Caddy),后端与前端在终端跑,改代码即时生效。
# 从模板生成 .env,并把必填的 on-demand TLS 密钥直接填进去
# 重定向会覆盖已有的 .env(数据库密码等手工改动会一起丢):重复执行前先备份
sed "s/^JANUS_CADDY_ASK_TOKEN=.*/JANUS_CADDY_ASK_TOKEN=$(openssl rand -hex 32)/" .env.example > .env
docker compose up -d # postgres:5432、caddy:80/443编辑 /etc/hosts(需要管理员 / sudo 权限):
127.0.0.1 app.janus.test
127.0.0.1 alice.janus.test # 每注册一个租户加一行,hosts 不支持通配符
这些行只服务于浏览器与 Caddy:平台默认子域 <slug>.janus.test 建号即 active,不做 DNS 校验;
自有域名的激活校验走真实 DNS(实现刻意绕开 /etc/hosts),往 hosts 里加行无效,详见 docs/deploy.md 第 5 节。
mkdir -p certs
docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt > certs/caddy-root.pem
sudo caddy trust --ca certs/caddy-root.pem以下命令都在仓库根目录执行(JANUS_MIGRATIONS_DIR 默认是相对路径 migrations,换目录会读不到 init.sql):
# go run 不读 .env(那是 compose 的行为):set -a 把 .env 里的变量注入进程环境
set -a; . ./.env; set +a
JANUS_COOKIE_SECURE=false JANUS_ADDR=:8080 go run ./cmd/janus这一步的 set -a; . ./.env 不能省:后端的 JANUS_CADDY_ASK_TOKEN 只从进程环境读,
而 Caddy 容器用的是 .env 里那一份。两边不一致时授权端点一律 403(fail-closed),
表现为 Caddy 一张证书都签不出来(https://app.janus.test/healthz 握手失败、租户域名全不可用),
后端日志只有一行 "JANUS_CADDY_ASK_TOKEN 未配置"。
也可以直接跑 ./run_backend.sh(做同样的事,并先 cd 到仓库根)。
corepack enable # 本机没有 pnpm 时:corepack 会按 packageManager 字段装上 10.x
cd web && pnpm install && pnpm dev # http://localhost:5173,/api 代理到 :8080curl -s http://127.0.0.1:8080/healthz # → {"status":"ok"}
curl -sk https://app.janus.test/healthz # 经 Caddy 走通全链路浏览器打开 http://localhost:5173 注册账号(账号 + 密码 + 域名前缀),注册即生效,不再需要邮箱验证。
Caddy 占用宿主 80/443,与本机其他绑定这两个端口的服务错开启动。
要验证平台管理端,用内置超管账号 superadmin / 123456 登录(启动时幂等创建)。
首次登录会被强制跳 /account 改密码,改完才能访问其它页面。
改了 migrations/init.sql 之后要连数据一起重建(项目约定不写增量迁移):
docker compose down -v && docker compose up -d # -v 删掉 pgdata 卷,数据全清-v 会连 caddy_data 一起删掉,本地 CA 根证书随之重建:要重做第 3 步的 caddy trust,
否则浏览器会对 https://*.janus.test 报证书不受信任。
全部配置走环境变量,模板与逐项说明见 .env.example,默认值可直接用于开发。常用项:
| 变量 | 开发默认值 | 说明 |
|---|---|---|
JANUS_ADDR |
:8080 |
HTTP 监听地址 |
JANUS_PLATFORM_DOMAIN |
janus.test |
平台裸域名,租户默认域名 <slug>.<平台域名> |
JANUS_SERVER_PUBLIC_IP |
127.0.0.1 |
自有域名 DNS 激活校验的比对地址 |
JANUS_DATABASE_URL |
postgres://janus:janus@localhost:5432/janus?sslmode=disable |
后端连接串 |
JANUS_COOKIE_SECURE |
false |
会话 cookie Secure 标记,http 开发必须 false |
JANUS_JWT_SECRET |
空 | JWT 签名密钥;留空则每次启动随机生成(重启后 token 失效) |
JANUS_CADDY_ASK_TOKEN |
空(必填) | on-demand TLS 授权端点共享密钥,compose 与后端同值注入 |
JANUS_DNS_RETRY_INTERVAL / JANUS_DNS_MAX_AGE |
5m / 72h |
域名激活重试间隔 / 最长等待 |
JANUS_VISIT_RETENTION |
2160h |
访问记录保留时长(90 天) |
.env含数据库密码等敏感配置,已在.gitignore中。平台管理员是内置账号
superadmin/123456,启动时幂等创建,首次登录强制改密;不需要配置项。
完整步骤与上线检查清单见 docs/deploy.md。最小流程:
cp .env.example .env && chmod 600 .env
# 下面几项留空会让 docker compose 直接报错退出(必填),先在 .env 里填好:
# JANUS_PLATFORM_DOMAIN 裸平台域名,如 example.com
# JANUS_SERVER_PUBLIC_IP 服务器公网 IP
# JANUS_DB_PASSWORD 别留模板里的 janus
# JANUS_TRUSTED_PROXY_CIDRS 172.16.0.0/12(Docker 默认网桥段)
# JANUS_CADDY_ASK_TOKEN openssl rand -hex 32
# 另外建议同时设 JANUS_JWT_SECRET(openssl rand -hex 32);超管是内置的 superadmin / 123456,无需配置
# Caddyfile.prod 的站点地址不支持环境变量:把 example.com 换成你的平台域名(与 .env 的 JANUS_PLATFORM_DOMAIN 一致)。
# -i.bak 在 macOS(BSD sed)与 Linux(GNU sed)下都可用,故随后删掉备份文件。
sed -i.bak 's/example\.com/your-domain.com/g' Caddyfile.prod && rm Caddyfile.prod.bak
docker compose -f docker-compose.prod.yml up -d --build前置条件是 DNS:平台裸域名与泛解析 *.<平台域名> 均指向服务器公网 IP。首次启动自动执行初始化脚本、创建免费档种子、初始化超管;证书按需签发。
每个 v* 标签会产出两个镜像(ghcr.io/ruanbw/janus-backend / ghcr.io/ruanbw/janus-web)。
不想在服务器上装 Go 与 Node 时,叠加覆盖文件、加 --no-build 即可:
JANUS_VERSION=1.0.0 docker compose \
-f docker-compose.prod.yml -f docker-compose.images.yml up -d --no-build--no-build 不能省(两份文件合并后 service 上仍有 build:,否则会先构建再打标签)。
一个已知限制:注册页的域名提示是构建期烘进前端的,发布镜像用中性默认值;登录后的平台域名由后端下发。
要让注册页显示你自己的域名,需从源码构建。
首次上线后还有三件事不在这一段里:超管首登(superadmin / 123456,登录后强制改密,见 docs/deploy.md 第 3 节)、升级与回滚、备份(尤其 caddy_data 卷,丢了要重签全部证书)——分别见 docs/deploy.md 第 3、8、9 节。
- 提交前:后端
gofmt -l internal cmd、go vet ./...、定向go test -count=1 ./internal/<pkg>/;前端pnpm type-check+pnpm build+pnpm check:ui。 - 测试即规格:需求以 Given-When-Then 测试沉淀,日常只跑被改动包的定向用例,不跑
go test ./...。详见docs/testing.md。 - 数据库:
migrations/init.sql是唯一 schema 事实来源,不写增量迁移;改表直接改脚本并 DROP 开发库重建。 - 新增受保护路由:必须同步
internal/rbac/rbac.go的policyText,漏了会被 403。 - 架构取舍:写
docs/adr/NNNN-*.md(背景 → 权衡 → 后果)。
| 文档 | 内容 |
|---|---|
docs/deploy.md |
生产部署指南与上线检查清单 |
docs/testing.md |
测试流程与分层验证 |
web/README.md |
前端技术栈、页面清单、请求约定 |
web/UI_KIT.md |
前端组件契约与三层设计令牌 |
docs/adr/ |
架构决策记录 |
GNU Affero General Public License v3.0(AGPL-3.0),全文见 LICENSE。
Copyright (C) 2026 ruanbw and Janus contributors
SPDX-License-Identifier: AGPL-3.0-only
可自由使用、修改、再分发,包括闭源商用;但通过网络向用户提供本程序功能(部署成在线服务)时,必须向这些用户提供对应源码。商业授权 / 闭源分发需求请单独联系作者。