全栈自动化与纵深防御:构建基于 Cloudflare、Docker 与 Shlink 的生产级短链系统

本文基于AI生成写作 作者: Gemini & Claude
关键词: DevOps, Cloudflare, Docker Security, RESTful API, Python Middleware, TCP/IP Debugging


📋 目录


🏗️ 第一章:顶层架构设计与技术哲学

在动手敲代码之前,我们需要理解整个系统全貌。我们最终搭建的不再是一个简单的"软件",而是一个具有纵深防御体系(Defense in Depth)的分布式系统。

1.1 架构全景图

我们的流量链路经历了四个关键节点的流转:

用户请求
   ↓
[最外层] Cloudflare CDN + WAF
   ↓ (HTTPS)
[入口层] VPS 宿主机 (Python Middleware)
   ↓ (HTTP/localhost)
[核心层] Docker 容器 (Shlink)
   ↓
[存储层] SQLite Volume

关键节点解析:

  1. 最外层 (The Edge): Cloudflare

    • 职责: 拦截 DDoS 攻击、清洗恶意流量、强制 HTTPS 加密
    • 技术: Anycast 网络、WAF 规则、SSL/TLS 终止
  2. 入口层 (Ingress): VPS 宿主机

    • 职责: 运行 Telegram Bot,作为用户与核心服务的"胶水"
    • 技术: Python asyncio、systemd 守护进程
  3. 核心层 (Core): Docker 容器

    • 职责: Shlink 服务运行环境,隔离依赖
    • 技术: Docker Compose、网络命名空间、资源限制
  4. 控制层 (Control): Systemd

    • 职责: 进程生命周期管理,确保服务永不掉线
    • 技术: Unit 文件、依赖管理、自动重启

1.2 选型方法论:为何弃用 Gokapi?

在项目初期,我们选择了 Gokapi。它的失败给我们上了生动的一课:

Gokapi 的问题:

  • 单体应用的脆弱性: All-in-One 设计,鉴权、存储、Web 服务都在一个二进制文件里
  • 调试困难: 遇到 UTC 时间跨年 Bug 时,由于 Debug 模式开启失败,系统变得不可维护
  • API 非标准化: Header 定义和 Token 校验机制非主流,对接自动化脚本困难

Shlink 的优势:

  • 解耦设计: 数据库、后端、前端分离
  • 标准规范: 遵循 RESTful API 标准,错误返回清晰(Problem Details RFC 7807)
  • 成熟生态: Docker 镜像稳定,环境变量配置灵活,社区活跃

1.3 技术决策矩阵

考量维度 Gokapi Shlink 权重
API 标准化 ⭐⭐ ⭐⭐⭐⭐⭐
可维护性 ⭐⭐ ⭐⭐⭐⭐⭐
容器化支持 ⭐⭐⭐ ⭐⭐⭐⭐⭐
文档完整性 ⭐⭐ ⭐⭐⭐⭐
轻量级 ⭐⭐⭐⭐ ⭐⭐⭐

🛡️ 第二章:第一道防线——Cloudflare 的安全接管

在生产环境中,暴露源站 IP 是大忌。Cloudflare (CF) 是我们系统架构中不可或缺的安全层。

2.1 DNS 与 CDN 的原理

技术原理:

当我们把域名托管给 CF 时,实际上是修改了 NS (Name Server) 记录。

不使用 CF 时:

用户 → DNS 解析 → 你的真实 IP (1.2.3.4)
攻击者可以直接攻击这个 IP

开启 CF 代理 (小黄云) 后:

用户 → DNS 解析 → CF 的 Anycast 边缘节点 IP
     → CF 节点作为反向代理访问你的源站

2.2 实操配置步骤

Step 1: DNS 记录配置

在 Cloudflare Dashboard:

DNS Records:
  Type: A
  Name: t (或 @)
  IPv4: 你的VPS真实IP
  Proxy status: Proxied ☁️ (橙色云朵图标)
  TTL: Auto

Step 2: 源站 IP 隐藏

在 VPS 上配置防火墙,只允许 Cloudflare IP 段访问:

