基于 Docker 和 Nginx 部署 Shlink

声明:本文由 AI 生成。

基于 Docker 和 Nginx 部署 Shlink

【运维复盘】基于 Docker Compose 与 Nginx 构建 Shlink 高性能自建短网址服务(含单域名路由与 Nginx 底层坑点排查)

1. 前言与架构设计

在自建短网址服务时,Shlink 因其干净轻量、原生 API 支持以及强大的统计功能成为了首选。

为了保证系统的隔离性、易维护性以及未来更换 VPS 时的零成本迁移,本次部署采用 Docker Compose 容器化方案,并结合宿主机已有的 Nginx + Cloudflare SSL 证书 实现 HTTPS 卸载与精准反向代理。

系统整体架构

  • 数据库层 (shlink-db):PostgreSQL 15 (Alpine 镜像),数据持久化挂载至宿主机。
  • 后端 API 层 (shlink-backend):Shlink 官方核心服务,监听本地 127.0.0.1:8080
  • 前端 Web 面板 (shlink-frontend):Shlink 官方 Web Client,监听本地 127.0.0.1:8081
  • 反向代理层 (Nginx):宿主机 Nginx 统一监听 443 端口,通过单域名正则分流,将前端页面与后端 API 路由至对应的本地容器端口。

2. 环境准备与项目初始化

首先在 Cloudflare 中,新建一条 DNS 记录,让域名 s.kukmoon.com 指向 VPS 的 IP 地址。

第二,在 VPS 宿主机上创建专门的项目目录,用于统一存放 Docker 配置与持久化数据库文件。

1
2
# 创建并进入目录
mkdir -p /opt/shlink && cd /opt/shlink

3. 部署 Docker Compose 服务栈

/opt/shlink 目录下创建 docker-compose.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
services:
shlink-db:
image: postgres:15-alpine
container_name: shlink-db
restart: always
environment:
POSTGRES_DB: shlink
POSTGRES_USER: shlink
POSTGRES_PASSWORD: YourSuperPassword123 # 请修改为强密码
volumes:
- ./postgres_data:/var/lib/postgresql/data

shlink-backend:
image: ghcr.io/shlinkio/shlink:stable
container_name: shlink-backend
restart: always
depends_on:
- shlink-db
ports:
- "127.0.0.1:8080:8080" # 限制仅本地回环访问,提升安全性
environment:
- DEFAULT_DOMAIN=s.kukmoon.com
- IS_HTTPS_ENABLED=true
- DB_DRIVER=postgres
- DB_NAME=shlink
- DB_USER=shlink
- DB_PASSWORD=YourSuperPassword123
- DB_HOST=shlink-db

shlink-frontend:
image: ghcr.io/shlinkio/shlink-web-client:stable
container_name: shlink-frontend
restart: always
depends_on:
- shlink-backend
ports:
- "127.0.0.1:8081:8080" # 映射前端面板到本地 8081 端口

