LinkBreeze自托管部署:Docker Compose与Caddy配置
使用Docker Compose和Caddy在VPS部署LinkBreeze,涵盖固定镜像标签、无Cookie点击统计,以及保存整个站点数据的唯一SQLite卷。
LinkBreeze 是什么
LinkBreeze 是一个可自行托管的 Linktree 替代方案:一个 Docker 容器同时提供公开的个人简介链接页面和管理后台,所有状态都保存在一个 SQLite 文件中。它采用 MIT 许可证,以 TypeScript 和 Next.js 编写,并以 ghcr.io/manak-hash/linkbreeze 形式发布。要运行它,您需要一台 VPS、一个将 A 记录指向该 VPS 的域名、已开放的 80 和 443 端口,以及带 Compose 插件的 Docker Engine。
本指南介绍该仓库实际支持的部署方式:在能够自行获取证书的反向代理后使用 Docker Compose。指南也会说明哪些情况会导致故障,因为个人简介中的链接是其他人会点击的公开 URL,链接失效就会损失点击。
在此之前,请先明确这个项目有多新。
LinkBreeze 是否足够成熟,可以用于公开个人资料链接?
截至 2026 年 8 月,该仓库有 178 个 stars、17 个 forks,并且只有一名维护者。第一个带标签的版本 v1.0.0 发布于 2026 年 7 月 1 日。这是一个发布了几周的项目,而不是已经维护了几年的项目。
The data behind this chart
[
{
"week": "2026-06-29",
"releases": 3,
"cumulative": 3
},
{
"week": "2026-07-06",
"releases": 3,
"cumulative": 6
},
{
"week": "2026-07-13",
"releases": 1,
"cumulative": 7
},
{
"week": "2026-07-20",
"releases": 2,
"cumulative": 9
},
{
"week": "2026-07-27",
"releases": 3,
"cumulative": 12
},
{
"week": "2026-08-03",
"releases": 2,
"cumulative": 14
},
{
"week": "2026-08-10",
"releases": 3,
"cumulative": 17
}
]自 v1.0.0 以来,该项目在 17 个日历周内发布了 7 个带标签的版本。撰写本指南时,图表中的最后一周尚未结束,但该周已经包含其中的 3 个版本。
请将其理解为两个独立的事实。维护者很活跃,bug 通常能在几天内修复。但 schema 和默认值仍在变化,因此部署后不再维护的实例,可能会逐渐与正在编写的代码产生很大差异。
该许可证可以降低最坏情况的风险。MIT 许可证、容器镜像和保存在您自己磁盘上的 SQLite 文件意味着,即使项目停止开发,您现有的实例仍可继续运行。但它无法避免另一种风险:面向公网的 Web 应用如果不再获得安全修复,长期运行后会成为负担。请将其作为需要持续更新的服务部署,并从第一天起确保下面的备份流程正常运行。
固定镜像标签,不要使用 latest
发布工作流每个版本只推送两个标签:latest,以及去掉前导 v 的版本号。因此,发布版本 v1.2.7 对应的固定标签是 ghcr.io/manak-hash/linkbreeze:1.2.7。写入 :v1.2.7 不会拉取任何内容,Docker 会报告 manifest unknown,因为该标签从未被推送。
请固定标签,因为 latest 会变化。按照上方图表中的发布频率,针对 latest 使用 docker compose pull,相当于未经审核地升级受众正在访问的页面。使用固定标签后,只有编辑文件时才会升级。
还需要注意镜像的构建方式。发布工作流构建镜像时未设置 platforms:,因此发布的镜像仅适用于 linux/amd64。在 arm64 主机上拉取会失败,并显示 no matching manifest for linux/arm64/v8 in the manifest list entries。如果您运行的是 ARM VPS,而不是 x86,请直接在该主机上构建镜像:
git clone --branch v1.2.7 --depth 1 https://github.com/Manak-hash/LinkBreeze.git
cd LinkBreeze
docker build -t linkbreeze:1.2.7 .然后在下方的 compose 文件中使用 linkbreeze:1.2.7 作为镜像名称。
在 Caddy 后部署 LinkBreeze 并自动启用 TLS
Caddy 会自行从 Let's Encrypt 请求和续期证书,因此无需单独执行 TLS(传输层安全)证书配置步骤。整个部署只需在同一目录中放置 3 个文件。
先生成密钥:
mkdir -p ~/linkbreeze && cd ~/linkbreeze
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .envSECRET_KEY用于签名管理会话 cookie,并为分析功能中的访客哈希添加盐值。仓库中发布的 compose 文件默认将其设置为 ${SECRET_KEY:-changeme-in-production}。如果跳过此步骤,实例将使用公开显示在 GitHub 上的会话签名密钥。请在首次启动前设置它,因为之后修改会使当前登录失效,并重置分析功能使用的盐值。
写入 docker-compose.yml:
services:
linkbreeze:
image: ghcr.io/manak-hash/linkbreeze:1.2.7
restart: unless-stopped
volumes:
- linkbreeze-data:/app/data
environment:
- DATABASE_PATH=/app/data/linkbreeze.db
- SECRET_KEY=${SECRET_KEY}
- BASE_URL=https://links.example.com
networks:
- linkbreeze-net
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
- linkbreeze-net
networks:
linkbreeze-net:
volumes:
linkbreeze-data:
caddy-data:
caddy-config:BASE_URL是可选配置,但建议设置。它会告知应用实际的公网地址,避免请求携带伪造的 Host 标头时,应用生成指向他人域名的链接。
在同一目录中写入 Caddyfile,替换为您自己的域名:
links.example.com {
encode zstd gzip
reverse_proxy linkbreeze:3000
}Caddy 默认会在代理请求中设置 X-Forwarded-For 和 X-Forwarded-Proto,分析功能依赖这两个标头。启动服务:
docker compose up -d
docker compose ps
docker compose logs -f caddydocker compose ps 应显示 LinkBreeze 容器的状态为 healthy。该镜像自带健康检查,即 wget --spider -q http://127.0.0.1:3000/api/health,因此无需自行添加。不要从仓库中的 Caddy 示例复制健康检查配置:它会调用 curl,而该镜像基于 node:22-alpine 构建,其中包含 busybox wget,但没有 curl。因此,该容器在页面运行正常时仍会报告 unhealthy。
在浏览器中打开 https://links.example.com。首次访问会进入 /setup 处的设置向导,用于创建唯一的管理账户。完成后,控制面板位于 /dashboard,登录表单位于 /login。该账户仅适用于此实例,应用没有单点登录集成。因此,如果您希望控制面板使用与其他托管服务相同的登录方式,必须在其前面配置转发认证代理,例如 自托管的 Authentik。
请注意,compose 文件没有发布 3000 端口。只有 Caddy 监听公网接口。如果您不熟悉 Compose 文件语法,VPS 的 Docker Compose 基础介绍了此文件所依赖的相关部分;如果您已经在前面运行了其他代理,Nginx、Caddy 和 Traefik 对比说明了需要进行哪些调整。仓库还提供了使用 Nginx 和 Certbot、Traefik 以及 Cloudflare 隧道的可用示例。
数据存储位置,以及备份必须包含的内容
DATABASE_PATH 指向 /app/data/linkbreeze.db。上传的头像和链接缩略图会写入其旁的 /app/data/uploads。两者都存储在名为 linkbreeze-data 的卷中,因此备份单位是整个卷,而不是单独的数据库文件。还原时如果缺少 uploads 目录,页面上的每张图片都会返回 404。
其他数据确实都在这一个数据库中:页面、链接、设置、主题、电子邮件订阅者和分析记录。
请在容器停止后复制数据:
docker compose stop linkbreeze
docker compose cp linkbreeze:/app/data ./backup-$(date +%F)
docker compose start linkbreeze必须先停止容器,因为进程写入 SQLite 数据库时复制文件,可能会复制到未完成的事务,之后副本会以损坏文件的形式打开。复制期间页面不可用。还原时反向执行相同操作:
docker compose stop linkbreeze
docker compose cp ./backup-2026-08-14/. linkbreeze:/app/data
docker compose start linkbreeze
docker compose logs -f linkbreeze控制面板还提供 JSON 导出功能,地址为 /api/backup,格式为 linkbreeze-backup-YYYY-MM-DD.json。导出内容包括个人资料、链接、设置和已保存的主题。不包括分析历史、电子邮件订阅者或上传的图片;还原时会先删除这四张表中的当前记录,再插入导出文件中的记录。请将其视为用于迁移主机或撤销编辑错误的配置快照。卷副本才是备份。
这里适用两条与其他场景相同的存储规则:在 VPS 上运行 SQLite 生产环境时,数据库应存储在本地磁盘上,因为 SQLite 在网络文件系统上的锁定机制不可靠,页面损坏通常只有在发生故障时才会暴露。若要将命名卷替换为主机绑定挂载,请先对主机目录执行 chown:容器以非 root 的 node 用户运行,该用户在 node:22-alpine 中的 uid 为 1000。root 创建的目录无法由该用户写入,因此应用无法打开数据库,容器会在启动时退出。Compose 中的绑定挂载与命名卷完整介绍了这种取舍。
无需使用的分析功能和同意横幅
这是一个足以证明自行托管页面合理性的功能,因为同类页面在其他地方可以免费获得。
分析功能不使用 Cookie。系统不会为访问者设置 Cookie,公共页面也不会加载第三方脚本。系统使用 IP 地址、用户代理字符串和盐值的 SHA-256 哈希识别访问者,并截取前 16 个十六进制字符。盐值本身是当前 UTC 日期和您的 SECRET_KEY 的哈希,因此会在 UTC 午夜发生变化,昨天的哈希无法与今天的哈希匹配。原始 IP 地址永远不会写入数据库。
点击在服务器端统计。公共页面上的每个 http 链接都指向您自己域名下的 /go/<id>,该端点记录点击,然后通过 302 重定向响应跳转到真实目标。因此,即使读者禁用了 JavaScript,或者使用会阻止后台请求的应用内浏览器,点击统计仍然有效。页面浏览量通过 /api/track 记录。
有两种情况不会计入统计。携带有效管理员会话的请求会被跳过,因此编辑自己的页面不会增加数据。已知的爬虫用户代理也会被跳过。
关于同意:系统不会在读者设备上存储任何内容,而 Cookie 横幅请求许可的对象正是存储在读者设备上的 Cookie。您仍需根据读者所在地区履行相应义务,因此请自行确认。但这里没有需要披露的跟踪 Cookie,也没有接收数据的第三方。
有一个容易让人意外的注意事项:轮换 SECRET_KEY 后,每日盐值也会随之变化,因此从该时刻起,每个回访者都会被计为新访问者。
为什么 analytics 国家列为空?
因为您的整个技术栈都没有设置国家/地区请求头。LinkBreeze 会从 cf-ipcountry 和 x-vercel-ip-country 等代理请求头解析国家/地区。在由您自己的 Caddy 或 Nginx 反向代理的 VPS 上,这些请求头都不存在,因此国家/地区会记录为 null,细分结果也会保持为空。容器内没有 GeoIP 数据库。
有两种方式可以填充该字段。将域名接入 Cloudflare,由 Cloudflare 为其代理的每个请求添加 cf-ipcountry。或者在您自己的反向代理中执行本地 GeoIP 查询,并据此设置其中一个请求头。
另一个相关问题更严重,请一并检查。点击和查看处理程序会优先从 X-Forwarded-For 读取客户端地址,然后读取 X-Real-IP;两个请求头都不存在时,则回退到 0.0.0.0。如果不使用前置代理,直接将端口 3000 发布到互联网,所有访问者都会被哈希为同一个值。这意味着唯一访问者数会永远显示为 1,并且每分钟 60 个事件的单 IP 速率限制会同时应用于所有访问者。在上方的 reverse_proxy 指令后面使用 Caddy 时,Caddy 会自动为您设置该请求头,这两个问题都会消失。
从 Linktree 导入,以及无法导入的内容
控制面板中的迁移向导接受公开个人资料 URL 或导出文件。它支持识别 linktr.ee、bento.me、lnk.bio、tap.link、hopp.bio、beacons.ai、solo.to、linkfly、mssg.me 和 LittleLink 页面,也支持通用 HTML 和 JSON 导出文件。对于 Linktree 或 Bento URL,它会读取这些页面嵌入的 __NEXT_DATA__ JSON。对于静态页面,它会读取锚点标签。
可以导入每个链接的标题、URL、描述和图片、该链接是否为社交资料,以及您的显示名称、个人简介和头像。在写入数据库前,您可以选择要保留的链接。
无法导入分析历史记录、主题和布局、电子邮件订阅者、计划发布日期,以及旧平台要求通过其登录系统才能访问的任何内容。您需要手动重建页面外观,并接受旧的点击历史记录仍保留在旧服务中。
导入器从您的服务器而不是浏览器获取 URL,因此会拒绝非公开地址。Private/local URLs are not allowed 表示您提供了自己网络内部的地址;拒绝访问是有意设计的。否则,任何拥有控制面板访问权限的人都可能利用您的服务器探测只有服务器能够访问的计算机。您还可能看到其他消息:Only http and https URLs are allowed、Request timed out 和 Response too large。
抓取依赖其他人的页面标记。如果向导在明显包含链接的页面上找不到任何内容,说明该平台在解析器编写后修改了 HTML。请手动添加链接,不要等待修复。如果您实际需要的是可度量的短链接,而不是个人资料页面,那么使用自托管的 URL 缩短器(例如 Shlink)即可满足需求,并且可以稳定运行在同一台服务器上。
更新固定版本的部署
# edit the image tag in docker-compose.yml, then
docker compose pull
docker compose up -d
docker compose logs -f linkbreeze容器启动时会自动运行架构迁移。目前没有文档说明如何回滚迁移,因此请先备份卷。无法回滚的升级只有在能够恢复升级前数据时才是安全的。
有新版本发布时,仪表板会显示提示横幅。它每24小时从项目的 GitHub 仓库获取一次小型版本文件进行检查,不会发送任何有关您实例的信息。修改标签前请先阅读发行说明,因为在项目当前阶段,小版本可能会更改您所依赖的默认设置。
故障模式和您将看到的字符串
拉取时出现 manifest unknown。 标签写成了 :v1.2.7。Registry 标签不包含 v,因此请使用 :1.2.7。
no matching manifest for linux/arm64/v8 in the manifest list entries。 已发布的镜像仅支持 amd64。请在 ARM 主机上从带标签的源代码构建它。
容器报告 unhealthy,但页面加载正常。 您的 compose 文件中的健康检查正在调用 curl,而该镜像不包含此命令。请删除该健康检查,让镜像自带的 wget 健康检查运行。
Caddy 提供证书错误,或完全没有响应。 检查 docker compose logs caddy。常见原因是 A 记录尚未指向此 VPS,或防火墙关闭了端口 80。这会阻止 Caddy 使用 ACME(自动证书管理环境)HTTP challenge 来证明其控制该域名。
Unique visitors 停留在 1。 没有代理设置 X-Forwarded-For,因此所有访客的哈希值都相同。
容器启动后立即退出,但昨天还可以正常运行。 如果您从命名卷改用了主机 bind mount,数据目录的所有者就是 root,而应用以 uid 1000 运行,因此无法打开数据库文件。请对主机目录执行 sudo chown -R 1000:1000。
跟踪请求返回 HTTP 429。 /api/track 和 /go/<id> 上的每 IP 限流已达到限制。访客仍会被重定向到目标地址,只是不会记录此次点击。
FAQ
LinkBreeze 是否适合用于公开的 bio 链接页?
这是一个较新的项目。截至 2026 年 8 月,该仓库有 178 个 stars、17 个 forks 和 1 名维护者,首个版本的发布日期是 1 July 2026。平均每周发布次数超过 2 次,因此 bug 修复很快,行为变化也很快。MIT license 和本地 SQLite 文件意味着即使项目停止开发,页面仍可正常运行;但缺少安全修复的公开 Web 应用会带来风险。因此,应将其视为需要持续更新的软件,而不是安装一次后长期不管。
应运行哪个 LinkBreeze 镜像标签?
运行版本标签,例如 ghcr.io/manak-hash/linkbreeze:1.2.7,并有意控制版本变更。发布工作流只推送 latest 和不带前缀的版本号,因此带有 v 的 :v1.2.7 不存在,Docker 会返回 manifest unknown。该镜像仅为 linux/amd64 构建,因此在 arm64 VPS 上必须克隆对应标签并在本地构建。
为什么 LinkBreeze analytics 中的国家分布一直为空?
LinkBreeze 从 cf-ipcountry 或 x-vercel-ip-country 等代理请求头中读取访问者国家信息,并不自带 GeoIP 数据库。使用自有 Caddy 或 Nginx 的 VPS 不会设置这些请求头,因此国家字段会保存为 null。可以将 Cloudflare 放在域名前面,或者让反向代理通过本地 GeoIP 查询设置其中一个请求头。
具体需要备份什么,如何恢复?
备份整个 linkbreeze-data 卷,而不只是数据库文件。/app/data/linkbreeze.db 保存所有链接、页面、设置、订阅者和 analytics 记录,/app/data/uploads 保存页面引用的头像和缩略图。停止容器,运行 docker compose cp linkbreeze:/app/data ./backup-$(date +%F),然后重新启动容器。恢复时,将目录复制回已停止的容器,再启动容器。控制台中的 JSON 导出文件只是 profile、链接、设置和主题的配置快照,其中不包含 analytics 或图片。
从 Linktree 导入时,会同时导入 analytics 和主题吗?
不会。迁移向导会从旧的公开 profile 中读取链接标题、URL、描述和图片,以及显示名称、bio 和头像。Analytics 历史记录、主题、电子邮件订阅者和计划发布日期不会被导入。导入后,请在主题编辑器中重新设置页面外观,并注意点击历史记录仍会保留在旧平台上。