用 Shlink + Docker 自建一个 URL 短链服务

Shlink 是 PHP 圈事实上的自托管 URL 短链服务 — MIT 协议,由 Alejandro Celaya
(acelaya)持续维护,官方打包了开箱即用的 Docker 镜像,把 99% 的运维工作
都消化掉了。本文一步步带你搭起一套 2026 年可用的 Docker Compose 栈,
十分钟左右就能跑起来,自带 Web UI、GeoLite2 地理位置、HTTPS。

TL;DR. 起一个 shlinkio/shlink:stable、旁边放一个一次性的
shlink-installer 容器、配 MariaDB + Redis、把短域名解析到这台机器、
前面挂 Caddy 或 Traefik 做 HTTPS。总共七个 YAML 服务 + 一个 .env

Shlink 是什么 — 为什么需要自己托管

Shlink 是一个 PHP 服务(基于 Mezzio / Laminas,用 Doctrine 做 ORM,
跑在 RoadRunner 上),把长 URL 变成短 URL。你 POST 一个长 URL 到它的
REST API,得到一个短码,Shlink 会用 30x 把访客从
<你的短域名>/abc12 重定向回真实目的地,同时记录这次访问(IP、
UA、来源、国家)。

当前活跃版本线是 5.x。截至本文撰写时,最新镜像 tag 是 5.1.5
stablelatest 都指向它;2026-07-03 由 acelaya 推送)。
amd64 镜像 107 MB,arm64 镜像 98 MB,自 v4.0.0 起以非 root 身份运行 —
已经不再需要 -non-root / -alpine 之类的后缀体操了。

对比常见托管方案:

  • Bitly / TinyURL — 日常用还行,但你拿不到数据、扛不住大规模重定向
    速度,链接历史数据可能被服务商擅自删除。
  • YOURLS — 元老级 PHP 自托管方案,现在还能用,但数据模型仅支持 MySQL,
    UI 也明显停留在 2008 年审美。
  • Shlink — 现代化的 REST API、多数据库(MariaDB / MySQL / PostgreSQL /
    MSSQL)、QR 码生成(4.5.0 起 deprecated,见 Pitfalls 一节)、完整的
    OpenAPI 规范、可选的 React PWA Web 客户端。

Docker 栈 — 一份完整的 compose

官方推荐的栈一共七个服务。下面是最小可行版本,直接来自
shlinkio/shlink 自己仓库的 docker-compose.yml
shlinkio/shlink-docker 参考仓库:

Yaml
services:
  shlink:
    image: shlinkio/shlink:stable
    restart: unless-stopped
    depends_on:
      mariadb:
        condition: service_healthy
      redis:
        condition: service_healthy
    env_file: .env
    networks: [shlink-net]

  shlink-installer:
    image: shlinkio/shlink:stable
    depends_on:
      mariadb:
        condition: service_healthy
    env_file: .env
    entrypoint: shlink-installer
    command: init --no-interaction
    networks: [shlink-net]

  shlink-web-client:
    image: shlinkio/shlink-web-client
    restart: unless-stopped
    depends_on:
      - shlink
    environment:
      SHLINK_SERVER_URL: ${SHLINK_SERVER_URL}
      SHLINK_SERVER_API_KEY: ${INITIAL_API_KEY}
    networks: [shlink-net]

  mariadb:
    image: mariadb:11
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: shlink
      MARIADB_USER: shlink
      MARIADB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mariadb_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks: [shlink-net]

  redis:
    image: redis:7-alpine
    networks: [shlink-net]

networks:
  shlink-net:
    driver: bridge

volumes:
  mariadb_data:

有两件事值得专门强调:

  • shlinkshlink-installer 是同一个镜像,只是启动方式不同。
    installer 容器跑一次 shlink-installer init --no-interaction,等
    MariaDB 起好、跑迁移、下载 GeoLite2,然后退出 0。这是故意的 ——
    docker compose up 不能被卡住。
  • shlink-web-client 可选但强烈建议开。 它是一个 React PWA
    (nginx:alpine 基础镜像、25 MB),通过上面两个环境变量读 server
    配置。