踩坑记录:镜像拉取失败(Forwarding failure
在启动容器时若遇到 Docker Hub 无法连接的问题,可以通过修改 /etc/docker/daemon.json 配置可靠的公共 DNS(如 1.1.1.18.8.8.8),并执行 systemctl restart docker 重启 Docker 服务解决。

拉取镜像并后台启动容器:

1
docker compose up -d

4. Nginx 单域名精细化反向代理配置

为了让前端管理面板与后端短网址跳转共用同一个域名(s.kukmoon.com),我们需要利用 Nginx 的正则表达式匹配,将不同路径的请求路由至对应的端口。

/etc/nginx/sites-available/s.kukmoon.com.conf 中写入如下配置,并创建软链接至 sites-enabled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# s.kukmoon.com
#

# 现代运维规范其实强烈反对在 server 块内部使用 if 指令(因为 Nginx 官方有一篇著名的技术文档叫作 “If is Evil”,if 在某些复杂场景下会导致非预期行为)。最优雅、效率最高、大厂生产环境标准的写法,是直接把 HTTP 和 HTTPS 拆分成两个独立的 server 块,连判断都不需要判断:

# 专门拦截纯明文 HTTP 流量的 server 块
server {
# 设置监听端口为 80
listen 80;
listen [::]:80;
# 设置用来伪装网站的域名
server_name s.kukmoon.com;
# http 跳转 https
return 301 https://$host$request_uri;
}

# 专门处理安全 HTTPS 流量的 server 块
server {
# 设置监听端口为 443
listen 443 ssl http2;
listen [::]:443 ssl http2;
# 设置用来伪装网站的域名
server_name s.kukmoon.com;
# 开启 gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

# 设置证书和密钥
ssl_certificate /etc/nginx/ssl/cfcert/kukmoon.com.pem;
ssl_certificate_key /etc/nginx/ssl/cfcert/kukmoon.com.key;
# 设置其他 SSL 参数
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000" always;
error_page 497 https://$host$request_uri;

# 通用代理头部配置(公共配置提取)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Ssl on;

# ----------------------------------------------------
# 1. 明确匹配 Shlink 前端 Web 面板的固定静态资源与页面路径 (打给 8081)
# ----------------------------------------------------
location ~ ^/(static|assets|favicon|logo|manifest|robots\.txt|edit|manage|settings|v[0-9]+) {
proxy_pass http://127.0.0.1:8081;
proxy_redirect off;
}

# 前端面板首页 (访问 s.kukmoon.com/ 直接打给 8081 面板)
location = / {
proxy_pass http://127.0.0.1:8081;
proxy_redirect off;
}

# ----------------------------------------------------
# 2. 其余所有访问(包括 REST API、短链接、301跳转等),全部打给 8080 后端
# ----------------------------------------------------
location / {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;

client_max_body_size 100m;
client_body_buffer_size 128k;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}

# 对用户屏蔽某些文件
location ~ ^/(\.ht\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
{
return 404;
}

#access_log /var/log/s.kukmoon.com.log;
#error_log /var/log/s.kukmoon.com.error.log;
}

5. 核心排坑复盘:域名哈希表内存溢出(server_names_hash

问题现象

配置完成后,重新加载 Nginx 并访问 [https://s.kukmoon.com](https://s.kukmoon.com),浏览器地址栏虽然是 s.kukmoon.com,但页面展现的却是同台 VPS 上另一个站点(博客)的内容。

通过本地 curl -vk --resolve s.kukmoon.com:443:127.0.0.1 [https://s.kukmoon.com](https://s.kukmoon.com) 测试发现,Nginx 在接收到 HTTPS 请求时,忽略了 s.kukmoon.com 的配置块,直接退化(Fallback)交给了默认站点(第一配置站点)处理。

原因深度剖析

在排除通配符拦截和端口配置冲突后,确认根因在于 Nginx 的域名哈希表内存溢出

当 VPS 上配置的子域名数量较多或域名字符较长时,Nginx 默认的域名哈希桶空间(server_names_hash_bucket_size,默认通常为 32 或 64 字节)会被挤爆。塞满后,Nginx 会在内存中静默放弃建立新子域名的索引,且不抛出任何语法报错,导致请求无法精准匹配到指定的 server_name

根治方案

修改主配置文件 /etc/nginx/nginx.conf,在 http { ... } 上下文内显式扩大域名哈希表的内存容量:

1
2
3
4
5
6
7
8
http {
# 扩大域名哈希表空间,防止多子域名索引失效
server_names_hash_bucket_size 256;
server_names_hash_max_size 2048;

# ... 其他全局配置保持不变 ...
}

修改完成后,测试并重载 Nginx:

1
2
sudo nginx -t && sudo systemctl reload nginx

页面即刻恢复正常,顺利加载 Shlink Web Client 前端管理界面。


6. 服务初始化与 API 绑定

  1. 生成 API 密钥
    在 VPS 终端执行以下命令生成 Backend API 访问 Key:
1
docker exec -it shlink-backend shlink api-key:generate
  1. 连接面板
  • 浏览器访问 [https://s.kukmoon.com](https://s.kukmoon.com) 进入 Web 面板。

  • 点击 Add a Server

  • Name: 自定义服务名称(如 My Shortener

  • URL: [https://s.kukmoon.com](https://s.kukmoon.com)

  • API Key: 粘贴刚才生成的 API 密钥。

  • 保存后即可开启完全自主可控的短网址生成与数据分析之旅!


7. 定位访客位置(可选)

声明:我用不到这个,所以没做,在此仅记录步骤供读者参考。

Geodata(地理位置数据),在 Shlink 中是用来定位访客位置的。

它的作用是:当有人点击你的短链接时,Shlink 可以通过点击者的 IP 地址,自动分析出他来自哪个国家、哪个城市,并在 Shlink Web Client 前端面板的统计地图里直观展示出来。

Shlink 默认使用的是全球最大的 IP 数据库提供商 MaxMind 提供的免费 GeoLite2 数据库。因为 MaxMind 要求使用者必须注册免费账号并生成密钥才能下载,所以我们需要在 MaxMind 网站上免费注册并获取一个 License Key(许可密钥)

整个操作非常简单,只需要 5 分钟。


第一步:注册 MaxMind 免费账号

  1. 打开 MaxMind 的 GeoLite2 注册页面:https://www.maxmind.com/en/geolite2/signup
  2. 填写注册表单(支持任意邮箱,按照要求填入姓名、邮箱和公司/个人信息等)。
  3. 提交后,MaxMind 会发送一封确认邮件到你的邮箱。打开邮件,点击里面的验证链接设置账号密码。

第二步:生成 GeoIP License Key (密钥)

  1. 登录 MaxMind 控制台。
  2. 在左侧菜单栏或 User Menu 中,找到并点击 Account -> Manage License Keys(管理许可密钥)。
  3. 点击 Generate new license key(生成新的许可密钥)。
  4. 系统会弹出一个配置提示:
  • License key description:随手填一个名称(例如:Shlink GeoIP)。
  • Will this key be used for GeoIP Update?:勾选 Yes
  1. 点击 Confirm(确认)。
  2. 【非常重要】 页面会展示生成出来的 License Key(一串由字母和数字组成的长密钥,如 a1b2c3d4e5f6...)。
  • 请立刻把这串密钥复制保存好。因为页面一旦刷新,出于安全考虑,这串密钥就再也看不到明文了!

拿到了密钥,我们只需要把这个密钥作为环境变量告诉 Shlink 即可。

  1. 登录你的 VPS 终端,编辑之前创建的 /opt/shlink/docker-compose.yml 文件:
1
nano /opt/shlink/docker-compose.yml
  1. shlink-backend 服务的 environment: 列表下,增加一行 GEOLITE_LICENSE_KEY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shlink-backend:
image: ghcr.io/shlinkio/shlink:stable
container_name: shlink-backend
restart: always
depends_on:
- shlink-db
ports:
- "127.0.0.1:8080:8080"
environment:
- DEFAULT_DOMAIN=s.kukmoon.com
- IS_HTTPS_ENABLED=true
- DB_DRIVER=postgres
- DB_NAME=shlink
- DB_USER=shlink
- DB_PASSWORD=YourSuperPassword123
- DB_HOST=shlink-db
- GEOLITE_LICENSE_KEY=你刚复制的MaxMind密钥 # <---【新增这一行】

  1. 保存并退出编辑器(Ctrl + O -> 回车 -> Ctrl + X)。

/opt/shlink 目录下,运行下面这行命令重新应用配置:

1
docker compose up -d

验证效果

配置完成后,当有别人通过你的短链接访问网站时:

  1. Shlink 后台会自动去 MaxMind 官方下载/更新 IP 地理位置数据库。
  2. 打开你的短网址管理面板([https://s.kukmoon.com](https://s.kukmoon.com)),进入某个短链接的 Visits(访问统计) 页面,你就会看到非常精美的世界地图分布,以及访问者来自哪个国家、城市、甚至 ISP 运营商的详细记录了!

如果不配置这个密钥,Shlink 依然能正常缩短和重定向链接,只是访问统计里会丢失城市/国家数据,所以配置上体验会更好!

图片版权

题图:AI 生成。

头图:https://pixabay.com/photos/sea-rocks-mountains-croatia-coast-6948569/


求扫码打赏
“我这么可爱,请给我钱 o(*^ω^*)o”

基于 Docker 和 Nginx 部署 Shlink
https://blog.kukmoon.com/af2950f40947/
作者
Kukmoon谷月
发布于
2026年8月4日
许可协议