使用 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,然后通过命令行使用它。
本文中的版本号截至 2026 年 7 月为当前版本: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。在 HTTPS 代理后保留 false 后,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 对象表示密钥有效。包含 INVALID_API_KEY 的 401 表示密钥错误、已禁用或已过期。
从命令行创建短链接
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,因此数据不会经过任何第三方。
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 是代码周围以像素为单位的留白,生成的图像尺寸等于代码尺寸加上两倍边距。添加 errorCorrection=Q 后,即使代码打印得很小或部分被遮挡,仍可扫描。
保持服务运行
短链接服务出现故障时通常不会明显报错。链接停止跳转,但没有人通知您,因为点击链接的人会认为链接本来就已失效。将运行状态检查指向真实的短 URL,而不是主页,并在响应不是重定向时发出告警。自行托管的 Uptime Kuma 实例很适合执行此操作,还可以检查特定的状态代码。
备份数据库,而不是容器。使用一条命令即可导出数据库。
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gz该文件和 compose 文件可以在新服务器上重建整个服务。升级流程是 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 数据库。只有提供 GEOLITE_LICENSE_KEY 后,Shlink 才会下载该数据库。该密钥可从 MaxMind 免费获取。将其添加到环境变量部分,重新创建容器,新的访问记录就会获得地理位置。此前记录的访问仍为空,直到运行 shlink visit:locate。
如何将 Shlink 迁移到其他服务器?
保留域名并迁移数据。使用 pg_dump 导出数据库,将导出文件和 compose 文件复制到新服务器,启动堆栈,然后在真实流量到达前,将导出文件恢复到空数据库中。最后再修改 DNS 记录。短代码及其访问历史会保留,因为所有数据都存储在数据库中。