使用 Shlink 和 Docker Compose 自托管短链接服务
在 VPS 上部署 Shlink 5.1 和 Web 客户端 4.8,配置短域名 DNS、Postgres、HTTPS 与 API 密钥,并启用二维码和点击统计。
构建内容
自托管 URL 缩短服务是一种小型服务器,可将长链接转换为由您控制的短链接,并统计每次点击。推荐使用 Shlink:它是开源软件,提供 Docker 镜像,并可通过一个容器和一个数据库完成全部工作。本指南将在 VPS 上部署 Shlink,通过正式的短域名提供服务,并配置 HTTPS、API 密钥、二维码和点击统计。
要实现类似商业 URL 缩短服务的体验,需要两个组件。API 服务器负责响应重定向请求并保存数据。Web 客户端是独立的静态应用,浏览器通过它与 API 通信。您可以同时运行这两个组件,也可以只运行 API,然后通过命令行操作。
以下版本号截至 July 2026 为当前版本:Shlink 5.1 和 shlink-web-client 4.8。
先将短域名指向服务器
域名是这项服务的基础。s.example.com/abc123 是用户看到的链接,因此应选择简短的域名,并在安装任何组件前确定下来。Shlink 会将该域名保存到每个短链接中。之后更改域名会导致已经分发的所有链接失效。
为短域名创建一条 DNS A 记录,并将其指向 VPS 的公网 IPv4 地址。如果服务器支持 IPv6,也请添加 AAAA 记录。然后确认域名可以解析,再继续操作。
dig +short s.example.com A输出应为服务器的地址。如果输出为空,说明记录尚未完成传播。后续步骤都会以难以诊断的方式失败,因为无法为无法解析的名称签发 TLS(传输层安全)证书。
Compose 文件
Shlink 需要数据库。SQLite 适合测试,但如果计划长期保留数据,应选择 Postgres,因为访问记录会不断累积,Postgres 对索引和并发写入的处理更好。将以下内容写入 /opt/shlink/compose.yaml。
services:
shlink:
image: shlinkio/shlink:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
DEFAULT_DOMAIN: s.example.com
IS_HTTPS_ENABLED: "true"
DB_DRIVER: postgres
DB_HOST: database
DB_NAME: shlink
DB_USER: shlink
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
- database
database:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: shlink
POSTGRES_USER: shlink
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- shlink_db:/var/lib/postgresql/data
web-client:
image: shlinkio/shlink-web-client:stable
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
volumes:
shlink_db:两个发布端口都绑定到 127.0.0.1,因此在下一节配置反向代理之前,互联网无法访问该服务。Docker 会在主机防火墙规则之前写入自己的转发规则,因此即使主机防火墙看起来处于关闭状态,单独使用普通的 8080:8080 行也会暴露应用。绑定到回环地址可以避免这一问题。通过这种方式运行的其他应用也应采用相同模式,详见VPS 上的 Docker Compose 指南。
数据库密码来自 compose 文件旁边的 .env 文件,因此不会写入 YAML。
sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.env启动服务并监控 API 启动过程。
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlink首次启动会执行数据库迁移,因此耗时比后续启动更长。服务稳定后,检查它是否能在本地响应。
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/health200 表示 API 已运行且数据库连接正常。这里出现 500 时,问题几乎总是出在数据库上:.env 中的 DB_PASSWORD 与创建 Postgres 时使用的值不匹配,因为 Postgres 镜像只会在初始化空数据目录时读取 POSTGRES_PASSWORD。之后编辑密码不会生效,除非删除卷后重新启动。
在前端终止 HTTPS
Shlink 在 8080 端口提供普通 HTTP。TLS 应在反向代理中终止,唯一需要注意的设置是传递原始主机名。Shlink 通过读取 Host 标头确定短代码所属的域名,因此,重写该标头的代理会导致现有链接返回 404,并将访问统计关联到错误的域名。
server {
server_name s.example.com;
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}然后申请证书。完整操作流程(包括续期计时器)请参阅 Ubuntu 24.04 上使用 nginx 的 Certbot 指南。
sudo certbot --nginx -d s.example.comcompose 文件中的 IS_HTTPS_ENABLED: "true" 会让 Shlink 在返回的短 URL 中输出 https://。它本身不会启用 TLS。将其保留为 false 并置于 HTTPS 代理后面后,API 返回的每个链接都会是一个随后重定向的 http:// 链接。这会增加一次往返,并且在 Web 客户端中显示不正确。
创建 API 密钥
没有密钥,任何客户端都无法访问 API。请在容器内通过 CLI 生成密钥。
sudo docker compose exec shlink shlink api-key:generate --name "web client"该命令只显示一次密钥。请立即复制,因为系统以哈希形式存储密钥,之后无法再次显示。shlink api-key:list显示密钥名称及其是否启用,但不会显示密钥本身。使用 shlink api-key:disable 和密钥名称可撤销密钥。
每个 REST 请求都通过 X-Api-Key 标头携带密钥。
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urls如果返回的 JSON 对象包含 shortUrls 密钥,说明密钥有效。返回 401 且包含 INVALID_API_KEY,表示密钥错误、已禁用或已过期。
从命令行创建短链接
CLI 是创建链接最快的方式,也最适合用于脚本。
sudo docker compose exec shlink shlink short-url:create https://example.com/a/very/long/path
sudo docker compose exec shlink shlink short-url:create https://example.com/docs --custom-slug docs --tag reference--custom-slug 会生成可读链接,而不是随机代码。每个域名下的 slug 都是唯一的,因此如果再次使用已占用的 slug,操作会失败,不会静默覆盖第一个链接。--tag 可以重复使用;如果以后需要查看合并统计信息,可使用标签对链接进行分组。
先列出已有内容,再查看一个链接的流量。
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits 为每次点击输出一行,其中包含日期、来源页和用户代理。除非设置 GEOLITE_LICENSE_KEY 环境变量,否则国家和城市列为空。该变量是免费的 MaxMind 密钥,Shlink 使用它下载 GeoLite2 数据库。未设置该变量时,访问仍会记录,但不会进行地理定位。
Web 客户端和 QR 码
Web 客户端现在位于 127.0.0.1:8081,需要单独配置代理入口。如果不希望将其公开,也可以使用 SSH 隧道。首次加载时,客户端会要求输入服务器 URL 和 API key。输入 https://s.example.com 以及您生成的密钥。客户端会将这两项保存在浏览器存储中,并直接调用您的 API,因此不会有数据经过任何第三方。将界面与 API 分离是一种值得注意的模式,因为 Halcyon 正是利用这种模式,在不修改后端媒体服务器的情况下,将 Jellyfin 媒体库包装成 1990 年代的租赁商店。
QR 码无需任何配置。将 /qr-code 附加到任意短 URL,API 就会返回图像。
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size 表示像素宽度,可接受 50 到 1000,默认值为 300。format 可以是 png 或 svg。margin 表示 QR 码周围的空白区域,单位为像素;最终图像的尺寸等于大小加上两倍边距。添加 errorCorrection=Q,即可生成即使打印得较小或部分被遮挡仍能扫描的 QR 码。
保持服务运行
短链接服务可能会静默失败。链接停止重定向,但没有人通知您,因为点击链接的人可能以为链接已经失效。应使用真实的短 URL 配置运行状态检查,而不是检查首页;只要响应不是重定向,就应发出告警。自托管的 Uptime Kuma 实例可以很好地完成这项工作,也可以检查指定的状态码。
请备份数据库,而不是容器。使用一条命令即可导出数据库。
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gz将此文件与 compose 文件结合,即可在新服务器上重建整个服务。服务器上的每个应用都需要单独保存这两项内容。照片库是特殊情况,因为 PhotoPrism 和 Immich既会将原始文件保存在磁盘上,也会将记录保存在数据库中;因此,仅还原数据库转储不会恢复任何内容。升级时先执行 sudo docker compose pull,再执行 sudo docker compose up -d,Shlink 会在启动时运行所有新迁移。请在拉取更新前导出数据库,因为迁移无法回滚。
FAQ
为什么添加反向代理后,短链接返回 404?
Shlink 会根据 Host 请求头中的域名匹配短代码。如果代理发送的是代理自身的名称或内部地址,Shlink 就会在没有任何链接的域名下查找该短代码,因此返回 404。在 nginx location 块中设置 proxy_set_header Host $host;,然后重新加载代理。链接会立即恢复正常,无需重启容器。
我需要 Postgres,还是 SQLite 就够了?
SQLite 适合试用 Shlink,且不需要额外的容器。在发布重要链接前切换到 Postgres,因为每次点击都会增加访问记录,而 SQLite 会串行处理写入。之后再切换需要导出并重新导入链接,因此一开始选择 Postgres 可以避免迁移。
忘记复制 API 密钥后,还能恢复吗?
不能。Shlink 只存储密钥的哈希值,因此 api-key:list 只显示名称和状态,不会显示密钥值。使用 shlink api-key:generate 生成替代密钥,将其粘贴到 Web 客户端中,然后使用 shlink api-key:disable 禁用旧密钥,使其失效。
为什么访问统计中的国家列为空?
地理定位需要 GeoLite2 数据库。Shlink 只有在提供 GEOLITE_LICENSE_KEY 后才会下载该数据库。该密钥可从 MaxMind 免费获取。将其添加到环境变量部分,重新创建容器,新访问记录就会获得地理位置。此前记录的访问仍为空,直到运行 shlink visit:locate。
如何将 Shlink 迁移到其他服务器?
保留域名并迁移数据。使用 pg_dump 导出数据库,将导出文件和 compose 文件复制到新服务器,启动服务栈,然后在真实流量到达前,将导出文件恢复到空数据库中。最后再修改 DNS 记录。短代码及其访问历史都会保留,因为所有数据都存储在数据库中。