惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
U
Unit 42
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
G
Google Developers Blog
Recent Announcements
Recent Announcements
B
Blog RSS Feed
罗磊的独立博客
博客园 - Franky
J
Java Code Geeks
S
SegmentFault 最新的问题
D
DataBreaches.Net
C
Check Point Blog
Blog — PlanetScale
Blog — PlanetScale
T
The Blog of Author Tim Ferriss
有赞技术团队
有赞技术团队
腾讯CDC
博客园_首页
美团技术团队
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
The Cloudflare Blog
aimingoo的专栏
aimingoo的专栏

Dejavu's Blog

甲骨文 ARM 实例部署 Gemma 4 模型 Headscale + Tailscale 组建虚拟专用网 在 Linux 上使用 Yubikey OpenPGP 应用 BuyVM VPS 块存储挂载教程 Alpine Linux 服务器配置指南 Alpine Linux 安装 Cloudflared Docker 多容器共享中心数据库 安装 Komari 服务器监控工具 Scaleway VPS 安装 Debian Linux Debian 13 下部署 AsmBB 论坛 使用 Kopia 自动化备份服务器数据 给 Docker 启用 IPv6 支持 Netcup 服务器安装自定义 ISO 镜像 烽火 HG5582A 光猫开启桥接模式 部署 Obsidian LiveSync 实时同步服务指南 我的 2025 年不完全回顾 Linux 下 Intel 核显驱动配置与硬件加速 Fedora Linux 安装配置记录 2025 年优雅地自托管 RSS 服务 Woodpecker CI 和 Gitea 实现 Hugo 自动部署 Gitea/Forgejo 集成 Woodpecker CI/CD 在 Blinko 中使用 Ollama 作为 AI 供应商 Docker 部署 Gitea/Forgejo Plausible CE 启用城市级地理位置识别 Blinko 开源 AI 知识库 Docker 部署指南 Netcup 免税账号注册及购买服务器全记录 Docker 自托管 Cloudreve Pro 私有网盘服务 GiffGaff SIM 卡使用体验和注意事项 简体中文互联网在变得糟糕吗? 如何低成本申请 S/MIME 证书用于个人邮件服务
Docker 自托管 Shlink 短链服务
Dejavu Moe · 2026-01-13 · via Dejavu's Blog

引言

Shlink 是一款基于 PHP 的自托管网址缩短服务。由于个人对 PHP 存在一定心理门槛😅,此前一直未尝试部署。近期阅读项目文档后发现,它实际上支持多种 PHP Runtime(运行时):

  • RoadRunner
  • FrankenPHP
  • 传统方式(Web Server + FastCGI)

其中,RoadRunner 是 Shlink 官方推荐的默认运行方式。它是一款由 Go 编写的 PHP 运行时,通过多个 Worker 将 Shlink 常驻于内存 (RAM) 中运行,在响应速度和整体性能方面表现良好。

Shlink 后端服务与 Web 管理后台是分离的,Web 管理面板有两种解决方案:

  • shlink-web-client :官方提供的 PWA Web 应用,纯静态应用,API Key 存储在浏览器的本地存储中
  • shlink-dashboard :官方的下一代 Web 面板,包含 shlink-web-client 的所有功能,并支持高级用户认证和角色管理,API Key 统一存储在服务器端。

我选择 shlink-dashboard 作为 Web 管理面板,并使用 PostgreSQL 作为后端服务与管理面板共用的数据库。

准备 Docker 模板

新建 compose.yml 模板,写入