让它能跑起来的 .env

Shlink 的配置几乎全部由环境变量驱动。最小可用的 .env

Bash
# 品牌
DEFAULT_DOMAIN=s.example.com
IS_HTTPS_ENABLED=true
SHORT_URL_TRAILING_SLASH=false

# 数据库(MariaDB 用 'maria',不是 'mariadb' — 常见拼错)
DB_DRIVER=maria
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=shlink
DB_USER=shlink
DB_PASSWORD=please...n
# 访问记录 & 限流
REDIS_SERVERS=redis://redis:6379

# 首次启动的 API key(首次启动后忽略)
INITIAL_API_KEY=*** openssl rand -hex 32)

# GeoLite2(免费,2019-12-30 起必需,用于访问地理位置)
GEOLITE_LICENSE_KEY=your-maxmind-license-key

# Web 客户端配置(shlink-web-client 读,不是 shlink)
SHLINK_SERVER_URL=https://s.example.com

三个看起来像拼错但其实没拼错的设置:

  • DB_DRIVER=maria — MariaDB 用这个。mariadb 是悄悄错的(对不上任何
    driver)。MySQL 用 mysql,PostgreSQL 用 postgres,MSSQL 用 mssql
    SQLite 用 sqlite
  • REDIS_SERVERS 支持逗号分隔的多个实例 — 当你横向扩 Shlink 到多实例、
    想用 Redis Cluster 时很有用。v5.0.0 还加了 unix:/path/to/redis.sock
    支持本机 Unix socket。
  • 任何环境变量 X 都可以用 X_FILE 指向一个文件(Docker secrets 模式)。
    这是生产环境传 DB_PASSWORDINITIAL_API_KEY 的唯一安全姿势。

GeoLite2 — 唯一有点烦的配置

2019-12-30 起,MaxMind 要求用(免费)license key 才能下载 GeoLite2。
Shlink 把它读成 GEOLITE_LICENSE_KEY 环境变量,并在首次启动时下载数据库。

申请 key 的步骤:

  1. https://www.maxmind.com/en/geolite2/signup 注册账号。
  2. 生成 license key — “Will you be using geoipupdate?” 一项选 No
    (Shlink 自己负责下载)。
  3. .env 里写 GEOLITE_LICENSE_KEY=<key>,然后等大约 30 分钟让 key 在
    MaxMind 的边缘同步生效。(是的这很烦。事实就是这样。)
  4. 重启 shlink 服务(docker compose up -d shlink)。下一次访问就会
    触发 GeoLite2 下载。

如果忘了设这个 key,Shlink 照样能跑 —— 访问记录照样记,但国家/城市
字段会一直是 Unknown。等你真的想要地理分析时就难受了。

创建 API key

调用 Shlink REST API 时认证靠 X-Api-Key: <uuid> 头。没有 admin 用户 —
每把 key 都是显式创建的。三种创建方式:

Bash
# 1. 首次启动前在 .env 里设 INITIAL_API_KEY。首次启动时会创建一把 key,
#    后续启动忽略。

# 2. 栈起来之后,在容器里用 CLI 创建:
docker compose exec shlink shlink api-key:create --name default
# -> 把一个 UUID 打到 stdout。

# 3. 也可以用 REST API 创建(需要已经有一把 author_apis=ALL 的 key):
curl -X POST https://s.example.com/rest/v3/api-keys 
  -H "X-Api-Key: $EXISTING_KEY" 
  -H "Content-Type: application/json" 
  -d '{"name":"secondary"}'

Web UI 不能创建 key — 按 Shlink 的设计,它只消费 key。如果丢了 key,
唯一的恢复方式是 docker compose exec shlink shlink api-key:create --name replacement

通过 API 创建第一个短链

拿到 INITIAL_API_KEY 之后,短化你的第一个 URL:

Bash
KEY=$(grep INITIAL_API_KEY .env | cut -d= -f2)

curl -X POST https://s.example.com/rest/v3/short-urls 
  -H "X-Api-Key: $KEY" 
  -H "Content-Type: application/json" 
  -d '{
    "longUrl": "https://github.com/shlinkio/shlink",
    "tags": ["github","docs"],
    "title": "Shlink on GitHub"
  }'

