使用 Shlink 和 Docker Compose 自托管短链接服务
在 VPS 上部署 Shlink 5.1 和 Web 客户端 4.8,配置短域名 DNS、Postgres、HTTPS 与 API 密钥,并启用二维码和点击统计。
构建内容
自托管 URL 缩短服务是一台小型服务器,可将较长的链接转换为您自己的短链接,并记录每次点击。Shlink 是合适的选择:它采用开源许可证,以 Docker 镜像形式发布,并可通过一个容器和一个数据库完成全部工作。本指南将在 VPS 上部署 Shlink,通过真实的短域名提供服务,并配置 HTTPS、API 密钥、二维码和点击统计。
要实现类似商业化短链接服务的体验,需要两个组件。API 服务器负责响应重定向请求并保存数据。Web 客户端是独立的静态应用,可在浏览器中调用该 API。您可以同时运行两者,也可以只运行 API,再通过命令行操作。
本文中的版本号截至 2026 年 10 月为当前版本: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 正常运行,且数据库连接正常。503 表示 Shlink 正在运行,但数据库检查失败。对于任何数据库错误,健康检查端点都会返回 503,不会返回 500。如果 curl 输出 000,则表示该端口没有服务响应:容器在启动检查期间停止,Docker 正在重新启动它,因此应查看上面的日志。无论哪种情况,通常原因都是 .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包含 shortUrls 键的 JSON 对象表示密钥有效。若返回 401,其 type 为 https://shlink.io/api/error/invalid-api-key,且标题为 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 密钥。输入 https://s.example.com 以及刚生成的密钥。客户端会将两者保存在浏览器存储中,并直接调用您的 API,因此数据不会经过任何第三方。将界面与 API 分离是一种值得注意的模式,因为 Halcyon 可以在不更改后端媒体服务器的情况下,将 Jellyfin 媒体库包装成 1990s 年代的租赁商店,使用的就是同一种模式。
QR 码由 Web 客户端生成,而不是由 API 生成。Shlink 5 移除了过去在短 URL 后追加 /qr-code 的方法,因此该地址不再返回图像。现在,Web 客户端直接在浏览器中绘制 QR 码,不会向 Shlink 发起请求。在短 URL 列表中打开任意链接的 QR 码选项,设置大小、格式和边距,然后下载结果。对于打印尺寸较小或部分被遮挡后仍需扫描的 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 记录。短代码及其访问历史都会保留,因为所有数据都存储在数据库中。