开源自托管短链方案:Shlink、YOURLS、Polr、Supabase 四方案选型指南

Shlink、YOURLS、Polr、Supabase+自研四种自托管短链方案横向对比,含Docker一键部署、数据迁移、成本分析。

引言

自建短链 = 域名自主 + 数据自有 + 成本可控

对于年链接分发量超过 100 万次的团队,商业短链服务的订阅费用会快速累积(Bitly Enterprise 年费 $5000+)。而开源自托管方案,不仅成本仅为云服务器费用,还能获得完整的数据所有权和品牌独立性。

本文对比4种主流自托管方案,从功能、技术栈、部署难度、维护成本四个维度分析,帮你做出正确的选择。


一、四种方案速览

方案技术栈首次发布活跃维护亮点适合
ShlinkPHP/Swoole + MySQL2016✅ 极度活跃功能最全、API丰富企业级/开发者
YOURLSPHP + MySQL2009✅ 稳定维护经典、插件生态成熟WordPress/PHP团队
PolrPHP + MySQL2015⚠️ 半维护极简界面、轻量快速上线/小团队
Supabase+自研Postgres + Edge Functions2022✅ 生态活跃无服务器、按需付费技术团队/云原生

二、Shlink:现代短链的标杆

核心特性

  • 双模式运行:传统 PHP-FPM 或 Swoole 高性能模式
  • REST API + CLI:完整的程序化接口
  • 多域名支持:同一实例服务多个自定义域名
  • GEO 定位:MaxMind 数据库精准定位
  • QR 码生成:原生支持动态二维码
  • 标签系统:链接分类管理
  • 访问规则:基于地理位置/时间/设备的条件跳转 参见 AI智能跳转

Docker 一键部署

# docker-compose.yml
version: '3'
services:
  shlink:
    image: shlinkio/shlink:stable
    restart: always
    ports:
      - "8080:8080"
    environment:
      - DEFAULT_DOMAIN=go.yourdomain.com
      - IS_HTTPS_ENABLED=true
      - GEOLITE_LICENSE_KEY=${GEOLITE_KEY}
      - DB_DRIVER=mysql
      - DB_NAME=shlink
      - DB_USER=shlink
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_HOST=database
    depends_on:
      - database

  database:
    image: mysql:8.0
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=shlink
      - MYSQL_USER=shlink
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

启动:

docker-compose up -d
# 生成初始 API Key
docker exec shlink shlink api-key:generate

API 使用示例

# 创建短链
curl -X POST https://go.yourdomain.com/rest/v3/short-urls \
  -H "X-Api-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "longUrl": "https://example.com/very/long/path",
    "customSlug": "launch2026",
    "tags": ["campaign", "spring"],
    "findIfExists": true
  }'

# 获取点击统计
curl https://go.yourdomain.com/rest/v3/short-urls/launch2026/visits \
  -H "X-Api-Key: YOUR_KEY"

访问规则配置(YAML)

# config/redirect-rules.yml
rules:
  - shortCode: sale2026
    conditions:
      - type: geo_country
        match: CN
        target: https://example.com/cn-sale
      - type: device_type
        match: mobile
        target: https://m.example.com/sale
    default: https://example.com/sale

成本估算(月)

组件阿里云/腾讯云AWS
ECS/EC2 (2C4G)¥150-200$20-30
RDS/MySQL基础版¥100-150$15-25
带宽 1TB包含~$90
总计¥250-350$35-55 + 带宽

三、YOURLS:经典 PHP 的稳定之选

核心特性

  • 极简哲学:只做短链核心(创建、跳转、统计)
  • 插件系统:100+ 社区插件
  • 公共/私有模式:支持公共创建或仅管理员
  • Bookmarklet:浏览器一键缩短
  • JSONP API:跨域调用

Docker 部署

version: '3'
services:
  yourls:
    image: yourls:latest
    restart: always
    ports:
      - "8080:80"
    environment:
      - YOURLS_DB_HOST=database
      - YOURLS_DB_USER=yourls
      - YOURLS_DB_PASS=${DB_PASSWORD}
      - YOURLS_DB_NAME=yourls
      - YOURLS_SITE=https://go.yourdomain.com
      - YOURLS_USER=admin
      - YOURLS_PASS=${ADMIN_PASSWORD}
    depends_on:
      - database

  database:
    image: mysql:5.7
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
      - MYSQL_DATABASE=yourls
      - MYSQL_USER=yourls
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

必装插件推荐

插件功能
YOURLS-Import-Export批量导入/导出链接
Random-Shorturls随机短码代替顺序递增
Change-Title自定义短链标题(SEO)
QRCode-ShortURL二维码生成
Better-Analytics增强统计数据