# 安装 UFW
sudo apt update
sudo apt install ufw
# 默认规则:拒绝所有入站
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 允许 SSH (务必先配置,否则会被锁在外面)
sudo ufw allow 22/tcp
# 下载并应用 Cloudflare IP 范围
# IPv4
curl https://www.cloudflare.com/ips-v4 | while read ip; do
sudo ufw allow from $ip to any port 80,443 proto tcp
done
# IPv6
curl https://www.cloudflare.com/ips-v6 | while read ip; do
sudo ufw allow from $ip to any port 80,443 proto tcp
done
# 启用防火墙
sudo ufw enable
# 查看状态
sudo ufw status verbose

⚠️ 安全提示: 执行此操作前,确保你有其他方式访问服务器(如 VNC 控制台),以防 SSH 被误封锁。

2.3 SSL/TLS 加密模式深度解析

为了实现全链路 HTTPS,我们需要正确配置加密模式。

加密模式对比

模式 用户→CF CF→源站 优点 缺点 推荐度
Off HTTP HTTP 完全不安全
Flexible HTTPS HTTP 源站无需证书 CF到源站明文传输 ⚠️
Full HTTPS HTTPS 全链路加密 接受自签名证书
Full (Strict) HTTPS HTTPS 全链路加密+证书验证 需要有效证书 ⭐ 推荐

配置 Full (Strict) 模式

方案 A: 使用 Cloudflare Origin CA 证书

  1. 在 Cloudflare Dashboard → SSL/TLS → Origin Server
  2. 点击 "Create Certificate"
  3. 选择证书有效期(推荐 15 年)
  4. 复制生成的证书和私钥

在 VPS 上保存证书:

sudo mkdir -p /etc/ssl/cloudflare
sudo nano /etc/ssl/cloudflare/cert.pem
# 粘贴证书内容
sudo nano /etc/ssl/cloudflare/key.pem
# 粘贴私钥内容
sudo chmod 600 /etc/ssl/cloudflare/key.pem

方案 B: 使用 Let's Encrypt

# 安装 Certbot
sudo apt install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d your.domain
# 证书自动续期
sudo certbot renew --dry-run

2.4 WAF (Web Application Firewall) 规则配置

Shlink 的 API 接口非常敏感,需要防护暴力破解。

规则 1: API 接口保护

创建规则路径: Security → WAF → Custom rules → Create rule

规则名称: Shlink API Protection
表达式:
  (http.request.uri.path contains "/rest/v3/") and
  (http.request.method eq "POST") and
  (cf.threat_score gt 10)

动作: Managed Challenge

原理:

  • 正常浏览器或 Telegram Bot 能通过验证
  • 简单的 Python 爬虫或攻击脚本会被拦截

规则 2: 速率限制

创建规则路径: Security → WAF → Rate limiting rules → Create rule

