本文基于AI生成写作
作者: Gemini & Claude
关键词: DevOps, Cloudflare, Docker Security, RESTful API, Python Middleware, TCP/IP Debugging
📋 目录
- 第一章:顶层架构设计与技术哲学
- 第二章:第一道防线——Cloudflare 的安全接管
- 第三章:Gokapi 排错实录——TCP/IP 协议深潜
- 第四章:Shlink 容器化部署与数据持久化
- 第五章:Python 中间件开发
- 第六章:Linux 系统级安全加固
- 第七章:监控与日志管理
- 第八章:项目文件结构与部署清单
- 第九章:故障排查与运维手册
🏗️ 第一章:顶层架构设计与技术哲学
在动手敲代码之前,我们需要理解整个系统全貌。我们最终搭建的不再是一个简单的"软件",而是一个具有纵深防御体系(Defense in Depth)的分布式系统。
1.1 架构全景图
我们的流量链路经历了四个关键节点的流转:
用户请求
↓
[最外层] Cloudflare CDN + WAF
↓ (HTTPS)
[入口层] VPS 宿主机 (Python Middleware)
↓ (HTTP/localhost)
[核心层] Docker 容器 (Shlink)
↓
[存储层] SQLite Volume关键节点解析:
最外层 (The Edge): Cloudflare
- 职责: 拦截 DDoS 攻击、清洗恶意流量、强制 HTTPS 加密
- 技术: Anycast 网络、WAF 规则、SSL/TLS 终止
入口层 (Ingress): VPS 宿主机
- 职责: 运行 Telegram Bot,作为用户与核心服务的"胶水"
- 技术: Python asyncio、systemd 守护进程
核心层 (Core): Docker 容器
- 职责: Shlink 服务运行环境,隔离依赖
- 技术: Docker Compose、网络命名空间、资源限制
控制层 (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: AutoStep 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 证书
- 在 Cloudflare Dashboard → SSL/TLS → Origin Server
- 点击 "Create Certificate"
- 选择证书有效期(推荐 15 年)
- 复制生成的证书和私钥
在 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/")
动作: Block2.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 教训总结
- 日志不是唯一真相: 当应用层日志失效时,网络层抓包是最后的事实来源
- 时间同步的重要性: 分布式系统中,时钟偏移(Clock Skew)是经典问题
- 数据库是 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.gz8.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 精神的核心:不畏惧报错,不盲信配置,用数据说话。
我们实现了什么:
- ✅ 安全层: Cloudflare 解决了 DDoS 防护、HTTPS 加密、WAF 过滤
- ✅ 隔离层: Docker 解决了环境一致性、依赖冲突、资源限制
- ✅ 业务层: Python 解决了 Telegram 集成、异步处理、自动化
- ✅ 调试层: TCPDump 和 SQLite 解决了最棘手的底层 Bug
- ✅ 运维层: Systemd 和权限管理保证了服务稳定性和安全性
技术栈总览
| 层级 | 技术 | 作用 |
|---|---|---|
| 边缘 | Cloudflare | DDoS 防护、WAF、SSL |
| 网络 | UFW/iptables | 源站 IP 保护 |
| 容器 | Docker Compose | 环境隔离、一键部署 |
| 应用 | Shlink | 短链接核心服务 |
| 中间件 | Python asyncio | Telegram Bot 集成 |
| 守护 | Systemd | 进程管理、自动重启 |
| 监控 | Journald + Cron | 日志管理、健康检查 |
| 安全 | 最小权限原则 | 用户隔离、资源限制 |
下一步优化方向
- 高可用: 多实例部署 + 负载均衡
- 监控: 接入 Prometheus + Grafana
- 告警: 集成 Telegram 通知或邮件
- 备份: 自动备份到对象存储(S3/OSS)
- CI/CD: GitHub Actions 自动部署
现在,你的服务器上运行的不再是一段简陋的代码,而是一个经过实战检验的、具备企业级特性的自动化系统。
🚀 Keep building, stay secure!
📚 参考资源
文档修订历史:
- v2.1 (2026-01-02): 补充监控章节、完善故障排查流程
- v2.0 (2025-12-xx): 初始版本