services:
  db:
    image: postgres:18-alpine
    container_name: shlink-db
    restart: unless-stopped
    environment:
      - POSTGRES_DB=shlink        # Shlink 数据库名称
      - POSTGRES_USER=shlink      # 数据库用户名
      - POSTGRES_PASSWORD=shlink  # 数据库密码
    volumes:
      - ./data:/var/lib/postgresql
      - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro  # 初始化阶段创建另一个数据库
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shlink -d shlink"]
      interval: 10s
      timeout: 5s
      retries: 5

  shlink-backend:
    image: shlinkio/shlink:stable
    container_name: shlink-backend
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DB_DRIVER=postgres
      - DB_HOST=db
      - DB_NAME=shlink              # Shlink 数据库名称
      - DB_USER=shlink              # 数据库用户名
      - DB_PASSWORD=shlink          # 数据库密码
      - DEFAULT_DOMAIN=your.domain  # 要绑定的短链接域名
      - IS_HTTPS_ENABLED=true       # 启用 HTTPS
      - TIMEZONE=Asia/Shanghai
      - GEOLITE_LICENSE_KEY=        # [可选] MaxMind GeoLite Key
      - TRUSTED_PROXIES=172.16.0.0/12
      - LOGS_FORMAT=json
      - DEFAULT_SHORT_CODES_LENGTH=4
      - SHORT_URL_TRAILING_SLASH=true
    ports:
      - '127.0.0.1:8080:8080'
  
  shlink-web:
    image: shlinkio/shlink-dashboard:stable
    container_name: shlink-web
    user: '1000:1000'
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
     - '127.0.0.1:3005:3005'
    environment:
      SHLINK_DASHBOARD_DB_DRIVER: postgres
      SHLINK_DASHBOARD_DB_HOST: db
      SHLINK_DASHBOARD_DB_PORT: 5432
      SHLINK_DASHBOARD_DB_NAME: dashboard   # shlink-dashboard 数据库名称
      SHLINK_DASHBOARD_DB_USER: shlink      # 数据库用户名
      SHLINK_DASHBOARD_DB_PASSWORD: shlink  # 数据库密码
      SHLINK_DASHBOARD_SESSION_SECRETS: secret1,secret2...  # 修改会话机密,设置多个高强度随机值,半角逗号隔开

Shlink 与 shlink-dashboard 将共用同一个 PostgreSQL 容器。由于该镜像在初始化阶段默认仅创建单一数据库,我们的预期是分别为两者提供独立的数据库。

准备一个 init-db.sql,并在其中定义相应的初始化逻辑,这会在 PostgreSQL 首次启动时执行并最终创建两个可用的数据库。

CREATE DATABASE dashboard;

启动服务

似乎没有必要,但为了稳妥起见,先启动数据库

sudo docker compose up -d db

低配置的 VPS 建议手动检查健康状况,确保数据库准备就绪

sudo docker compose ps db
# 或者
sudo docker compose logs -f db

现在可以启动所有服务

sudo docker compose up -d

生成访问凭据

使用 Shlink CLI 工具生成访问密钥,用于后续 Web 管理面板添加服务器

sudo docker exec -it shlink-backend shlink api-key:generate

看起来是 UUID 格式,复制保留备用

gen-api-key.webp

[附] Shlink CLI 命令手册建议保留备用

用法:
  command [options] [arguments]

选项:
  -h, --help            显示指定命令的帮助信息;如果未指定命令,则显示命令列表的帮助
      --silent          不输出任何信息
  -q, --quiet           仅显示错误信息,其他所有输出都会被抑制
  -V, --version         显示当前应用程序的版本
      --ansi|--no-ansi  强制启用(或禁用)ANSI 输出
  -n, --no-interaction  不询问任何交互式问题
  -v|vv|vvv, --verbose  提高输出详细程度:1 为普通输出,2 为更详细输出,3 为调试输出

可用命令:
  completion                      输出 shell 自动补全脚本
  help                            显示某个命令的帮助信息
  list                            列出所有可用命令
 api-key
  api-key:delete                  按名称删除一个 API Key
  api-key:disable                 按名称或明文 key 禁用一个 API Key(使用明文 key 的方式已被弃用)
  api-key:generate                生成一个新的有效 API Key
  api-key:initial                 尝试创建初始 API Key
  api-key:list                    列出所有可用的 API Key
  api-key:rename                  按名称重命名一个 API Key
 domain
  domain:list                     列出曾被用于任意短链接的所有域名
  domain:redirects                为指定域名设置特定的“未找到(404)”重定向规则
  domain:visits                   返回指定域名的访问记录列表
 integration
  integration:matomo:send-visits  将现有访问记录发送到已配置的 Matomo 实例
 short-url
  short-url:create                为提供的长链接生成一个短链接并返回
  short-url:delete                删除一个短链接
  short-url:delete-expired        删除所有被视为已过期的短链接(有效时间早于当前时间)
  short-url:edit                  编辑一个已有的短链接
  short-url:import                允许从第三方来源导入短链接
  short-url:list                  列出所有短链接
  short-url:manage-rules          为某个短链接设置重定向规则
  short-url:parse                 返回某个短代码对应的原始长链接
  short-url:visits                返回指定短代码的详细访问记录信息
  short-url:visits-delete         删除某个短链接的访问记录
 tag
  tag:delete                      删除一个或多个标签
  tag:list                        列出现有的标签
  tag:rename                      重命名一个已有的标签
  tag:visits                      返回指定标签的访问记录列表
 visit
  visit:download-db               检查 GeoLite2 数据库文件是否过旧或不存在,如是则尝试下载最新版本
  visit:locate                    解析访问记录的来源地理位置;如有需要会自动下载或更新 GeoLite2 数据库
  visit:non-orphan                返回非孤立访问记录的列表
  visit:orphan                    返回孤立访问记录的列表
  visit:orphan-delete             删除所有孤立访问记录