规则名称: API Rate Limit
请求条件:
  URI Path: /rest/v3/*
  
速率限制:
  10 requests per 10 seconds
  
匹配条件: IP 地址
阻断持续时间: 60 秒
动作: Block

规则 3: 地理位置过滤 (可选)

如果你的服务仅面向特定区域:

规则名称: Geographic Restriction
表达式:
  (ip.geoip.country ne "CN") and
  (ip.geoip.country ne "US") and
  (http.request.uri.path contains "/rest/")

动作: Block

2.5 附加安全措施

启用 Bot Fight Mode

Security → Bots → Configure
  √ Bot Fight Mode: ON
  √ Super Bot Fight Mode: ON (付费版)

配置缓存规则

Caching → Configuration → Cache Rules → Create rule

规则名称: No Cache for API
表达式: http.request.uri.path contains "/rest/"
设置: 
  Cache eligibility: Bypass cache

原因: API 响应不应该被缓存,确保数据实时性。


🐞 第三章:Gokapi 排错实录——TCP/IP 协议深潜

这一章记录了我们最痛苦但也最有价值的排错过程。这不仅是修 Bug,更是一次网络协议的教学。

3.1 现象回顾

症状:

  • 服务返回 400 Bad Request
  • 应用层日志完全空白
  • Gokapi Debug 模式无法开启

初步分析: 通常意味着请求在逻辑处理之前就被拦截了(鉴权层、协议层)。

3.2 TCPDump:上帝视角抓包

当 Log 失效时,我们需要看"线缆里流的是什么"。

基础抓包命令

# 监听特定端口
sudo tcpdump -i any -n -A -s 0 'port 53842'

参数解析:

  • -i any: 监听所有网络接口(包括 docker0 和 eth0)
  • -n: 不解析主机名(加快速度)
  • -A: ASCII 模式显示内容(关键:看到 HTTP Header)
  • -s 0: 抓取完整包,不截断(默认只抓 68 字节)
  • 'port 53842': BPF 过滤器表达式

高级过滤示例

# 只抓 HTTP POST 请求
sudo tcpdump -i any -A -s 0 'tcp port 8080 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)'
# 抓取包含特定字符串的包
sudo tcpdump -i any -A -s 0 'tcp port 8080' | grep -A 10 -B 5 "X-Api-Key"
# 保存到文件供 Wireshark 分析
sudo tcpdump -i any -s 0 -w /tmp/capture.pcap 'port 8080'

3.3 关键发现

通过抓包,我们发现了时间穿越 Bug:

HTTP/1.1 400 Bad Request
Date: Mon, 01 Jan 2026 08:00:00 GMT
Server: Gokapi/1.8.0
Content-Type: application/json

{"error": "API key expired"}

数据库检查:

sqlite3 gokapi.db "SELECT * FROM ApiKeys;"

输出:

key          | expiry     | permissions
abc123...    | 1735689600 | admin
             | (2025-12-31 23:59:59)

根因:

  • CurrentTime (2026-01-01) > ExpiryTime (2025-12-31) → Token Expired → 400 Error
  • Docker 容器的系统时钟与宿主机不同步(或基础镜像缺少时区配置)

3.4 临时修复方案

方案 A: 数据库外科手术

# 备份数据库
cp gokapi.db gokapi.db.backup
# 修改过期时间为 2099 年
sqlite3 gokapi.db <<EOF
UPDATE ApiKeys
SET Expiry = 4102444800
WHERE key = 'abc123...';
.quit
EOF
# 验证
sqlite3 gokapi.db "SELECT key, datetime(Expiry, 'unixepoch') FROM ApiKeys;"

⚠️ 警告: 这是"核武器"级别的运维操作,慎用。生产环境应该通过 API 或管理界面操作。

方案 B: 容器时间同步

# 检查容器时间
docker exec gokapi date
# 同步宿主机时间到容器
docker run -it --rm \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/timezone:/etc/timezone:ro \
gokapi:latest date

3.5 教训总结

  1. 日志不是唯一真相: 当应用层日志失效时,网络层抓包是最后的事实来源
  2. 时间同步的重要性: 分布式系统中,时钟偏移(Clock Skew)是经典问题
  3. 数据库是 Source of Truth: 前端可以欺骗你,日志可以欺骗你,但数据库里的二进制数据不会撒谎

🐳 第四章:Shlink 容器化部署与数据持久化

4.1 Docker Compose 的声明式管理

我们放弃了 docker run 的命令式操作,转而使用 docker-compose.yml

完整配置文件

创建 /opt/shlink/docker-compose.yml:

version: '3.8'
services:
shlink:
image: shlinkio/shlink:stable
container_name: shlink
restart: always # 容器崩溃自动重启
environment:
# 核心配置
- DEFAULT_DOMAIN=your.domain
- IS_HTTPS_ENABLED=true
- GEOLITE_LICENSE_KEY=${GEOLITE_KEY} # 从环境变量读取
# 数据库配置 (使用内置 SQLite)
- DB_DRIVER=sqlite
# 性能优化
- INITIAL_API_KEYS=${INITIAL_API_KEY} # 首次启动自动生成 API Key
# 安全配置
- DISABLE_TRACKING=false # 启用访问统计
- ANONYMIZE_REMOTE_ADDR=true # 匿名化 IP 地址(GDPR 合规)
ports:
- "127.0.0.1:8080:8080" # 只监听本地回环,不暴露到公网
volumes:
- ./data:/etc/shlink/data # 数据持久化
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/rest/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- shlink-network
# 资源限制(防止内存泄漏)
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 256M
networks:
shlink-network:
driver: bridge

环境变量文件

创建 .env 文件(不要提交到 Git):

GEOLITE_KEY=your_maxmind_license_key
INITIAL_API_KEY=your_generated_api_key_here

4.2 深入理解 Volume (卷)

Bind Mount vs Named Volume

# Bind Mount (绑定挂载) - 我们使用的方式
volumes:
- ./data:/etc/shlink/data
# Named Volume (命名卷)
volumes:
- shlink-data:/etc/shlink/data
volumes:
shlink-data:
driver: local

对比:

特性 Bind Mount Named Volume
路径 宿主机绝对/相对路径 Docker 管理的内部路径
可见性 直接在文件系统可见 需通过 Docker 命令访问
备份 cp -r ./data backup/ docker run --rm -v ...
适用场景 开发环境、需直接访问 生产环境、数据隔离

数据持久化验证

# 启动服务
docker compose up -d
# 创建一个短链接
curl -X POST http://localhost:8080/rest/v3/short-urls \
-H "X-Api-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"longUrl": "https://example.com"}'
# 检查数据文件
ls -lh /opt/shlink/data/
# 应该能看到 database.sqlite
# 销毁容器
docker compose down
# 数据文件依然存在
ls -lh /opt/shlink/data/
# database.sqlite 仍然在
# 重新启动,数据恢复
docker compose up -d
# 之前创建的短链接仍然有效

4.3 API Key 生成与管理

方法 1: 通过 CLI 生成

docker exec -it shlink shlink api-key:generate

输出示例:

Generated API key: abc123def456...

方法 2: 通过环境变量自动生成

docker-compose.yml 中添加:

environment:
- INITIAL_API_KEYS=my_predefined_key_12345

首次启动时会自动创建该 Key

验证 API Key

curl -X GET http://localhost:8080/rest/v3/short-urls \
-H "X-Api-Key: your_api_key"

成功返回:

{
"shortUrls": {
"data": [],
"pagination": {...}
}
}

失败返回(401):

{
"title": "Invalid API key",
"type": "https://shlink.io/api/error/invalid-api-key",
"detail": "Provided API key does not exist or is invalid",
"status": 401
}

4.4 部署命令集

# 启动服务
cd /opt/shlink
docker compose up -d
# 查看日志
docker compose logs -f
# 查看实时日志(最近 50 行)
docker compose logs --tail=50 -f shlink
# 重启服务
docker compose restart
# 停止并删除容器(数据保留)
docker compose down
# 停止并删除容器及数据(危险操作)
docker compose down -v
# 更新镜像
docker compose pull
docker compose up -d
# 查看容器状态
docker compose ps
# 查看资源使用
docker stats shlink

🐍 第五章:Python 中间件开发

这是连接 Telegram 和 Shlink 的桥梁,我们采用了 Python Middleware 模式。

5.1 依赖管理

创建 requirements.txt:

python-telegram-bot==20.7
requests==2.31.0
python-dotenv==1.0.0
aiohttp==3.9.1

安装依赖:

cd /opt/shlink_bot
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

5.2 完整 Bot 代码

创建 bot.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import logging
import asyncio
import aiohttp
from telegram import Update
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes
)
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 配置日志
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# 配置常量
TELEGRAM_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
SHLINK_API_URL = os.getenv('SHLINK_API_URL', 'http://localhost:8080/rest/v3')
SHLINK_API_KEY = os.getenv('SHLINK_API_KEY')
ALLOWED_USER_IDS = [int(uid) for uid in os.getenv('ALLOWED_USER_IDS', '').split(',') if uid]
# 验证配置
if not all([TELEGRAM_TOKEN, SHLINK_API_KEY]):
logger.error("缺少必要的环境变量!")
exit(1)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理 /start 命令"""
user = update.effective_user
await update.message.reply_text(
f'你好 {user.mention_html()}\n\n'
'发送任何链接给我,我会为你创建短链接。\n\n'
'命令列表:\n'
'/start - 查看帮助\n'
'/stats - 查看统计信息',
parse_mode='HTML'
)
async def create_short_url(long_url: str) -> dict:
"""调用 Shlink API 创建短链接"""
headers = {
"X-Api-Key": SHLINK_API_KEY,
"Content-Type": "application/json"
}
payload = {
"longUrl": long_url,
"validateUrl": False, # 不验证 URL 有效性
"findIfExists": True, # 如果已存在则返回现有短链
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{SHLINK_API_URL}/short-urls",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 200:
data = await response.json()
return {
'success': True,
'short_url': data.get('shortUrl'),
'long_url': data.get('longUrl')
}
else:
error_text = await response.text()
logger.error(f"Shlink API 错误: {response.status} - {error_text}")
return {
'success': False,
'error': f'API 返回错误: {response.status}'
}
except asyncio.TimeoutError:
logger.error("Shlink API 请求超时")
return {'success': False, 'error': '请求超时,请稍后重试'}
except Exception as e:
logger.error(f"创建短链接时发生异常: {e}")
return {'success': False, 'error': str(e)}
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理用户消息"""
# 权限检查
if ALLOWED_USER_IDS and update.effective_user.id not in ALLOWED_USER_IDS:
await update.message.reply_text("❌ 你没有使用此 Bot 的权限。")
return
text = update.message.text
# 简单的 URL 验证
if not (text.startswith('http://') or text.startswith('https://')):
await update.message.reply_text(
"⚠️ 请发送有效的 URL(以 http:// 或 https:// 开头)"
)
return
# 发送处理中消息
processing_msg = await update.message.reply_text("⏳ 正在生成短链接...")
# 创建短链接
result = await create_short_url(text)
if result['success']:
await processing_msg.edit_text(
f"✅ 短链接创建成功!\n\n"
f"🔗 短链接: {result['short_url']}\n"
f"📎 原始链接: {result['long_url'][:50]}..."
)
else:
await processing_msg.edit_text(
f"❌ 创建失败\n\n"
f"错误信息: {result.get('error', '未知错误')}"
)
async def get_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""获取统计信息"""
if ALLOWED_USER_IDS and update.effective_user.id not in ALLOWED_USER_IDS:
await update.message.reply_text("❌ 你没有使用此 Bot 的权限。")
return
headers = {"X-Api-Key": SHLINK_API_KEY}
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{SHLINK_API_URL}/short-urls",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 200:
data = await response.json()
total = data.get('shortUrls', {}).get('pagination', {}).get('totalItems', 0)
await update.message.reply_text(
f"📊 统计信息\n\n"
f"短链接总数: {total}"
)
else:
await update.message.reply_text("❌ 获取统计信息失败")
except Exception as e:
logger.error(f"获取统计信息时发生异常: {e}")
await update.message.reply_text(f"❌ 错误: {str(e)}")
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""全局错误处理"""
logger.error(f"Update {update} caused error {context.error}")
if update and update.effective_message:
await update.effective_message.reply_text(
"⚠️ 处理你的请求时发生了错误,请稍后重试。"
)
def main():
"""主函数"""
# 创建 Application
application = Application.builder().token(TELEGRAM_TOKEN).build()
# 注册处理器
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("stats", get_stats))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
# 注册错误处理器
application.add_error_handler(error_handler)
# 启动 Bot
logger.info("Bot 启动中...")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == '__main__':
main()

5.3 环境变量配置

创建 .env 文件:

# Telegram Bot Token (从 @BotFather 获取)
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
# Shlink 配置
SHLINK_API_URL=http://localhost:8080/rest/v3
SHLINK_API_KEY=your_shlink_api_key_here
# 访问控制(可选,留空则允许所有用户)
ALLOWED_USER_IDS=123456789,987654321

5.4 异步编程要点

同步 vs 异步对比:

# 同步版本(阻塞)
def create_short_url_sync(url):
response = requests.post(...) # 这里会阻塞整个程序
return response.json()
# 异步版本(非阻塞)
async def create_short_url_async(url):
async with aiohttp.ClientSession() as session:
async with session.post(...) as response: # 可以处理其他请求
return await response.json()

性能差异:

  • 同步: 100 个请求 × 1 秒 = 100 秒
  • 异步: 100 个请求 / 并发 = ~1-2 秒

5.5 测试与调试

# 激活虚拟环境
source venv/bin/activate
# 直接运行测试
python bot.py
# 查看详细日志
python bot.py 2>&1 | tee bot.log
# 后台运行
nohup python bot.py > bot.log 2>&1 &

🔒 第六章:Linux 系统级安全加固

在服务跑通后,安全是第一优先级。我们实施最小权限原则(Principle of Least Privilege)。

6.1 风险:Root 用户的达摩克利斯之剑

攻击场景:

1. Python 脚本以 root 运行
2. requests 库爆出反序列化漏洞
3. 攻击者发送恶意构造的链接
4. Python 解析时执行恶意代码
5. 攻击者获得 Root Shell
6. 你的 VPS 变成矿机/被勒索

6.2 创建影子用户

Step 1: 创建系统用户

# 创建专用用户(无家目录,无登录 Shell)
sudo useradd -r -s /bin/false -M -d /opt/shlink_bot tgbot_user
# 参数说明:
# -r: 创建系统账户
# -s /bin/false: 禁止登录
# -M: 不创建家目录
# -d: 指定工作目录(但不实际创建)

Step 2: 设置目录权限

# 创建工作目录
sudo mkdir -p /opt/shlink_bot
# 转移所有权
sudo chown -R tgbot_user:tgbot_user /opt/shlink_bot
# 设置权限(用户可读写执行,其他人无权限)
sudo chmod 750 /opt/shlink_bot

Step 3: 验证权限

ls -ld /opt/shlink_bot
# 输出: drwxr-x--- 2 tgbot_user tgbot_user 4096 Jan 2 10:00 /opt/shlink_bot
# 测试是否可以写入
sudo -u tgbot_user touch /opt/shlink_bot/test.txt
sudo -u tgbot_user rm /opt/shlink_bot/test.txt

6.3 Systemd 服务配置

创建 /etc/systemd/system/shlink-bot.service:

[Unit]
Description=Telegram Shlink Bot
Documentation=https://github.com/your-repo
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=tgbot_user
Group=tgbot_user
WorkingDirectory=/opt/shlink_bot
# 环境变量(从文件加载)
EnvironmentFile=/opt/shlink_bot/.env
# 启动命令
ExecStart=/opt/shlink_bot/venv/bin/python /opt/shlink_bot/bot.py
# 重启策略
Restart=always
RestartSec=10
# 安全加固
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/shlink_bot
# 资源限制
LimitNOFILE=4096
MemoryLimit=512M
CPUQuota=50%
# 日志配置
StandardOutput=journal
StandardError=journal
SyslogIdentifier=shlink-bot
[Install]
WantedBy=multi-user.target

6.4 服务管理命令

# 重新加载 systemd 配置
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start shlink-bot
# 设置开机自启
sudo systemctl enable shlink-bot
# 查看状态
sudo systemctl status shlink-bot
# 查看日志(实时)
sudo journalctl -u shlink-bot -f
# 查看最近 100 行日志
sudo journalctl -u shlink-bot -n 100
# 重启服务
sudo systemctl restart shlink-bot
# 停止服务
sudo systemctl stop shlink-bot

6.5 附加安全措施

禁用 Root SSH 登录

编辑 /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no # 只允许密钥登录
PubkeyAuthentication yes

重启 SSH:

sudo systemctl restart sshd

配置 Fail2Ban

# 安装
sudo apt install fail2ban
# 配置
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
# 修改:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
# 启动
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

📊 第七章:监控与日志管理

7.1 日志集中管理

配置日志轮转

创建 /etc/logrotate.d/shlink-bot:

/var/log/shlink-bot/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0640 tgbot_user tgbot_user
    sharedscripts
    postrotate
        systemctl reload shlink-bot > /dev/null 2>&1 || true
    endscript
}

7.2 性能监控

使用 htop 监控资源

sudo apt install htop
htop -p $(pgrep -f bot.py)

使用 docker stats

# 实时监控容器资源
docker stats shlink
# 输出格式化
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

7.3 健康检查脚本

创建 /opt/shlink_bot/health_check.sh:

#!/bin/bash
# 检查 Bot 进程
if ! pgrep -f "bot.py" > /dev/null; then
echo "ERROR: Bot process not running"
systemctl restart shlink-bot
exit 1
fi
# 检查 Shlink 容器
if ! docker ps | grep -q shlink; then
echo "ERROR: Shlink container not running"
cd /opt/shlink && docker compose up -d
exit 1
fi
# 检查 API 可用性
if ! curl -s -f http://localhost:8080/rest/health > /dev/null; then
echo "ERROR: Shlink API not responding"
exit 1
fi
echo "OK: All services healthy"
exit 0

添加到 crontab:

# 每 5 分钟检查一次
*/5 * * * * /opt/shlink_bot/health_check.sh >> /var/log/health_check.log 2>&1

