服务器运维
服务器安装
使用 Docker Compose 在一台公网服务器上安装和运营 Pabal 服务器的方法。
本文介绍如何把 Pabal 服务器安装到一台公网服务器上。用 Docker Compose 启动 Pabal 服务器 · PostgreSQL · Caddy(HTTPS)三个容器,所有不能丢失的数据都绑定挂载到主机目录。从上到下依次照做即可。
域名 pabal.me,服务器公网 IP 203.0.113.10(用于说明的地址——请换成实际 IP 来理解),源代码位置 /opt/pabal,数据位置 /srv/pabal,操作系统 Ubuntu 24.04 LTS。所需文件在仓库的 deploy/ 文件夹中。
整体概览
图示说明
- 三个区域:互联网上的用户 → 运营服务器上的三个容器 → 主机磁盘上的绑定目录。容器随时可以删除重建,需要保留的数据全部在
/srv/pabal中。 - 入口只有两个:应用通过 MTProto 端口直接连接服务器(自带加密,因此不需要 HTTPS),网站、文档和 Bot API 则经过 Caddy 的 443。Caddy 会拦截管理用的路径(
/admin、/health…),只转发其余请求。 - 红色虚线是运营方专用的路径。管理页面只在服务器的
127.0.0.1:8080上开放,只能通过 SSH 隧道进入。PostgreSQL 则根本没有对外开放的端口。 - 两个红色方框是最重要的数据。
pabal_server/keys中的 RSA 密钥在构建时内置于应用中,一旦丢失就必须重新分发所有应用;postgres_data/中保存着所有账号和对话。它们是备份的第一优先级。 - 灰色箭头表示写入存储:服务器写入密钥、照片、设置和日志,PostgreSQL 写入事件,Caddy 写入证书,各自写到自己的目录中。
准备工作
| 项目 | 内容 |
|---|---|
| 服务器 | Ubuntu 24.04 LTS,起步配置 2 vCPU · 4 GB RAM · 40 GB SSD。用户和消息增多时,请优先增加内存 |
| 公网 IPv4 | 固定 IP。应用在构建时以 IP 形式内置服务器地址,服务器也以 IP 形式告知应用 |
| 域名 | 一个可以修改 DNS 的域名(A 记录) |
| 注册验证码的发送方式 | 一个短信(Twilio · Solapi · Webhook)或电子邮件(SMTP)账号 |
| Mac | 一台用于为这台服务器构建应用(Pabal.app)的 Mac |
端口
| 端口 | 使用者 | 开放范围 | 说明 |
|---|---|---|---|
| 22/tcp | 运营方 | 建议只对运营方 IP 开放 | SSH |
| 80/tcp | Caddy | 所有人 | 签发证书 · 跳转到 HTTPS |
| 443/tcp, 443/udp | Caddy | 所有人 | 网站 · 文档 · Bot API(udp 用于 HTTP/3) |
| 8443/tcp | 服务器 | 所有人 | 应用连接(MTProto) |
| 8080/tcp | 服务器 | 不开放 | 管理页面 · 健康检查——仅限服务器的 127.0.0.1 |
| 5432/tcp | PostgreSQL | 不开放 | 仅限容器内部网络 |
443 由网站(HTTPS)使用。MTProto 既不是 HTTP 也不是 TLS,无法在同一个端口上分流接收。如果因为公司或学校的网络会拦截不常见的端口,而希望应用也通过 443 连接,请再申请一个 IP,把那个 IP 的 443 交给服务器(在 .env 中把 PUBLIC_IP 设为第二个 IP、设置 MTPROTO_PORT=443,并在 Compose 文件的端口前加上 IP 加以区分)。服务器会告诉应用“连接这个 IP 的这个端口”,因此容器内外的应用端口要保持一致。
1. 准备服务器
sudo apt update && sudo apt -y upgrade
timedatectl # 确认显示 "System clock synchronized: yes"
时间同步是必需的。MTProto 的消息编号由时间生成,服务器时钟不准时,应用会反复重新连接。如果显示 no,请执行 sudo timedatectl set-ntp true。
# 防火墙
sudo ufw allow OpenSSH # 如果可以:sudo ufw allow from <运营方 IP> to any port 22
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 8443/tcp
sudo ufw enable
# Docker(官方安装脚本)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER # 重新登录后无需 sudo 即可使用 docker
docker compose version # v2 及以上
Docker 开放的(ports:)端口不受 ufw 规则的约束。因此 Compose 文件只开放需要公开的端口(80 · 443 · 8443),管理端口只以 127.0.0.1:8080 开放,PostgreSQL 则不开放。修改 ports: 时请记住这一点。
2. 获取源代码
sudo mkdir -p /opt/pabal && sudo chown $USER: /opt/pabal
git clone <仓库地址> /opt/pabal
如果从开发电脑复制,不要把密钥、数据和日志传过去(rsync --exclude 'keys/' --exclude 'data/' --exclude 'logs/' --exclude '**/target/')。运营服务器的 RSA 密钥会在运营服务器首次启动时重新生成——如果在运营环境中使用开发用的密钥,那台开发电脑就成了能够解密运营流量的地方。
3. 数据目录(绑定挂载)
sudo mkdir -p /srv/pabal/{postgres_data,pabal_server/keys,pabal_server/data,pabal_server/logs,caddy/data,caddy/config,backups}
sudo chown -R 1000:1000 /srv/pabal/pabal_server # 服务器容器以 uid 1000 运行
sudo chown -R $USER: /srv/pabal/backups
sudo chmod 700 /srv/pabal/pabal_server/keys /srv/pabal/backups
| 目录 | 容器内路径 | 存放的内容 | 丢失后 |
|---|---|---|---|
pabal_server/keys | /app/keys | private.pem(服务器 RSA 私钥)、private.pem.pub | 需要重新构建并分发所有应用 |
pabal_server/data | /app/data | 照片(media/)、admin-token、operations.json(注册验证码、短信、SMTP 设置,包含机密值) | 照片、运营设置 |
pabal_server/logs | /app/logs | 服务器日志(每天压缩) | 只是记录 |
postgres_data | /var/lib/postgresql/data | 账号、对话、消息、登录、机器人、Webhook 设置等全部数据 | 整个服务 |
caddy/data, caddy/config | /data, /config | HTTPS 证书 | 会重新签发 |
backups | (仅限主机) | backup.sh 的输出 | — |
4. 配置文件 .env
cd /opt/pabal/deploy
cp .env.example .env
chmod 600 .env
openssl rand -base64 30 | tr -d '/+=' | cut -c1-32 # 把输出的值填入 POSTGRES_PASSWORD
nano .env
DOMAIN=pabal.me
PUBLIC_IP=203.0.113.10
POSTGRES_PASSWORD=(上面生成的值)
PABAL_HOME=/srv/pabal
PABAL_SOURCE=..
PABAL_NETWORK=ssemiya-net
MTPROTO_PORT=8443
HTTP_PORT=80
HTTPS_PORT=443
ADMIN_PORT=8080
ADMIN_TOKEN=
JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=100"
| 键 | 填写内容 | 备注 |
|---|---|---|
DOMAIN | 网站域名 | 应用生成的链接(pabal.me/用户名)、文档中的地址、链接预览也都使用这个地址 |
PUBLIC_IP | 服务器的公网 IPv4 | 如果是云服务器,请填写控制台中显示的公网 IP(不是服务器内部的私有 IP) |
POSTGRES_PASSWORD | 随机值 | 请在首次启动之前设定。PostgreSQL 只在首次创建数据目录时使用这个值 |
PABAL_HOME | /srv/pabal | 第 3 步中的目录 |
PABAL_SOURCE | .. | 构建服务器镜像所用的代码仓库。compose 文件在仓库的 deploy/ 中时为 ..,放在仓库外时填写源码路径 |
PABAL_NETWORK | ssemiya-net | 三个容器共同加入的 Docker 网络。需预先创建(第 6 步) |
MTPROTO_PORT | 8443 | 修改时,防火墙和应用构建也要一起修改 |
ADMIN_TOKEN | 留空 | 留空时,首次启动会在 pabal_server/data/admin-token 中生成。如果要自行设定,至少 16 个字符 |
JAVA_OPTS | 默认值 | 内存不足时请调高 -Xmx(最多约为服务器内存的一半) |
正式运营用的 Compose 文件会在关闭测试号码的状态下(TELEGRAM_TEST_NUMBERS=false)启动服务器。服务器读取的全部设置项,请参阅管理与设置——环境变量。
5. DNS
| 名称 | 类型 | 值 |
|---|---|---|
pabal.me | A | 203.0.113.10 |
www.pabal.me | A | 203.0.113.10 |
dig +short pabal.me # 应该输出 203.0.113.10(生效需要几分钟到几小时)
在 DNS 指向这台服务器之前启动也没关系。Caddy 会自动重试,直到拿到证书为止。
6. 构建并启动
cd /opt/pabal/deploy
docker network inspect ssemiya-net >/dev/null 2>&1 || docker network create ssemiya-net # 只需一次
docker compose up -d --build # 第一次需要几分钟(Maven 会下载依赖库)
docker compose ps # pabal-server、pabal-postgres、pabal-caddy 为 running (healthy)
docker compose logs -f pabal-server # 按 Ctrl+C 退出
首次启动时,日志中会出现如下几行。
WARN ServerKeys - Generated a new RSA key at /app/keys/private.pem (fingerprint -3898654385185406269). Clients must embed /app/keys/private.pem.pub
INFO TelegramServer - JDBC persistence active (jdbc:postgresql://pabal-postgres:5432/pabal)
INFO BotFather - BotFather is user 100000
INFO AdminToken - Created the admin token in /app/data/admin-token
INFO TelegramServer - Website: http://0.0.0.0:8080/ (published at https://pabal.me/)
- 第一行虽然是 WARN,但属于正常情况(只是告知首次生成了密钥,只会出现一次)。之后会显示
Loaded RSA key … (fingerprint …)。fingerprint 因服务器而异。 - 数据表会在服务器启动时自动创建。
7. 验证
# 在外部(自己的电脑)执行
curl -sI https://pabal.me/ | head -1 # HTTP/2 200
curl -s -o /dev/null -w '%{http_code}\n' https://pabal.me/docs/ # 200 — 本文档
curl -s -o /dev/null -w '%{http_code}\n' https://pabal.me/admin/ # 404 — 管理页面不对外公开
curl -s https://pabal.me/docs/server-key.pem | head -1 # -----BEGIN RSA PUBLIC KEY-----
nc -vz 203.0.113.10 8443 # succeeded — 应用端口
# 在服务器上执行
curl -s http://127.0.0.1:8080/health # {"status":"UP",…}
ls -l /srv/pabal/pabal_server/keys # private.pem(600), private.pem.pub
8. 管理页面与初始设置
# 在自己的电脑上执行(保持运行)
ssh -N -L 8080:127.0.0.1:8080 <用户>@203.0.113.10
# 令牌在服务器上查看
sudo cat /srv/pabal/pabal_server/data/admin-token
在浏览器中打开 http://localhost:8080/admin/,输入令牌。正式运营前一定要完成的设置(详见管理与设置):
- 把验证码发送方式(코드 전달 방식)设为短信或电子邮件。“管理页面”(관리 화면)方式需要运营方逐一告知验证码。
- 填入 SMS 或电子邮件(SMTP)(이메일 (SMTP))的设置值并保存 → 用测试发送(테스트 발송)确认确实能够收到。
- 确认测试号码(테스트 번호)已关闭。开启后,任何人都能用
+99966…号码登录。 - 如有需要,可以先关闭允许新注册(새 가입 허용)再开始,只让受邀的人先加入。
9. 让应用连接这台服务器
应用在构建时会内置服务器 IP · 端口 · 公钥。必须用这台服务器的公钥重新构建,应用才能连接这台服务器。在 Mac 上执行:
# 1. 这台服务器的公钥(是公钥而不是私钥——也可以从文档网站下载)
curl -s -o keys/production.pem.pub https://pabal.me/docs/server-key.pem
# 2. 让应用源代码指向这台服务器 + 应用品牌
python3 scripts/tdesktop/point_to_server.py ~/Developer/tdesktop \
--host 203.0.113.10 --port 8443 --key keys/production.pem.pub
python3 scripts/tdesktop/apply_branding.py ~/Developer/tdesktop
然后执行仓库中 docs/tdesktop-build-guide.md 的第 5 步(configure)和第 6 步(构建)。请把生成的 out/Debug/Pabal.app 以其他名称另行复制保存——用同一份源代码为另一台服务器重新构建时,它会被覆盖。
目前的构建是没有签名和公证的调试构建,收到的人第一次打开时需要右键 → 打开。调试构建会把数据存放在应用旁边的文件夹(tdata/)中,如果那里无法写入,就会使用与真正的 Telegram Desktop 相同的数据文件夹。请引导用户把应用放在用户文件夹中(例如 ~/Applications/Pabal/)。用于正式分发的发布构建(分离数据文件夹、Apple 签名和公证)是另外一项工作。
运维
状态与日志
cd /opt/pabal/deploy
docker compose ps
docker compose logs --since 1h pabal-server
tail -f /srv/pabal/pabal_server/logs/telegram-server.log
更新
cd /opt/pabal && git pull
cd deploy
./backup.sh # 先备份
docker compose up -d --build pabal-server # 用新镜像只替换服务器
docker compose logs --since 5m pabal-server | grep -E "Website|ERROR"
在服务器重启的几十秒内,应用会断开,然后无需重新登录就会自动重新连接。数据表的变更会在服务器启动时自动应用。绑定目录与镜像无关,因此 docker compose down 或删除镜像都不会删除数据——只要不删除 /srv/pabal 就行。
备份
./backup.sh # → /srv/pabal/backups/<日期-时间>/{pabal.dump, server-keys-data.tar.gz}
crontab -e # 每天 03:00:
# 0 3 * * * /opt/pabal/deploy/backup.sh >> /srv/pabal/backups/backup.log 2>&1
- 超过 14 天的备份会被自动删除(可以像
KEEP_DAYS=30 ./backup.sh这样修改)。 - 请同时复制到其他地方。放在同一块磁盘上的备份扛不住磁盘故障。
- 备份中包含 RSA 私钥以及短信、SMTP 的机密值。存放备份的地方也要像服务器一样严加保护。
恢复
cd /opt/pabal/deploy
B=/srv/pabal/backups/20260920-030000 # 要恢复的备份
docker compose stop pabal-server pabal-caddy
docker compose exec -T pabal-postgres dropdb -U pabal pabal
docker compose exec -T pabal-postgres createdb -U pabal pabal
docker compose exec -T pabal-postgres pg_restore -U pabal -d pabal --no-owner < $B/pabal.dump
sudo tar -C /srv/pabal/pabal_server -xzf $B/server-keys-data.tar.gz
sudo chown -R 1000:1000 /srv/pabal/pabal_server
docker compose up -d
迁移到新服务器时也一样:完成第 1~5 步后,不执行第 6 步,而是执行上面的恢复再启动。恢复同一个密钥后,就不需要重新构建应用(如果服务器 IP 变了,则需要重新构建)。
启动与停止
docker compose restart pabal-server # 只重启服务器
docker compose stop # 全部停止(数据保持不变)
docker compose up -d # 重新启动
即使服务器主机重启,容器也因为设置了 restart: unless-stopped 而会自动重新启动。
安全检查清单
.env权限为 600,POSTGRES_PASSWORD为随机值/srv/pabal/pabal_server/keys权限为 700,私钥除了备份之外不存在于其他任何地方- 防火墙:只开放 22(尽量只对运营方 IP)、80、443、8443
- 从外部访问
https://域名/admin/返回 404 - 管理页面:测试号码(테스트 번호)已关闭,验证码发送方式(코드 전달 방식)为短信/电子邮件,测试发送(테스트 발송)成功
- 没有开启
TELEGRAM_WEBHOOK_ALLOW_LOCAL(防止机器人 Webhook 访问内部网络) - SSH:关闭密码登录,只允许密钥登录
- 确认备份 cron 和外部复制,并演练一次恢复
- 开启服务器操作系统的自动安全更新(
unattended-upgrades)
故障排除
| 症状 | 原因 → 解决方法 |
|---|---|
| 应用一直停在“正在连接…” | ① 8443 被拦截 → nc -vz IP 8443,检查 ufw 和云安全组 ② 应用是用其他公钥构建的 → 用这台服务器的密钥重新执行第 9 步 ③ PUBLIC_IP 有误 → 修改 .env 后执行 docker compose up -d pabal-server |
| 一开始能连上,但很快断开 | 应用会转到服务器告知的地址(PUBLIC_IP:MTPROTO_PORT),而这个地址有误 → 检查 PUBLIC_IP |
| HTTPS 证书错误 | DNS 尚未指向这台服务器,或者 80 端口被拦截 → dig +short 域名、docker compose logs pabal-caddy |
server 不断重启,password authentication failed | 在创建数据之后修改了 POSTGRES_PASSWORD → 改回原来的值,或者在数据库中执行 ALTER USER pabal PASSWORD '…' |
AccessDeniedException: /app/keys/… | 绑定目录的所有者不对 → sudo chown -R 1000:1000 /srv/pabal/pabal_server |
OutOfMemoryError、运行变慢 | 调高 JAVA_OPTS 中的 -Xmx,然后执行 docker compose up -d pabal-server |
| 收不到注册验证码 | 查看管理页面注册验证码(가입 코드)标签页中的发送状态和失败原因 → 检查设置中的短信、SMTP 值,并进行测试发送 |
| 无法进入管理页面 | SSH 隧道是否开着;自己电脑上的 8080 是否被其他程序占用(改用 -L 18080:127.0.0.1:8080,并访问 localhost:18080) |
network ssemiya-net declared as external, but could not be found | 网络尚未创建 → docker network create ssemiya-net(若用其他名称,设置 .env 中的 PABAL_NETWORK) |
已知限制
- 单服务器架构。启动时会把所有账号、对话和消息加载到内存中。暂不支持拆分到多台服务器的水平扩展,数据增多时需要增加内存。
- 等待中的注册验证码、机器人尚未取走的更新保存在内存中,服务器重启后就会丢失。
- 注册验证码的请求有间隔和每日上限,但其他所有请求都还没有频率限制。刚对外公开后,请经常查看日志和仪表盘。
- 没有手机应用和推送通知。消息附件只支持照片。频道、超级群组和两步验证尚未支持。
- 授权密钥以明文形式保存在数据库中(服务器解密时需要)。请像保护 RSA 密钥一样保护数据库和备份。
文件
| 文件 | 作用 |
|---|---|
deploy/docker-compose.yml | 运营配置(pabal-server · pabal-postgres · pabal-caddy,绑定挂载) |
deploy/Caddyfile | HTTPS、拦截管理路径、转发网站、文档和 Bot API |
deploy/.env.example | 配置样例 → deploy/.env |
deploy/backup.sh | 数据库转储 + 密钥和数据归档,清理旧备份 |
Dockerfile | 服务器镜像(构建 → JRE 21,uid 1000) |