适用场景

  • 已有 PHP/MySQL 技术栈的团队
  • WordPress 生态用户(同一主机托管)
  • 对功能要求不高,只要"稳定可用"
  • 10人以下小团队,月点击量 < 500万

四、Polr:极简轻量的选择

核心特性

  • Material Design UI:现代化界面
  • 轻量部署:单 PHP 文件即可运行
  • 无依赖缓存:文件缓存无需 Redis

部署(共享主机也支持)

git clone https://github.com/Cydrobolt/polr.git
cd polr
composer install --no-dev
php artisan migrate
php artisan key:generate

⚠️ 注意事项

  • 最近一次发布为 2020 年,社区维护力度下降
  • 功能相对单一,无 API 认证机制
  • 建议:仅用于个人或极小团队临时需求

五、Supabase + 自研:云原生方案

架构设计

┌─────────────┐     ┌─────────────────┐     ┌──────────────┐
│   Client    │────▶│  Supabase Edge  │────▶│  Postgres    │
│             │◀────│    Function     │◀────│   (RLS)      │
└─────────────┘     └─────────────────┘     └──────────────┘
                            │
                     ┌──────┴──────┐
                     ▼             ▼
               Upstash Redis   Custom Domain
               (Rate Limit)    (CNAME + SSL)

数据库表结构

-- 启用 Row Level Security
CREATE TABLE short_links (
    id BIGSERIAL PRIMARY KEY,
    slug VARCHAR(16) UNIQUE NOT NULL,
    target_url TEXT NOT NULL,
    title VARCHAR(200),
    clicks BIGINT DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    expires_at TIMESTAMPTZ,
    is_active BOOLEAN DEFAULT true,
    tenant_id UUID REFERENCES auth.users(id)
);

-- RLS 策略:用户只能查看自己的链接
ALTER TABLE short_links ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can only access own links"
    ON short_links
    FOR ALL
    USING (tenant_id = auth.uid());

-- 统计表(时序)
CREATE TABLE click_events (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    link_id BIGINT REFERENCES short_links(id),
    ip_hash VARCHAR(64),
    country_code CHAR(2),
    city VARCHAR(100),
    user_agent TEXT,
    referrer TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Edge Function(跳转处理)

// supabase/functions/redirect/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const slug = new URL(req.url).pathname.replace('/', '')
  const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!)

  // 1. 从数据库获取目标链接
  const { data: link } = await supabase
    .from('short_links')
    .select('target_url, is_active, expires_at')
    .eq('slug', slug)
    .single()

  if (!link || !link.is_active || (link.expires_at && new Date(link.expires_at) < new Date())) {
    return new Response('Not Found', { status: 404 })
  }

  // 2. 异步记录点击(不阻塞跳转)
  const eventData = {
    link_id: slug,
    ip_hash: await sha256(req.headers.get('x-forwarded-for') || ''),
    country_code: req.headers.get('cf-ipcountry'),
    referrer: req.headers.get('referer'),
    user_agent: req.headers.get('user-agent'),
  }

  // Edge Function 结束后异步触发
  supabase.from('click_events').insert(eventData).then(() => {})

  // 3. 302 临时跳转或 301 永久跳转
  return new Response(null, {
    status: 301,
    headers: {
      'Location': link.target_url,
      'Cache-Control': 'public, max-age=3600',
    },
  })
})

async function sha256(str: string): Promise<string> {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
  return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
}

创建短链 API

// supabase/functions/create-link/index.ts
serve(async (req) => {
  const { target_url, custom_slug, expires_in_days } = await req.json()

  // 生成 slug
  const slug = custom_slug || await generateUniqueSlug()

  const { data, error } = await supabase
    .from('short_links')
    .insert({
      slug,
      target_url,
      expires_at: expires_in_days ? new Date(Date.now() + expires_in_days * 86400000) : null,
      tenant_id: user.id,
    })
    .select()
    .single()

  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' },
  })
})

async function generateUniqueSlug(): Promise<string> {
  const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  while (true) {
    const slug = Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * 62)]).join('')
    const { count } = await supabase.from('short_links').select('*', { count: 'exact' }).eq('slug', slug)
    if (count === 0) return slug
  }
}

成本估算(月,Supabase 免费档起步)

档位费用配置适合
Free$0500MB DB, 2GB 带宽, 50万 Edge Function 调用起步/验证
Pro$258GB DB, 100GB 带宽, 200万调用< 1000万点击/月
Team$599更大规格 + 支持企业级

六、四方案横向深度对比

功能矩阵

功能ShlinkYOURLSPolrSupabase自研
自定义 slug
密码保护链接插件需自建
二维码生成插件需自建
GEO 跳转规则
时间到期插件
访问统计丰富基础基础需自建
REST API完善基础自定义
多租户/多域名
批量操作CLI/API插件自定义
标签/分类需自建