设置反向代理

为了方便管理,我将 Shlink 和 shlink-dashboard 放到同一个 Nginx 虚拟主机反向代理配置内:

server {
    listen 80;
    listen [::]:80;
    server_name your.domain shlink-dash.your.domain;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name your.domain;

    ssl_certificate /path/to/cert/your.domain.pem;
    ssl_certificate_key /path/to/cert/your.domain.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:P-256:P-384;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256';
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    access_log /var/log/nginx/your.domain.access.log;
    error_log /var/log/nginx/your.domain.error.log;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $realip_remote_addr;
        proxy_set_header X-Forwarded-Proto https;

        proxy_connect_timeout 10s;
        proxy_send_timeout 30s;
        proxy_read_timeout 90s;

        proxy_redirect off;

        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name shlink-dash.your.domain;

    ssl_certificate /path/to/cert/shlink-dash.your.domain.pem;
    ssl_certificate_key /path/to/cert/shlink-dash.your.domain.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:P-256:P-384;
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256';
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    access_log /var/log/nginx/shlink-dash.your.domain.access.log;
    error_log /var/log/nginx/shlink-dash.your.domain.error.log;

    location / {
        proxy_pass http://127.0.0.1:3005;
        proxy_http_version 1.1;

        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $realip_remote_addr;
        proxy_set_header X-Forwarded-Proto https;

        proxy_connect_timeout 10s;
        proxy_send_timeout 30s;
        proxy_read_timeout 60s;

        proxy_redirect off;

        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
    }
}

检查 Nginx 配置

重载 Nginx 配置

使用服务

登录管理后台

访问 shlink-dash.your.domain 面板,默认用户名和密码都是 admin,登录后会要求修改密码。

login.webp

添加服务器

重置密码后就进入管理后台了,点击添加服务器

add-server.webp

输入服务器信息和访问凭据即可

add-new-server.webp

现在就可以愉快的使用了。

enjoy.webp

批量导入短链

最近需要将短链域名由 via.moe 改为 zsh.moe,从后台导出了 short_urls.csv

需要转换 CSV 格式:

cat << 'EOF' > convert.py
import csv

input_file = 'short_urls.csv'
output_file = 'import.csv'

# 定义 Shlink 标准列名
# 映射关系: longUrl -> Long URL, shortCode -> Short code, title -> Title, domain -> Domain, tags -> Tags
field_map = {
    'longUrl': 'Long URL',
    'shortCode': 'Short code',
    'title': 'Title',
    'domain': 'Domain',
    'tags': 'Tags'
}

with open(input_file, mode='r', encoding='utf-8') as f_in:
    reader = csv.DictReader(f_in)
    
    with open(output_file, mode='w', encoding='utf-8', newline='') as f_out:
        writer = csv.DictWriter(f_out, fieldnames=field_map.values())
        writer.writeheader()
        
        for row in reader:
            # 提取并清理数据
            new_row = {
                'Long URL': row.get('longUrl', ''),
                'Short code': row.get('shortCode', ''),
                'Title': row.get('title', ''),
                'Domain': row.get('domain', ''),
                'Tags': row.get('tags', '').replace(',', '|') # 确保标签用 | 分隔
            }
            writer.writerow(new_row)

print(f"转换完成!已生成 {output_file}")
EOF

开始转换

设置权限

编辑 compose.yml,挂载文件到容器

services:
  db:
    #...

  shlink-backend:
    image: shlinkio/shlink:stable
	# ...
	# 添加挂载映射
    volumes:
      - ./import.csv:/etc/shlink/import.csv:ro
	#...

  shlink-web:
    #...

重新创建容器

sudo docker compose up -d

开始导入

sudo docker exec -it shlink-backend shlink short-url:import

开始导入

 What is the source you want to import from:
 > csv


 What's the path for the CSV file you want to import:
 > ./import.csv

 What's the delimiter used to separate values? [Comma]:
  [,] Comma
  [;] Semicolon
 > ,

Importing short URLs
====================

 https://squoosh.app/: Imported
 https://shottr.cc/: Imported

 [OK] Data properly imported!

完成。