最安全的方式自托管短链:Shlink + Docker + Cloudflare Tunnel

在一台 2C2G 云服务器上用 Docker 跑 Shlink,通过 Cloudflare Tunnel 只暴露短链跳转,管理走官方 app.shlink.io,实现最小攻击面。

#自托管#Docker#Cloudflare

我以前一直用 Bitly。免费的,够用,也没多想。直到有一天我发现,我所有的分享链接,每一次点击,每一台设备的位置,全在他们的数据库里躺着。我一分钱没花,但代价是我的数据归他们了。

更难受的是,这些链接不是我的。Bitly 哪天关门,或者改了政策,我几年积累的短链全废。

所以我开始找自托管方案。

市面上的开源短链工具不少,我主要看了两个:

Dub.co,GitHub 上 21k star,确实漂亮,功能也猛。但它太重了。PlanetScale MySQL + Upstash Redis + Tinybird ClickHouse + Cloudflare R2 + GitHub OAuth,五六个外部依赖。我就是想缩个 URL,不是想搭一个数据分析平台。

Shlink 不一样。PHP 单体应用,一个 Docker 容器加一个数据库就跑起来了。API-first 设计,自带点击统计、地理定位、自定义短码、多域名支持、标签管理。MIT 协议,代码在 GitHub 上。

关键是轻。2C2G 的小服务器跑起来绰绰有余。

Shlink 官网首页

安全思路:最小暴露

大部分 Shlink 教程的做法是这样的:把 Shlink 的 API 和管理面板一起暴露到公网,然后加个密码保护。

这样做的问题在于,你的管理面板和 API 端点都暴露在互联网上了。密码泄露、零日漏洞、暴力破解,任何一个环节出问题,你的短链服务就被别人控制了。

我的做法不一样。

核心原则:只暴露必须暴露的东西。

一个短链服务对公众只需要做一件事:当有人访问 go.wyt.icu/xxx 时,把它 301 跳转到对应的长链接。就这么简单。不需要让任何人访问你的管理界面。

所以我的架构是这样的:

  • Cloudflare Tunnel 只代理短链跳转的 80 端口,其他什么都不开
  • 管理面板不部署,不暴露,连容器都不起
  • 管理操作走官方 app.shlink.io,通过 API key 连接服务器
  • API 端点也只有 Tunnel 可以触达,加了 IP 白名单

这样即使有人扫到了我的服务器,他也只能看到一个跳转服务。没有登录页,没有管理面板,没有可以攻击的入口。

部署步骤

1. 准备工作

你需要:

  • 一个短域名(我用的是 go.wyt.icu)
  • 一台云服务器(2C2G 足够)
  • Docker 和 Docker Compose
  • Cloudflare 账号(管理你的域名 DNS)
  • 服务器上装好 cloudflared

2. Cloudflare Tunnel 配置

在 Cloudflare Zero Trust 面板创建一个 Tunnel,拿到 Token。然后在服务器的 cloudflared 配置里,只把一个域名指向 Shlink 容器:

# ~/.cloudflared/config.yml
tunnel: your-tunnel-id
credentials-file: /root/.cloudflared/your-tunnel-id.json

ingress:
  - hostname: go.wyt.icu
    service: http://localhost:8080
  - service: http_status:404

注意这里只代理了 go.wyt.icu 到本地的 8080 端口。没有把 API 路径单独开出来,也没有暴露管理面板。

在 Cloudflare DNS 里把 go.wyt.icu 的 CNAME 指向你的 Tunnel 就行。

3. Docker Compose

# docker-compose.yml
version: "3.8"

services:
  shlink:
    image: shlinkio/shlink:stable
    container_name: shlink
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - DEFAULT_DOMAIN=go.wyt.icu
      - IS_HTTPS_ENABLED=true
      - DB_DRIVER=postgres
      - DB_HOST=shlink-db
      - DB_PORT=5432
      - DB_NAME=shlink
      - DB_USER=shlink
      - DB_PASSWORD=换成你的密码
      - GEOLITE_LICENSE_KEY=你的key  # 可选,地理定位用
    depends_on:
      - shlink-db
    volumes:
      - ./data:/etc/shlink/data

  shlink-db:
    image: postgres:16-alpine
    container_name: shlink-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=shlink
      - POSTGRES_PASSWORD=换成你的密码
      - POSTGRES_DB=shlink
    volumes:
      - ./db-data:/var/lib/postgresql/data

用 Postgres 是个人偏好,Shlink 也支持 MySQL/MariaDB 和 SQLite。如果更想省资源,SQLite 也行,2C2G 甚至可以降到 1C1G。

然后:

docker compose up -d

看一眼日志确认启动成功:

docker logs shlink

4. 生成 API Key

Shlink 启动后,用 CLI 生成一个 API key:

docker exec -it shlink shlink api-key:generate

记下这个 key,后面要用它连接管理面板。

管理走官方 PWA

这是我觉得最优雅的地方。

Shlink 官方提供了一个 Web 客户端,托管在 app.shlink.io。它是一个纯前端的 PWA,所有操作都通过 API 调用你的服务器。

Shlink 官方 Web 客户端

你打开 app.shlink.io,填入你的服务器地址(https://go.wyt.icu)和 API key,就能管理所有短链了。

这样做的好处:

  • 管理面板完全不部署在你的服务器上,零攻击面
  • 官方维护,自动更新,你不用管前端版本
  • 数据始终在你的服务器上,app.shlink.io 只是一个操作界面,不存储任何数据
  • 可以连接多个 Shlink 实例,如果你以后跑了多个短链服务

当然,如果你特别介意 API key 经过第三方域名,你也可以自己部署 shlinkio/shlink-web-client 镜像,但那样就又多了一个需要保护的入口。对我来说,官方 PWA 够了。

日常使用

创建短链,直接在 app.shlink.io 的界面操作,或者走 API:

curl -X POST https://go.wyt.icu/rest/v3/short-urls \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"longUrl": "https://jask.dev"}'

返回:

{
  "shortCode": "0wPLj",
  "shortUrl": "https://go.wyt.icu/0wPLj",
  "longUrl": "https://jask.dev"
}

点击统计、访问来源、设备类型、地理位置,全都有。数据在你自己的 Postgres 里,不经过任何第三方。

成本

  • 服务器:2C2G VPS,一年几十块
  • 域名:wyt.icu 已经有了,加个 go 子域不花钱
  • Cloudflare Tunnel:免费
  • Shlink:开源免费

总计:接近零。

最后

我的短链已经跑起来了:go.wyt.icu/0wPLj,指向我的个人主页 jask.dev。

整个架构的思路其实就一句话:最小暴露面。服务器上只开一个跳转端口,管理面板不部署,API 不公开。你扫到的只是一个安静的 301 服务,别的什么都看不到。

如果你也在找自托管短链方案,Shlink + Docker + Cloudflare Tunnel 这套组合是我目前能想到的最干净的方式。

如果你还不了解 Cloudflare Tunnel,可以先看我的另一篇:Cloudflare 从入门到入土,全教给你。