返回:

Json
{
  "shortCode": "gh5h2",
  "shortUrl": "https://s.example.com/gh5h2",
  "longUrl": "https://github.com/shlinkio/shlink",
  "tags": ["github","docs"],
  "title": "Shlink on GitHub"
}

访问 https://s.example.com/gh5h2,会跳到 GitHub。
访问 https://s.example.com/rest/health 返回 200(确认服务在线)。

HTTPS — 终结者在外面,不在 Shlink 里

Shlink 的 Docker 镜像只说 HTTP,监听 8080。它不做 TLS 终结。在它前面挂一个:

  • Caddys.example.com { reverse_proxy shlink:8080 }。通过 Let’s Encrypt
    自动签证书。
  • Traefik — 基于 label 的配置,同样自动签证书。YAML 略多一点。
  • nginx + certbot — 能用,但要手动跑 certbot certonly + 配续期 cron。
  • Cloudflare Tunnel — 自己不用管证书,Cloudflare 帮你搞定。如果你已经在
    Cloudflare 上,挺合适。

启用 HTTPS 后两个环境变量坑:

  • IS_HTTPS_ENABLED=true 告诉 Shlink 生成 https:// 短链(默认不设就是
    http://)。
  • 如果前面挂了多层代理(Cloudflare → Caddy → Shlink),设 TRUSTED_PROXIES
    (v4.5.0 起)—— 逗号分隔的 IP 段列表,让 Shlink 能解析到真实客户端 IP,
    否则访问记录的 IP 字段全是错的。

坑(2026 版)

有几件事第一次部署一定会坑你:

  • 生产别用 SQLite。 官方文档明确警告:”Using it in production is not
    supported, even less if mounted via docker volumes.” 上公开服务之前换到
    MariaDB / Postgres / MSSQL。
  • latest vs stable. 有 alpha/beta 时 latest 会指过去;stable 永远
    跟最新的稳定版。生产环境 pin 具体版本(例如 5.1.5)或 stable
  • shlink-installer exited with code 0 不是错误。 首次初始化跑完
    schema、迁移、ORM proxy 生成、GeoLite2 下载,然后退出 0。看
    docker compose logs shlink-installer 就能知道它到底干了啥。
  • Web 客户端的环境变量要挂在 web-client 容器上,不要挂在 installer 上。
    SHLINK_SERVER_URL / SHLINK_SERVER_API_KEY 配的是 nginx 服务的 React 应用
    里预填的 server 列表。如果挂在 shlink 上,啥也不会发生。
  • 升级需要手动 db:migrate. installer 只在冷启动时跑一次。改镜像 tag →
    docker compose pulldocker compose up -ddocker compose exec shlink shlink db:migrate
  • 跑多实例必须用 Redis。 Shlink 用 symfony/lock 做共享状态;没有 Redis
    的话,锁是本地的,并发创建 domain/key 时会撞车。

你能控制什么 vs 不能控制什么

你能控制的:

  • 全部 URL → 短码映射。
  • 全部访问日志(IP 默认是 hash 化的;设 ANONYMIZE_REMOTE_ADDR=false 会关掉
    哈希 —— 会让你的实例违反 GDPR)。
  • 完整的 Redis / MariaDB 状态。任何时候 docker compose exec mariadb mysqldump
    就能把整库备份带走。

无法控制的:

  • 短域名本身 — 这是 DNS 的事。把 A/AAAA 解析到跑 Shlink 的机器就完事。
  • 证书 — 这是反向代理的活(或者 Cloudflare 的活)。

一句话总结

单用户 / 小团队部署,shlinkio/shlink:stable + mariadb:11 +
redis:7-alpine + 一个 Caddy 反向代理,就是 2026 年最稳的配置。镜像维护
活跃、API 干净、接入 GeoLite2 之后访问分析也好用。镜像总数:四个。
compose.yaml 里每服务的 YAML 量:十行。

参考资料

最后修改: 2026年7月16日

作者

评论

发表评论

您的邮箱地址不会被公开。