技术栈对比

维度ShlinkYOURLSPolrSupabase自研
学习成本中高
部署难度中(Docker)极低
扩展性极高
维护负担中(维护弱)
数据主权
无服务器

性能对比(估算,2C4G 服务器)

方案单机 QPS十万次点击响应备注
Shlink + Swoole~3000< 50msSwoole 协程模式
YOURLS + OPcache~1000~100msPHP-FPM 模式
Polr~800~120ms无缓存层
Supabase Edge~5000< 30ms全球 CDN 边缘节点

七、数据迁移:从商业服务回来自托管

# migrate_bitly_to_shlink.py
import requests

BITLY_TOKEN = "your_bitly_token"
SHLINK_URL = "https://go.yourdomain.com"
SHLINK_KEY = "your_shlink_api_key"

def fetch_bitly_links():
    links = []
    url = "https://api-ssl.bitly.com/v4/groups/default/bitlinks"
    headers = {"Authorization": f"Bearer {BITLY_TOKEN}"}

    while url:
        resp = requests.get(url, headers=headers)
        data = resp.json()
        links.extend(data['links'])
        url = data.get('pagination', {}).get('next')

    return links

def import_to_shlink(bitly_link):
    payload = {
        "longUrl": bitly_link['long_url'],
        "customSlug": bitly_link['id'].split('/')[-1],
        "tags": bitly_link.get('tags', []),
    }

    resp = requests.post(
        f"{SHLINK_URL}/rest/v3/short-urls",
        headers={"X-Api-Key": SHLINK_KEY, "Content-Type": "application/json"},
        json=payload
    )
    return resp.json()

if __name__ == "__main__":
    links = fetch_bitly_links()
    for link in links:
        result = import_to_shlink(link)
        print(f"Imported: {link['id']}{result.get('shortUrl')}")

迁移检查清单

  • 导出原平台所有短链列表(含目标 URL、自定义 slug、标签)
  • 统计原平台各短链的累计点击数(保留历史数据或记录存档)
  • 预先在新平台创建所有短链(保留原 slug 确保链接不中断)
  • DNS TTL 提前降至 300 秒以下
  • 切换 DNS 指向自建服务器/域名
  • 验证每条短链是否正常跳转
  • 保留原平台至少 30 天作为回退

八、选型决策树

                    需要自托管短链?
                        │
          ┌─────────────┴─────────────┐
          ▼                           ▼
    团队有PHP经验?              偏好云原生/JS栈?
          │                           │
    ┌─────┴─────┐                     │
    ▼           ▼                     ▼
   是          否               选择Supabase自研
    │           │                     │
    ▼           ▼                     │
需要丰富      快速上线                  │
功能?       即可?                     │
    │           │                     │
┌───┴───┐       ▼                     │
是       否   选YOURLS(简单稳定)     │
    │     │                           │
    ▼     ▼                           │
  Shlink  Polr                        │
(企业级) (极简)                     │

九、安全加固必读

必做安全设置

  1. HTTPS 强制:所有短链域名配置 HSTS
  2. SQL 注入防护:使用 ORM/参数化查询(上述方案均已内置)
  3. XSS 防护:过滤用户输入的自定义 slug
  4. 速率限制
    • 创建接口:10次/分钟/IP
    • 跳转接口:1000次/分钟/IP
  5. Referrer 策略Referrer-Policy: strict-origin-when-cross-origin
  6. Open Redirect 防护:验证目标域名白名单 参见 短链安全防线

Docker 安全建议

# 安全加固版 docker-compose
services:
  shlink:
    image: shlinkio/shlink:stable
    read_only: true          # 只读根目录
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m
    # 不使用 root 运行
    user: "1000:1000"

十、总结

你的情况推荐方案理由
企业级需求,团队有运维能力Shlink功能最全,API 完善,社区活跃
WordPress/PHP 团队,简单需求YOURLS稳定成熟,插件丰富,低维护
极小团队,临时需求Polr部署最快,但注意维护风险
云原生团队,JS/TS 技术栈Supabase 自研无服务器架构,按需扩展,数据自主
已有云服务器(阿里云/AWS)Shlink/YOURLS利用现有基础设施
零服务器,纯前端团队Supabase Free$0 启动,验证后再扩展

自托管短链的核心价值不仅是省钱,而是将链接这个关键基础设施掌握在自己手中。当你的品牌每年通过短链产生千万次触达时,拥有完整的数据和可控的体验,是任何商业 SaaS 难以替代的竞争优势。


相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「SaaS产品设计」更多文章