サーバー運用
サーバーのインストール
Pabal サーバーを、インターネットに公開された 1 台のサーバーに Docker Compose でインストールして運用する方法。
Pabal サーバーを、インターネットに公開された 1 台のサーバーにインストールする方法です。Docker Compose で Pabal サーバー · PostgreSQL · Caddy(HTTPS)の 3 つのコンテナを起動し、消えてはならないデータはすべてホストのディレクトリにバインドします。上から順に進めてください。
ドメイン pabal.me、サーバーのグローバル IP 203.0.113.10(説明用のアドレス — 実際の IP に読み替えてください)、ソースの場所 /opt/pabal、データの場所 /srv/pabal、OS は Ubuntu 24.04 LTS。必要なファイルは、リポジトリの deploy/ フォルダーにあります。
全体像
図の説明
- 3 つの領域:インターネット上のユーザー → 本番サーバーの 3 つのコンテナ → ホストのディスク上のバインドディレクトリ。コンテナはいつでも削除して作り直せ、残すべきものはすべて
/srv/pabalにあります。 - 入口は 2 つだけです:アプリは MTProto ポートでサーバーに直接接続し(独自に暗号化するため HTTPS は不要)、Web サイト・ドキュメント・Bot API は Caddy の 443 を経由します。Caddy は管理用のパス(
/admin、/health…)をブロックし、それ以外だけを転送します。 - 赤い点線は運営者だけの経路です。管理画面はサーバーの
127.0.0.1:8080でしか開いていないため、SSH トンネル経由でのみ入れます。PostgreSQL には、外部に公開するポートがまったくありません。 - 赤い 2 つの枠が最も重要なデータです。
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 を変更できるドメイン 1 つ(A レコード) |
| 登録コードの配信手段 | SMS(Twilio · Solapi · Webhook)またはメール(SMTP)のアカウント 1 つ |
| Mac | アプリ(Pabal.app)をこのサーバー用にビルドするための Mac |
ポート
| ポート | 利用者 | 開放 | 説明 |
|---|---|---|---|
| 22/tcp | 運営者 | 運営者の IP のみを推奨 | SSH |
| 80/tcp | Caddy | すべて | 証明書の発行 · HTTPS へのリダイレクト |
| 443/tcp, 443/udp | Caddy | すべて | Web サイト · ドキュメント · Bot API(udp は HTTP/3) |
| 8443/tcp | サーバー | すべて | アプリの接続(MTProto) |
| 8080/tcp | サーバー | 開放しない | 管理画面 · ヘルスチェック — サーバーの 127.0.0.1 のみ |
| 5432/tcp | PostgreSQL | 開放しない | コンテナの内部ネットワークのみ |
443 は Web サイト(HTTPS)が使います。MTProto は HTTP でも TLS でもないため、1 つのポートで振り分けて受けることはできません。珍しいポートをブロックする会社や学校のネットワークのために、アプリも 443 で受けたい場合は、IP をもう 1 つ取得し、その IP の 443 をサーバーに割り当ててください(.env で PUBLIC_IP を 2 つ目の 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 <リポジトリの URL> /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(登録コード・SMS・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 | Web サイトのドメイン | アプリが作るリンク(pabal.me/ユーザー名)、ドキュメントのアドレス、リンクプレビューもこのアドレス |
PUBLIC_IP | サーバーのグローバル IPv4 | クラウドなら、コンソールに表示されるグローバル IP(サーバー内のプライベート IP ではない) |
POSTGRES_PASSWORD | ランダムな値 | 初回起動の前に決めてください。PostgreSQL は、データディレクトリを最初に作るときだけこの値を使います |
PABAL_HOME | /srv/pabal | 手順 3 のディレクトリ |
PABAL_SOURCE | .. | サーバーイメージをビルドするリポジトリ。compose ファイルがリポジトリの deploy/ にあれば ..、外に置くならソースのパス |
PABAL_NETWORK | ssemiya-net | 3 つのコンテナが参加する 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 と表示されれば OK(反映には数分〜数時間)
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/)
- 1 行目は 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 またはメールにする。「管理画面」(관리 화면)方式では、運営者がコードを 1 つずつ伝える必要があります。
- 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 秘密鍵と SMS・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 - 管理画面:テスト番号(테스트 번호)が無効、コードの配信方式(코드 전달 방식)が SMS/メール、テスト送信(테스트 발송)が成功
TELEGRAM_WEBHOOK_ALLOW_LOCALを有効にしていない(ボットの Webhook が内部ネットワークへ送られないように)- SSH:パスワードログインを無効にし、鍵認証のみ
- バックアップの cron と外部へのコピーを確認し、復元を一度練習しておく
- サーバー OS のセキュリティアップデートの自動適用(
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 を変更した → 元の値に戻すか、DB 内で 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 |
| 登録コードが届かない | 管理画面の登録コード(가입 코드)タブで、送信状態・失敗の理由を確認 → 設定(설정)の SMS・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) |
既知の制限
- サーバー 1 台の構成です。起動時に、すべてのアカウント・会話・メッセージをメモリに読み込みます。複数台に分散する水平スケーリングはまだできず、データが増えたらメモリを増やす必要があります。
- 待機中の登録コードや、ボットが未取得のアップデートはメモリ上にあるため、サーバーを再起動すると消えます。
- 登録コードの要求には間隔と 1 日の上限がありますが、それ以外のリクエスト全体に対するレート制限はまだありません。公開直後は、ログとダッシュボード(대시보드)を頻繁に確認してください。
- スマートフォンアプリとプッシュ通知はありません。メッセージの添付は写真のみです。チャンネル・スーパーグループ、2 段階認証はまだです。
- 認証キーは DB に平文で保存されています(サーバーが復号するために必要)。DB とバックアップを、RSA 鍵と同じくらい厳重に守ってください。
ファイル
| ファイル | 役割 |
|---|---|
deploy/docker-compose.yml | 本番構成(pabal-server · pabal-postgres · pabal-caddy、バインドマウント) |
deploy/Caddyfile | HTTPS、管理用パスのブロック、サイト・ドキュメント・Bot API の転送 |
deploy/.env.example | 設定のひな形 → deploy/.env |
deploy/backup.sh | DB のダンプ + 鍵・データの保存、古いバックアップの整理 |
Dockerfile | サーバーのイメージ(ビルド → JRE 21、uid 1000) |