开发者文档
简体中文

服务器运维

服务器安装

使用 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/ 文件夹中。

整体概览

互联网 docker compose · ssemiya-net /srv/pabal (绑定挂载) Pabal 应用Pabal.app 浏览器 · 机器人网站 · Bot API 运营方SSH pabal-server MTProto :8443 网页 · Bot API · 管理 :8080 uid 1000 · JRE 21 Webhook → 机器人服务器 pabal-caddy:80 · :443 自动证书 pabal-postgres 16仅限内网 · 无端口 pabal_server/keys · RSA 密钥 pabal_server/data · 照片·设置 pabal_server/logs postgres_data/ · 所有对话 caddy/ · 证书 backups/ · backup.sh MTProto :8443 (自带加密) HTTPS :443 SSH 隧道 → 127.0.0.1:8080/admin/
一台服务器、三个容器,需要保留的一切都在 /srv/pabal 中

图示说明

  • 三个区域:互联网上的用户 → 运营服务器上的三个容器 → 主机磁盘上的绑定目录。容器随时可以删除重建,需要保留的数据全部在 /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/tcpCaddy所有人签发证书 · 跳转到 HTTPS
443/tcp, 443/udpCaddy所有人网站 · 文档 · Bot API(udp 用于 HTTP/3)
8443/tcp服务器所有人应用连接(MTProto)
8080/tcp服务器不开放管理页面 · 健康检查——仅限服务器的 127.0.0.1
5432/tcpPostgreSQL不开放仅限容器内部网络
为什么应用端口不是 443

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 与 ufw

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/keysprivate.pem(服务器 RSA 私钥)、private.pem.pub需要重新构建并分发所有应用
pabal_server/data/app/data照片(media/)、admin-tokenoperations.json(注册验证码、短信、SMTP 设置,包含机密值)照片、运营设置
pabal_server/logs/app/logs服务器日志(每天压缩)只是记录
postgres_data/var/lib/postgresql/data账号、对话、消息、登录、机器人、Webhook 设置等全部数据整个服务
caddy/data, caddy/config/data, /configHTTPS 证书会重新签发
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_NETWORKssemiya-net三个容器共同加入的 Docker 网络。需预先创建(第 6 步)
MTPROTO_PORT8443修改时,防火墙和应用构建也要一起修改
ADMIN_TOKEN留空留空时,首次启动会在 pabal_server/data/admin-token 中生成。如果要自行设定,至少 16 个字符
JAVA_OPTS默认值内存不足时请调高 -Xmx(最多约为服务器内存的一半)

正式运营用的 Compose 文件会在关闭测试号码的状态下TELEGRAM_TEST_NUMBERS=false)启动服务器。服务器读取的全部设置项,请参阅管理与设置——环境变量

5. DNS

名称类型
pabal.meA203.0.113.10
www.pabal.meA203.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/,输入令牌。正式运营前一定要完成的设置(详见管理与设置):

  1. 验证码发送方式코드 전달 방식)设为短信或电子邮件。“管理页面”(관리 화면)方式需要运营方逐一告知验证码。
  2. 填入 SMS电子邮件(SMTP)이메일 (SMTP))的设置值并保存 → 用测试发送테스트 발송)确认确实能够收到。
  3. 确认测试号码테스트 번호)已关闭。开启后,任何人都能用 +99966… 号码登录。
  4. 如有需要,可以先关闭允许新注册새 가입 허용)再开始,只让受邀的人先加入。

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/CaddyfileHTTPS、拦截管理路径、转发网站、文档和 Bot API
deploy/.env.example配置样例 → deploy/.env
deploy/backup.sh数据库转储 + 密钥和数据归档,清理旧备份
Dockerfile服务器镜像(构建 → JRE 21,uid 1000)