📁 第八章:项目文件结构与部署清单

8.1 最终目录结构

/opt/
├── shlink/                     # [后端服务域]
│   ├── docker-compose.yml      # 容器编排配置
│   ├── .env                    # 环境变量(敏感信息)
│   └── data/                   # [持久化数据]
│       ├── database.sqlite     # SQLite 数据库
│       └── geoLite2/           # IP 地理位置数据
│
├── shlink_bot/                 # [中间件域]
│   ├── bot.py                  # 主程序
│   ├── requirements.txt        # Python 依赖
│   ├── .env                    # Bot 配置
│   ├── venv/                   # 虚拟环境
│   ├── health_check.sh         # 健康检查脚本
│   └── logs/                   # 日志目录
│
├── gokapi_archive/             # [归档] 失败的 Gokapi 遗迹
│   └── LESSONS_LEARNED.md      # 经验教训文档
│
└── backups/                    # 备份目录
    ├── shlink_data_YYYYMMDD.tar.gz
    └── bot_config_YYYYMMDD.tar.gz

8.2 部署检查清单

1. Cloudflare 配置 ☁️

  • 域名已添加到 Cloudflare
  • DNS 记录已设置为 Proxied(橙色云朵)
  • SSL/TLS 模式设置为 Full (Strict)
  • Origin CA 证书已安装到 VPS
  • WAF 规则已配置(API 保护)
  • Rate Limiting 已启用
  • Bot Fight Mode 已开启

