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

推荐订阅源

爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
S
Secure Thoughts
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
H
Heimdal Security Blog
H
Help Net Security
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
F
Full Disclosure
小众软件
小众软件
S
Securelist
罗磊的独立博客
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
Know Your Adversary
Know Your Adversary
S
Schneier on Security
D
DataBreaches.Net
M
MIT News - Artificial intelligence
V
Vulnerabilities – Threatpost
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
T
Tenable Blog
The Register - Security
The Register - Security
C
Check Point Blog
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google Online Security Blog
Google Online Security Blog
博客园 - 叶小钗
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美

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 证书用于个人邮件服务 迁移到 NameCrane Mail 域名邮箱套件服务 香烟与咖啡 在 Windows 中使用 Yubikey 的 OpenPGP 应用 Windows 11 IoT LTSC 养老指南 巧用浏览器生成 HAR 文件批量下载所需网页资源 聊聊静态网站的评论系统 在 Hugo 站点中优雅地展示实况照片 使用 snac 部署一个简洁优雅的 ActivityPub 实例 当 25 岁来临的时候 断网的这两年 从 Bitwarden/Vaultwarden 到 KeePassXC 也许是 Android 上最好的本地播放器 - 椒盐音乐 2023 半年小结 写一个好玩的 Hugo 货币汇率转换短代码 写一个 Hugo 短代码将不同类别的总字数统计转换为书名显示 海信 Touch Lite 墨水屏音乐阅读器的简单体验 从 Debian 11(bullseye) 无感升级到 Debian 12(bookworm) 使用 GitHub Actions 自动提交 URL 到 IndexNow 批量重写 Git 历史提交记录 使用 cgit 托管小小的也很可爱的 Git 服务器 在 Debian 上使用 stagit 和 Nginx 的简单 Git 服务器 托管简单 Git 服务器的一些尝试 使用 stagit 自托管纯静态 Git 存储库 使用 Docker 快速部署单人 Fediverse 实例 我是如何建立自己的个人博客的? 简单、隐私友好的谷歌分析替代品,Plausible 自托管部署指南 服务器使用 Cloudflare CDN 的最佳实践 甲骨文服务器分配并启用 IPv6 地址 Git 版本控制学习笔记(二) Cloudflare WAF 防护策略简易指南 Mastodon 服务器批量导入自定义表情 低成本自托管 Mastodon 实例简明指南 自托管服务指南──有了服务器可以做什么? 使用 Miniflux + RSSHub 打造个人 RSS 阅读器 构建支持多种 CPU 架构的 Docker 镜像 一个可自托管的静态短链接应用 别了,二〇二二! Duplicacy CLI 进阶使用指南 Duplicacy CLI 备份工具的基本使用 初探 Cloudflare 零信任 - 通过 Docker 部署 Bitwarden 密码管理器 Yubikey 转运日志及上手指南 初探 Cloudflare 零信任 — 通过 Cloudflare Tunnel 搭建 SSH 聊天室 初探 Cloudflare 零信任 - 通过 Cloudflare Tunnel 访问服务 某科学的 PGP 算号指南 2022 年的 MacBook Air M1 使用体验及应用推荐 使用 Vercel 和 Supabase 自托管 Cusdis 评论系统 为什么你的搜索引擎不好用? 个人在 Windows 上常用软件清单 macOS 基础开发环境设置指南 在 macOS 上使用 Canokey 的 OpenPGP 应用 提取 Windows UWP 应用商店的安装包以供离线安装 Git 版本控制学习笔记(一) 使用 Notion 管理 GitHub Star 项目 使用 Vercel 免费部署 Giscus 评论系统 将博客评论从 Utterances 无缝迁移到 Giscus Canokey 签名和认证应用指南 Canokey 的基本使用指南 Canokey Pigeon 揽件日志及简单开箱 关于 Google Play 保护机制和 DRM 安全等级的那些事 记录一次捡垃圾装机 GitHub 使用 GPG 密钥提交签名认证 使用 Scoop 管理 Windows 下的软件和开发环境 Windows Terminal+PowerShell 7 打造 Windows 下最好用的终端 一个好用的 Telegram 文件批量上传/下载工具 Clash for Windows 优雅地使用 TUN 模式接管系统流量 全平台搭建 Hexo 静态博客指南 尝试将 Manjaro 作为主力操作系统使用 Hexo 进行 SEO 优化的基本指南 使用本地代理给 Git 和 NPM 加速 使用 flowerss 部署 Telegram RSS 订阅机器人
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!

完成。