本章目录
第 10 章
部署上线
第 1 章说过:机器人就是你电脑上的一个进程,关掉就没了。这一章把它挪到一台不关机的机器上——开机自启、崩了自动重来、出事能查。
第 1 章那句话,现在来兑现
开篇就说过:机器人是跑在你电脑上的一个进程,你关掉电脑它就死了。前九章一直是这样——终端开着它就活着,Ctrl+C 它就没了。
这一章把它挪到一台不关机的机器上,并且做到三件事:
| 开机自启 | 服务器重启后它自己回来 |
| 崩了自动重来 | 不用你半夜爬起来 |
| 出事能查 | 日志留着,而且能按时间翻 |
买什么样的服务器
一个 Telegram bot 消耗的资源少得可怜。最便宜的那档就够:
| 配置 | 1 核 / 512MB 内存 / 10GB 硬盘。签到 bot 实际占用大概 60MB |
| 机房位置 | 别选国内 —— 国内机器连不上 Telegram,还得给服务器配代理,自找麻烦。日本、新加坡、香港都行 |
| 系统 | Ubuntu LTS。这一章的命令按它写 |
服务器直连 Telegram,.env 里的 PROXY_URL 那行可以删掉了。第 2 章配代理的麻烦,到这里自动消失。
先把 SSH 弄安全
服务器一开机就会被扫。别问为什么,公网上全是自动扫描的机器人,密码弱一点几个小时就被爆开。
第一件事是换成密钥登录。在你自己电脑上生成一对:
ssh-keygen -t ed25519 -f ~/.ssh/mybot -C "mybot"ssh-copy-id -i ~/.ssh/mybot.pub root@你的服务器IP
验证能用密钥登进去之后,再关掉密码登录:
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'PasswordAuthentication noKbdInteractiveAuthentication noPermitRootLogin prohibit-passwordEOFsshd -t && systemctl reload ssh
先关密码、后配密钥的话,你会把自己锁在门外,只能去服务商后台开网页控制台救。
正确顺序:装公钥 → 开个新终端验证密钥能登 → 那个窗口别关 → 再改配置。万一改错了,还有一个活着的连接能改回来。
再顺手把防火墙开上,只放需要的端口:
apt install -y ufwufw allow 22/tcpufw --force enableufw status
bot 用轮询模式,不需要开任何入站端口——它只往外连。所以除了 22,什么都不用开。
把代码搬上去
最省事的办法是 rsync,不用装 git、不用建仓库:
#!/usr/bin/env bashset -euo pipefailHOST="root@你的服务器IP"REMOTE="/opt/mybot"rsync -avz --delete \ --exclude '.venv/' \ --exclude '__pycache__/' \ --exclude 'data.db' \ # 别把本地测试数据推上去覆盖线上! --exclude '.env' \ # 线上的 .env 单独维护 ./ "$HOST:$REMOTE/"ssh "$HOST" "systemctl restart mybot && systemctl is-active mybot"
--delete 会让远端跟本地完全一致。不排除 data.db 的话,一次部署就能把线上所有用户数据换成你本地的测试数据。.env 同理——线上的 token 和配置跟本地不一样,覆盖了 bot 就起不来。
服务器上准备环境(只做一次):
apt install -y python3-venvmkdir -p /opt/mybot && cd /opt/mybotpython3 -m venv .venv.venv/bin/pip install aiogram python-dotenv# 线上的 .env,注意没有 PROXY_URLcat > .env <<'EOF'BOT_TOKEN=你的tokenEOFchmod 600 .env
交给 systemd 管着
这是整章的核心。写一个服务定义文件,剩下的事系统帮你干:
[Unit]Description=My Telegram BotAfter=network-online.targetWants=network-online.target[Service]Type=simpleWorkingDirectory=/opt/mybotExecStart=/opt/mybot/.venv/bin/python bot.py# 崩了就重启,但别疯狂重启Restart=alwaysRestartSec=5StartLimitIntervalSec=300StartLimitBurst=5# 日志直接进 journalStandardOutput=journalStandardError=journalSyslogIdentifier=mybot# 收紧权限:只能写自己的目录User=mybotNoNewPrivileges=truePrivateTmp=trueProtectSystem=strictProtectHome=trueReadWritePaths=/opt/mybot[Install]WantedBy=multi-user.target
| 这几行的作用 | |
|---|---|
Restart=always | 进程没了就拉起来,不管是崩的还是被 kill 的 |
StartLimitBurst=5 | 5 分钟内重启超过 5 次就停手。没有这个,配置写错时它会每 5 秒重启一次,日志刷满硬盘 |
After=network-online | 开机时等网络就绪再启动,否则第一次连 Telegram 会失败 |
ProtectSystem=strict | 整个文件系统只读,只有 ReadWritePaths 列出的能写。万一代码有漏洞,损失被圈在这一个目录里 |
User=mybot | 别用 root 跑。先 useradd -r -s /usr/sbin/nologin mybot 建这个用户,再把目录给它 |
useradd -r -s /usr/sbin/nologin mybotchown -R mybot:mybot /opt/mybotsystemctl daemon-reloadsystemctl enable --now mybotsystemctl status mybot
看到 active (running) 就成了。现在关掉终端、重启服务器,它都会自己回来。
出事了怎么查
日志进了 journal,用 journalctl 翻:
journalctl -u mybot -f # 实时盯着(相当于 tail -f)journalctl -u mybot -n 100 --no-pager # 最近 100 行journalctl -u mybot --since "1 hour ago" # 最近一小时journalctl -u mybot -p err # 只看错误journalctl -u mybot --since today | grep -i traceback
第 7 章配的那个带时间戳的日志格式,在这里就有用了 —— 用户说「刚才没反应」,你能直接按时间翻到那一段。
journal 默认能吃掉不少空间。10GB 的小机器上,在 /etc/systemd/journald.conf 里加一句 SystemMaxUse=500M,然后 systemctl restart systemd-journald。
怎么知道它还活着
Restart=always 管得了崩溃,管不了「进程还在但已经不干活了」 —— 比如卡在某个死循环里、或者第 8 章说的定时任务悄悄死了。
最省事的自检办法:让 bot 每小时给自己发个心跳,你隔一阵瞄一眼。
ADMIN_ID = 你的用户ID # 找 @userinfobot 问它async def heartbeat(bot: Bot) -> None: """每小时报一次平安,顺便带上关键数字。""" while True: await asyncio.sleep(3600) try: n = storage.count_today_checkins() await bot.send_message(ADMIN_ID, f"还活着 · 今日签到 {n}") except Exception: log.exception("心跳失败")
它的价值不在「告诉你活着」,在于停了你就会注意到。比装一套监控系统实在得多。
再加一个:把第 5 章的备份挂上 cron。
# 每天凌晨 3 点备份,只留最近 7 份0 3 * * * cd /opt/mybot && .venv/bin/python backup.py && ls -t backup-*.db | tail -n +8 | xargs -r rm
硬盘坏了或者服务器被删,备份跟着一起没。真在意数据的话,加一条 rsync 定期拉回你自己电脑,或者传到对象存储。
至少每个月手动恢复一次试试 —— 没验证过的备份不算备份。
更新代码的正确姿势
./deploy.sh
就一句。但有几件事要留神:
| 重启会打断正在进行的对话 | 用 MemoryStorage 的话(第 5 章),正在兑换的人会卡住。挑没人用的时候推 |
| 先看日志再走开 | journalctl -u mybot -f 盯十几秒,确认它真的起来了。systemctl restart 返回成功不代表程序没崩——它可能起来又立刻挂了 |
| 改了表结构要格外小心 | 第 5 章那个坑:本地删库重跑没事,线上老库直接炸。推之前把备份下载一份到本地 |
上线检查表
推给真实用户之前,把这几条过一遍:
| 检查 | 怎么验 | |
|---|---|---|
| 必查 | token 没写进代码 | grep -rn "[0-9]\{8,\}:AA" . 应该什么都搜不到 |
| 必查 | .env 在 .gitignore 里 | 而且权限是 600 |
| 必查 | 重启后能自己回来 | reboot,等两分钟,发一条 /start |
| 必查 | 崩了能自己起来 | kill -9 掉进程,5 秒后再发消息,应该正常 |
| 必查 | 有备份,而且能恢复 | 跑一次备份,再用备份文件开一次 sqlite3 |
| 建议 | 全局错误处理在(第 7 章) | 故意触发一个异常,看进程有没有死 |
| 建议 | 日志有时间戳 | journalctl -u mybot -n 5 |
| 建议 | 心跳能收到 | 把间隔临时改成 60 秒试一次 |
| 建议 | 磁盘还有空间 | df -h,日志和备份都会长 |
kill -9 试一次。很多人配完 systemd 从来没验证过,等真崩的那天才发现配置写错了。到这儿你会了什么
回头看第 1 章那张五步图 —— 现在五步都走完了:
更重要的是中间那几章:你知道了用户点两次会怎样、群里人多了消息为什么会丢、支付回调为什么会重复。这些东西换个框架、换个平台依然成立 —— 它们不是 aiogram 的知识,是写联网服务的知识。
这一章的要点
- 先验证密钥能登录,再关密码登录,而且留一个活着的终端窗口
- 轮询模式不需要开任何入站端口,防火墙只留 22
- systemd 三件套:
Restart=always+StartLimitBurst防疯狂重启 +ProtectSystem=strict - 部署脚本必须 exclude 掉
data.db和.env,否则一次部署冲掉所有线上数据 systemctl restart成功不代表程序没崩,要看日志- 心跳消息比监控系统实用:它停了你就会注意到
- 没恢复验证过的备份不算备份
故意把 .env 里的 token 改错一位,然后 systemctl restart mybot。
观察:systemctl status 显示什么?journalctl 里是什么错?5 分钟内它重启了几次,然后停在哪个状态?
把这个过程走一遍,以后线上真出事时你就知道该看哪儿了。
服务器配置最容易卡在细节上。哪一步出问题了,把报错贴进来。留不留联系方式都行。