2. VPS 安全配置 🔒

  • UFW 防火墙已配置,只允许 Cloudflare IP
  • SSH 已禁用 Root 登录
  • Fail2Ban 已安装并启用
  • 系统已更新到最新版本(apt update && apt upgrade
  • 时区已正确配置(timedatectl set-timezone Asia/Shanghai

3. Docker 服务 🐳

  • Docker 和 Docker Compose 已安装
  • Shlink 容器运行正常(docker ps
  • 端口 8080 只监听 localhost(netstat -tulpn | grep 8080
  • Volume 数据已持久化(ls /opt/shlink/data
  • API Key 已生成并测试

4. Python Bot 🐍

  • 虚拟环境已创建(/opt/shlink_bot/venv
  • 依赖已安装(pip list
  • .env 文件已配置(Telegram Token, API Key)
  • 用户权限已限制(tgbot_user
  • Systemd 服务已启用(systemctl status shlink-bot

5. 监控与日志 📊

  • Journald 日志可查看(journalctl -u shlink-bot
  • 日志轮转已配置(/etc/logrotate.d/shlink-bot
  • 健康检查脚本已添加到 crontab
  • 备份脚本已配置

6. 功能验证 ✅

  • 可以通过 Telegram 发送链接并获得短链
  • 短链可以正常跳转
  • API 统计功能正常(/stats 命令)
  • 服务重启后自动恢复

8.3 备份策略

创建 /opt/backups/backup.sh:

#!/bin/bash
BACKUP_DIR="/opt/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# 备份 Shlink 数据
tar -czf "${BACKUP_DIR}/shlink_data_${DATE}.tar.gz" /opt/shlink/data
# 备份 Bot 配置
tar -czf "${BACKUP_DIR}/bot_config_${DATE}.tar.gz" /opt/shlink_bot/.env /opt/shlink_bot/bot.py
# 保留最近 7 天的备份
find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +7 -delete
echo "Backup completed: ${DATE}"

添加到 crontab(每天凌晨 2 点):

0 2 * * * /opt/backups/backup.sh >> /var/log/backup.log 2>&1

🔧 第九章:故障排查与运维手册

9.1 常见问题诊断

问题 1: Bot 无响应

症状: 发送消息后无反应

诊断步骤:

# 1. 检查服务状态
sudo systemctl status shlink-bot
# 2. 查看最近日志
sudo journalctl -u shlink-bot -n 50
# 3. 检查进程
ps aux | grep bot.py
# 4. 测试网络连接
curl https://api.telegram.org/bot<TOKEN>/getMe

常见原因:

  • Telegram Token 错误
  • 网络被阻断
  • Python 依赖缺失

问题 2: API 返回 401

症状: Bot 提示"创建失败:API 返回错误: 401"

诊断:

# 验证 API Key
curl -X GET http://localhost:8080/rest/v3/short-urls \
-H "X-Api-Key: your_api_key"
# 检查 Shlink 日志
docker compose -f /opt/shlink/docker-compose.yml logs --tail=50

解决方案:

# 重新生成 API Key
docker exec -it shlink shlink api-key:generate
# 更新 .env 文件
nano /opt/shlink_bot/.env
# 重启 Bot
sudo systemctl restart shlink-bot

问题 3: 容器无法启动

症状: docker compose up 失败

诊断:

# 查看详细错误
docker compose up --no-start
docker compose logs
# 检查端口占用
sudo netstat -tulpn | grep 8080
# 检查磁盘空间
df -h

9.2 性能优化建议

1. 启用 Redis 缓存(可选)

修改 docker-compose.yml:

services:
redis:
image: redis:7-alpine
restart: always
shlink:
environment:
- CACHE_NAMESPACE=shlink
- REDIS_SERVERS=redis:6379
depends_on:
- redis

2. 数据库优化

# 定期清理访问记录(保留 30 天)
docker exec shlink shlink visit:delete --since=30d

9.3 安全事件响应

发现可疑活动

# 查看失败的 API 请求
docker compose logs shlink | grep "401\|403"
# 检查 Cloudflare 防火墙日志
# 访问 Cloudflare Dashboard → Security → Events
# 临时封禁 IP(在 Cloudflare WAF)
# Expression: ip.src eq 1.2.3.4
# Action: Block

紧急响应流程

# 1. 立即停止服务
sudo systemctl stop shlink-bot
docker compose -f /opt/shlink/docker-compose.yml down
# 2. 备份数据
cp -r /opt/shlink/data /opt/backups/emergency_backup_$(date +%s)
# 3. 检查日志
sudo journalctl -u shlink-bot --since "1 hour ago" > /tmp/incident.log
# 4. 更换 API Key
docker exec shlink shlink api-key:generate
# 吊销旧 Key
docker exec shlink shlink api-key:disable old_key_id
# 5. 重新启动
docker compose -f /opt/shlink/docker-compose.yml up -d
sudo systemctl start shlink-bot

🎓 总结

核心收获

这次旅程证明了 DevOps 精神的核心:不畏惧报错,不盲信配置,用数据说话

我们实现了什么:

  1. 安全层: Cloudflare 解决了 DDoS 防护、HTTPS 加密、WAF 过滤
  2. 隔离层: Docker 解决了环境一致性、依赖冲突、资源限制
  3. 业务层: Python 解决了 Telegram 集成、异步处理、自动化
  4. 调试层: TCPDump 和 SQLite 解决了最棘手的底层 Bug
  5. 运维层: Systemd 和权限管理保证了服务稳定性和安全性

技术栈总览

层级 技术 作用
边缘 Cloudflare DDoS 防护、WAF、SSL
网络 UFW/iptables 源站 IP 保护
容器 Docker Compose 环境隔离、一键部署
应用 Shlink 短链接核心服务
中间件 Python asyncio Telegram Bot 集成
守护 Systemd 进程管理、自动重启
监控 Journald + Cron 日志管理、健康检查
安全 最小权限原则 用户隔离、资源限制

下一步优化方向

  1. 高可用: 多实例部署 + 负载均衡
  2. 监控: 接入 Prometheus + Grafana
  3. 告警: 集成 Telegram 通知或邮件
  4. 备份: 自动备份到对象存储(S3/OSS)
  5. CI/CD: GitHub Actions 自动部署

现在,你的服务器上运行的不再是一段简陋的代码,而是一个经过实战检验的、具备企业级特性的自动化系统。

🚀 Keep building, stay secure!


📚 参考资源


文档修订历史:

  • v2.1 (2026-01-02): 补充监控章节、完善故障排查流程
  • v2.0 (2025-12-xx): 初始版本
All rights reserved
Except where otherwise noted, content on this page is copyrighted.