引言
Shlink 是一款基于 PHP 的自托管网址缩短服务。由于个人对 PHP 存在一定心理门槛😅,此前一直未尝试部署。近期阅读项目文档后发现,它实际上支持多种 PHP Runtime(运行时):
- RoadRunner
- FrankenPHP
- 传统方式(Web Server + FastCGI)
其中,RoadRunner 是 Shlink 官方推荐的默认运行方式。它是一款由 Go 编写的 PHP 运行时,通过多个 Worker 将 Shlink 常驻于内存 (RAM) 中运行,在响应速度和整体性能方面表现良好。
部署 Shlink
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 sudo docker compose up -d
生成访问凭据
使用 Shlink CLI 工具生成访问密钥,用于后续 Web 管理面板添加服务器
sudo docker exec -it shlink-backend shlink api-key:generate
看起来是 UUID 格式,复制保留备用

[附] 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 配置
sudo nginx -t
重载 Nginx 配置
sudo nginx -s reload
使用服务
登录管理后台
访问 shlink-dash.your.domain 面板,默认用户名和密码都是 admin,登录后会要求修改密码。

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

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

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



glzjin's blog
文章评论