開発者ドキュメント
日本語

サーバー運用

サーバーのインストール

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/ フォルダーにあります。

全体像

インターネット docker compose · ssemiya-net /srv/pabal (バインドマウント) Pabal アプリPabal.app ブラウザー · ボットサイト · Bot API 運営者SSH pabal-server MTProto :8443 Web · Bot API · 管理 :8080 uid 1000 · JRE 21 Webhook → ボット (HTTPS) 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/
サーバー 1 台、コンテナ 3 つ、残すべきものはすべて /srv/pabal に

図の説明

  • 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アプリをこのサーバー用にビルドするための Mac(Windows アプリも作るなら Windows PC も 1 台)
Firebase プロジェクト(任意)Android アプリにプッシュ通知を送る場合。Google アカウントで無料で作れます(手順 10)

ポート

ポート利用者開放説明
22/tcp運営者運営者の IP のみを推奨SSH
80/tcpCaddyすべて証明書の発行 · HTTPS へのリダイレクト
443/tcp, 443/udpCaddyすべてWeb サイト · ドキュメント · Bot API(udp は HTTP/3)
8443/tcpサーバーすべてアプリの接続(MTProto)
8080/tcpサーバー開放しない管理画面 · ヘルスチェック — サーバーの 127.0.0.1 のみ
5432/tcpPostgreSQL開放しないコンテナの内部ネットワークのみ
アプリのポートが 443 ではない理由

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 と 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 <リポジトリの 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/keysprivate.pem(サーバーの RSA 秘密鍵)、private.pem.pub、(任意)firebase-service-account.json(Android プッシュ用の Firebase 秘密鍵)すべてのアプリを再ビルド・再配布(Firebase の鍵はコンソールで新しく取得すれば済む)
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、/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"
FCM_CREDENTIALS=
FCM_PROJECT_ID=
PUSH_PREVIEWS=true
値設定内容備考
DOMAINWeb サイトのドメインアプリが作るリンク(pabal.me/ユーザー名)、ドキュメントのアドレス、リンクプレビューもこのアドレス
PUBLIC_IPサーバーのグローバル IPv4クラウドなら、コンソールに表示されるグローバル IP(サーバー内のプライベート IP ではない)
POSTGRES_PASSWORDランダムな値初回起動の前に決めてください。PostgreSQL は、データディレクトリを最初に作るときだけこの値を使います
PABAL_HOME/srv/pabal手順 3 のディレクトリ
PABAL_SOURCE..サーバーイメージをビルドするリポジトリ。Compose ファイルがリポジトリの deploy/ にあれば ..、外に置くならソースのパス
PABAL_NETWORKssemiya-net3 つのコンテナが参加する Docker ネットワーク。事前に作成しておきます(手順 6)
MTPROTO_PORT8443変更する場合は、ファイアウォールとアプリのビルドも合わせて変更
ADMIN_TOKEN空のまま空にすると、初回起動時に pabal_server/data/admin-token に作られる。自分で決める場合は 16 文字以上
JAVA_OPTS既定値メモリが足りなければ -Xmx を上げてください(サーバーのメモリの半分程度まで)
FCM_CREDENTIALS空のままAndroid のプッシュ通知。有効にするときは /app/keys/firebase-service-account.json(手順 10)。空ならプッシュなしで動作する
FCM_PROJECT_ID空のまま鍵ファイルの project_id ではない Firebase プロジェクトに送る場合のみ
PUSH_PREVIEWStruefalse にすると、通知にメッセージの内容が出ず、送信者だけが表示される

本番用の 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 と表示されれば 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/ を開き、トークンを入力します。管理画面の表示は韓国語なので、以下では項目名の後ろに韓国語の表記を添えています。運用前に必ず行う設定(詳しくは管理と設定):

  1. コードの配信方式(코드 전달 방식)を SMS またはメールにする。「管理画面」(관리 화면)方式では、運営者がコードを 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 の署名・公証)は別の作業です。

Android・iPhone・Windows アプリについても、リポジトリの docs/mobile-apps-guide.md に同じ方法(サーバーのアドレスと公開鍵を組み込むスクリプト → ビルド)が載っています。Android アプリにプッシュ通知を入れるには、ビルドする前に手順 10 の Firebase アプリ設定が必要です。

10. Android のプッシュ通知(任意)

Android は、画面から外れたアプリをまもなくスリープさせます。プッシュを有効にしないと、メッセージが表示され着信が鳴るのはアプリを開いている間だけです。プッシュは Google の Firebase Cloud Messaging(FCM)で送り(無料)、通知の内容(送信者の名前、メッセージのプレビュー)はサーバーがその端末だけが持つ鍵で暗号化するため、Google には読めません。

Firebase プロジェクト(運営者のもの) ビルド · インストール スマートフォン · Google google-services.json アプリに組み込む設定 APK に同梱 · 秘密ではない サービス アカウント鍵 firebase-service-account.json 秘密 · サーバーにのみ Android アプリのビルド apply_branding.py → APK pabal-server keys/ · chmod 600 FCM_CREDENTIALS 端末の鍵で通知を暗号化 スマートフォンの Pabal アプリ 初回起動時にこの端末を登録 Google FCM 暗号文のみ中継 · 内容は読めない ① 端末を登録 ② 暗号化通知 ③ 起こす → 通知
2 つのファイルはそれぞれ別の場所へ、通知は暗号文で

図の説明

  • 3 つの領域:運営者が作る Firebase プロジェクト → アプリをビルドしサーバーを動かす場所 → スマートフォンと Google。1 つの Firebase プロジェクトから 2 つのファイルを取得します。
  • 灰色の矢印は一度だけ行う準備です。緑の枠の google-services.json はアプリに組み込まれ、すべての APK に含まれるので秘密ではありません。赤の枠のサービス アカウント鍵は、サーバーの keys/ にだけ置く秘密です。2 つのファイルは同じプロジェクトから取得する必要があります。
  • 青の矢印は通知が届く経路です:① アプリを初めて開くと、端末が FCM トークンとその端末だけが持つ鍵をサーバーに登録し、② 新しいメッセージや着信があると、サーバーが通知をその鍵で暗号化して FCM に渡し、③ FCM が端末を起こすと、アプリが復号して通知を表示し、サーバーに再接続して見逃した分を受け取ります。
  • Google が見るものは、デバイストークンや送信時刻・優先度といった配信情報だけです。送信者の名前とメッセージは暗号文なので読めません。
  • アプリを見ている端末にはプッシュを送りません — すでに接続を通じて受け取っているからです。通知をオフにした会話のプッシュは急ぎでない優先度で送り、アプリ自身が除外します。
Firebase プロジェクトはアプリと対になっています

サーバーの鍵でプッシュを送れるのは、同じ Firebase プロジェクトでビルドされたアプリだけです。ほかの人がビルドしたアプリ(たとえば Pabal.me から入手したアプリ)はその人の Firebase プロジェクトに結び付いており、そもそもその人のサーバーのアドレスと公開鍵でビルドされています。ご自身のサーバー用のアプリはご自身でビルドし、Firebase プロジェクトもご自身で作成してください。

Firebase プロジェクトを作る

  1. Firebase コンソールでプロジェクトを追加します。名前は何でもかまいません。Google アナリティクスは不要です。
  2. プロジェクトの設定 → 全般 → マイアプリ → アプリを追加 → Android。パッケージ名に me.pabal.android(アプリのビルドスクリプトがアプリに付ける名前)を入力します。証明書のフィンガープリント(SHA-1)はプッシュには不要です。
  3. そのアプリの google-services.json をダウンロードします。アプリに組み込む設定で、秘密ではありません。
  4. プロジェクトの設定 → サービス アカウント → 新しい秘密鍵の生成で JSON ファイルを取得し、名前を firebase-service-account.json に変えます。このファイルは秘密です — 内容をリポジトリ・チャット・.env に入れないでください。
  5. プロジェクトの設定 → Cloud Messaging で、Firebase Cloud Messaging API(V1)が有効になっているか確認します(新しいプロジェクトでは通常有効です)。

アプリにアプリ設定を組み込む

Android アプリをビルドする前に、google-services.json をビルドスクリプトが探す場所に置きます。スクリプトはファイルを検査してから(me.pabal.android が登録されたプロジェクトか、秘密鍵を誤って渡していないか)アプリのソースに組み込みます。

mkdir -p ~/.pabal/android
cp ~/Downloads/google-services.json ~/.pabal/android/     # 別の場所に置く場合は apply_branding.py に --google-services <パス>
python3 scripts/android/point_to_server.py <Android のソース> \
    --host 203.0.113.10 --port 8443 --key keys/production.pem.pub
python3 scripts/android/apply_branding.py <Android のソース>
# 最後に "Firebase: project <プロジェクト ID>" と表示されるはずです。pabal-placeholder と表示されたら、プッシュのないアプリになります

あとはリポジトリの docs/mobile-apps-guide.md の「Android アプリ」の節に従ってビルドします。プッシュが入る前のアプリを使っていた端末も、新しくビルドしたアプリにアップデートしないと通知は届きません。

サーバーに秘密鍵を置く

sudo cp firebase-service-account.json /srv/pabal/pabal_server/keys/
sudo chown 1000:1000 /srv/pabal/pabal_server/keys/firebase-service-account.json    # サーバーのコンテナのユーザー
sudo chmod 600 /srv/pabal/pabal_server/keys/firebase-service-account.json

.env に 1 行追加します。コンテナの中では、pabal_server/keys は /app/keys です。

FCM_CREDENTIALS=/app/keys/firebase-service-account.json
docker compose up -d pabal-server
docker compose logs pabal-server | grep -i push
INFO  TelegramServer - Push notifications on: Firebase project <プロジェクト ID>, 0 device registrations, message text included (encrypted per device)
  • この行が表示されれば有効です。ファイルを読めない、または形式が正しくない場合は、Push notifications are off: … に理由が出ます(ファイルの内容はログに出ません)。
  • PUSH_PREVIEWS=false なら行末が message text left out になり、通知にはメッセージの内容が出ず、送信者だけが表示されます。
  • コンテナから oauth2.googleapis.com、fcm.googleapis.com(443)への外向きの接続が開いている必要があります。

確認

  1. 新しくビルドしたアプリをスマートフォンにインストール(またはアップデート)し、一度開きます。サーバーのログに Push device registered: user … type 2 token … (push key 256 bytes) が出ます。トークンはフィンガープリントとしてだけ記録されます。
  2. 最近使ったアプリの一覧からアプリをスワイプして閉じます。設定の「強制停止」は使わないでください — Android は、アプリを自分で再び開くまでプッシュを届けません。
  3. 別のアカウントからメッセージを送ると、ログに Push MESSAGE_TEXT to user …: sent が出て、スマートフォンに通知が表示されます。
  • プライバシーポリシー:管理画面(관리 화면)→ 設定(설정)→ 運営者情報(운영자 정보)の処理の委託(처리 위탁)と国外移転(국외 이전)に、Google LLC(Firebase Cloud Messaging、米国)を記入してください。ポリシーの本文には、FCM を経由するという文がすでにあります。Google Play の「データ セーフティ」フォームにも同じ内容を記入します。
  • 鍵が漏れた場合は、Firebase コンソールでその鍵を削除し、新しく取得してファイルを置き換えてから、サーバーを再起動します。アプリを再ビルドする必要はありません。
iPhone とデスクトップ

iPhone は Firebase ではなく、Apple Developer アカウントの APNs キー(.p8)で別に有効にします — すぐ下の手順 11 を参照してください。macOS・Windows アプリはプッシュなしで、起動している間に受け取ります。

11. iPhone のプッシュ通知(任意)

iPhone も、画面から外れたアプリをまもなく停止させます。プッシュを有効にしないと、メッセージと着信を受け取れるのはアプリを開いている間だけです。プッシュは Apple プッシュ通知サービス(APNs)で送り、Android と同じように、通知の内容はその iPhone だけが持つ鍵で暗号化します。Apple に見える通知の文言はアプリ名と「You have a new notification」のような中身のない言葉だけで、iPhone アプリの中の通知拡張(Notification Service Extension)が暗号文を復号し、送信者とメッセージに置き換えて表示します。

  • 登録:アプリは起動するたびに、アカウントごとに 2 つを登録します — 通知用(type 1)と着信用(VoIP、type 9)。サーバーは登録と一緒に、アプリの言語(表向きの文言を書く言語)とビルド番号を記憶します。
  • メッセージは通知で送られます。通知をオフにした会話には何も送りません。別の端末で既読にしたものや削除したものは、音の鳴らないバックグラウンドプッシュで知らせ、表示されていた通知をアプリが片付けます。
  • 着信は VoIP で送ると、ロックされた画面でも CallKit の着信画面が表示されます。ただし VoIP を受け取ったアプリはすぐに CallKit に着信を知らせなければならず、できないと iOS がアプリを終了させ、それが繰り返されると VoIP を届けなくなります。そのため、そのように作られたビルドからだけ APNS_VOIP_MIN_BUILD で有効にし、それまでは着信も通知で送ります。
APNs キーはアプリに署名したチームのものです

Firebase と違って、アプリに入れる設定ファイルはありません — アプリはすでに APNs を使えるようにビルドされており(通知の許可・App Group・通知拡張)、サーバーにキーを置くだけです。ただし、サーバーのキーでプッシュを送れるのは、そのキーを作ったチームが署名し、バンドル ID が APNS_TOPIC と同じアプリだけです。あなたのサーバーの iPhone アプリをあなたの Apple Developer アカウントで署名したなら、APNs キーもそのアカウントで作ります。

APNs キーを作る

  1. Apple Developer → Certificates, IDs & Profiles → Keys → +。アプリに署名したチームのアカウントである必要があります。
  2. 名前を入力し、Apple Push Notifications service (APNs) にチェックを入れます。環境は Sandbox & Production、範囲はチーム全体(Team Scoped)にします — 通知と VoIP の 2 か所に送るためです。
  3. Register → Download。AuthKey_<キー ID>.p8 は一度しかダウンロードできません。このファイルは秘密です — 内容をリポジトリ・チャット・.env に入れないでください。

サーバーにキーを置く

sudo cp AuthKey_<キー ID>.p8 /srv/pabal/pabal_server/keys/
sudo chown 1000:1000 /srv/pabal/pabal_server/keys/AuthKey_<キー ID>.p8    # サーバーのコンテナのユーザー
sudo chmod 600 /srv/pabal/pabal_server/keys/AuthKey_<キー ID>.p8

.env に追加します。コンテナの中では、pabal_server/keys は /app/keys です。

APNS_KEY=/app/keys/AuthKey_<キー ID>.p8
APNS_TEAM_ID=4Z8R468M2D
APNS_TOPIC=me.pabal.ios
APNS_MIN_BUILD=4
APNS_VOIP_MIN_BUILD=
値設定内容備考
APNS_KEYキーファイルのパスキー ID はファイル名から読み取ります。別の名前で置いた場合は、APNS_KEY_ID にキー ID を設定
APNS_TEAM_IDアプリに署名したチームの IDPabal アプリは 4Z8R468M2D。自分でビルド・署名したアプリなら、そのチーム
APNS_TOPICアプリのバンドル IDPabal アプリは me.pabal.ios。着信(VoIP)は、これに .voip を付けた宛先に送られる
APNS_MIN_BUILD4これより低いビルドには何も送らない(0 ならすべてに送る)。4 は、プッシュを試す前に作った Pabal 1.0 (3) を除く値
APNS_VOIP_MIN_BUILD空のまま空なら着信も通知で送られる。着信をすぐに CallKit に知らせる最初のビルドの番号を書くと、そのビルドから CallKit の着信画面になる
docker compose up -d pabal-server
docker compose logs pabal-server | grep -i push
INFO  TelegramServer - iPhone push notifications on: APNs key <キー ID> of team 4Z8R468M2D, topic me.pabal.ios, builds from 4, calls as alerts (TELEGRAM_APNS_VOIP_MIN_BUILD not set)
  • この行が表示されれば有効です。キーファイルを読めない、または形式が正しくない場合は、iPhone push notifications are off: … に理由が出ます(ファイルの内容はログに出ません)。Android(FCM)とは別々に有効・無効にします — 片方だけ有効にしてもかまいません。
  • コンテナから api.push.apple.com(443、HTTP/2)への外向きの接続が開いている必要があります。Xcode から実機に直接インストールしたデバッグビルドは api.sandbox.push.apple.com に送られます(アプリが登録するときに知らせます)。

確認

  1. 実機の iPhone でアプリを一度開き、通知を許可します。サーバーのログに Push device registered: user … type 1 token … (push key 256 bytes, build …) と type 9 の行が出ます。シミュレーターでは確認できません — APNs トークンも CallKit もありません。
  2. アプリを上にスワイプして閉じます。
  3. 別のアカウントからメッセージを送ると、ログに Push MESSAGE_TEXT to user …: sent が出て、ロック画面に送信者と内容が表示されます。
  • プライバシーポリシー:管理画面(관리 화면)→ 設定(설정)→ 運営者情報(운영자 정보)の処理の委託(처리 위탁)と国外移転(국외 이전)に、Apple Inc.(Apple プッシュ通知サービス、米国)を記入してください。ポリシーの本文には、APNs を経由するという文がすでにあります。App Store Connect の App のプライバシーの質問でも、デバイス ID(プッシュトークン)の項目を確認します。
  • キーが漏れた場合は、Apple Developer アカウントでそのキーを Revoke し、新しく作ってファイルを置き換えてから、サーバーを再起動します。アプリを再ビルドする必要はありません。

12. 音声ボット(任意)

音声ボットは、アプリから人がボットに電話をかけると AI 音声モデルが応答する機能です(ボットのオーナー向けの説明は音声ボット)。サーバーに部品を 2 つ追加し、法的文書とアプリのビルドがそろってはじめて有効になります。既定では無効です。

音声は追加の Compose ファイルで有効にします

音声の部品は deploy/docker-compose.yml ではなく、追加のファイル deploy/docker-compose.voice.yml にあります — サーバーの音声の設定とリンク専用のネットワーク、ゲートウェイ pabal-voice(プロファイル voice)、ウォッチの音声のエッジ pabal-voice-edge(プロファイル voice-watch)。.env がこのファイルを指定しなければ、音声の設定そのものがありません。この構成はエコーエンジンで動かしてみましたが、本番サーバーで音声を有効にしたことはまだなく、音声ボットの通話を本番の中継(TURN)で最後まで通したこともありません — エンドツーエンドのテストは、Docker ネットワーク内のテスト用の中継で行いました(scripts/voice-e2e/run.sh)。有効にする前に、リポジトリの deploy/voice_readiness.py で足りないものを点検できます。一般公開の前の条件(法的な確認、提供元の書面での回答、アプリの配布)は運営者が判断することで、コードはその一部だけを防ぎます。

部品役割ない場合
pabal-voice(ゲートウェイ、イメージ 0.11.0)ボットの代わりに通話の一方の端になって鍵交換を行い、音声を AI モデルとやり取りします。Python 3.12 + ntgcalls。サーバーとはリンク(WebSocket。認証はされるが暗号化はされない)で話しますすべてのボットの通話が話し中(no_gateway)
coturn(通話中継)ボットの通話は中継だけを使います:アプリもゲートウェイも直接の接続を切り、中継を通してだけ音声を送ります。人どうしの通話の中継と同じもの(TURN_SECRET)すべてのボットの通話が話し中(no_relay)、ゲートウェイも接続を拒否される(4008)
Vertex AI のキーGoogle Cloud のサービス アカウントの JSON キー。ゲートウェイのエンジンの作業プロセスだけが開きますモデルなしのエコーだけ(VOICE_ENGINE=echo、音声はマシンの外に出ない)
(任意)マスターキーボットのオーナーの自前のキーとストリームモードのシークレットを包むキー自前のキー・ストリームモードが無効
(任意)MCP の部品pabal-mcp · 署名器 · 送信プロキシ(下記)MCP サーバーが無効

有効になる条件

サーバーが見るものない場合
リンクのシークレットのファイル(TELEGRAM_VOICE_LINK_SECRET_FILE、32 バイト)音声は無効 — リンクのポートも開かない
通話中継(TELEGRAM_TURN_HOST・TELEGRAM_TURN_SECRET)すべてのボットの通話が話し中
サイトのプライバシーポリシー(ko・en の両方)の id="bot-calls" 節 — アプリの確認画面はここを指すどのボットも電話を受けない(ERROR ログ)。草案は docs/voice-bots/p0/privacy-*.draft.html
利用規約の id="voice-bot-owners" 節誰もボットオーナー規約に同意できない → 文字起こし・自前のキー・ストリームモード・MCP がすべて使えない
ポリシーの id="bot-calls-stream" · id="bot-calls-mcp" 節ストリームモード · MCP が無効
管理画面でのボットの許可(既定は許可したボットだけ)と一般公開(既定はテスターだけ、公開するには確認の文言を入力)許可していないボットは電話を受けず、一般公開の前はテスターだけが電話できる
発信者のアプリのビルド:デスクトップ pabal-bc2(確認画面が韓国語・英語だけの間は pabal-bc1 も)· Android 62151 · iPhone 5 以上 · Apple Watch 1 以上(TELEGRAM_VOICE_MIN_BUILD_*)その人には通話ボタンがなく、かけても話し中
(任意)Apple Watch:サーバーのウォッチ API(TELEGRAM_WATCH_API_PORT)と音声のアドレス(TELEGRAM_WATCH_AUDIO_URL)、音声のエッジ pabal-voice-edge、direct を示したゲートウェイ(下記)ウォッチからかけられない(スマートフォン・パソコンのアプリはそのまま)
(任意)会話を残す:ポリシーの id="bot-calls-keep-text" 節、keep_text を示したゲートウェイ(VOICE_KEEP_TEXT、下記)チェックボックスを選んでも会話が残らない
(任意)ボットからの電話:ポリシーの id="bot-calls-outbound" 節と規約の id="voice-bot-owners-outbound" 節、証拠の鍵のフォルダー、発信の最小ビルド、outbound を示したゲートウェイ、管理画面の全体のスイッチ(下記)どのボットも人に電話をかけられない

テストサーバー(TELEGRAM_TEST_NUMBERS=true)でだけ、TELEGRAM_VOICE_NOTICE_CHECK=off で法的な節の確認を飛ばし、TELEGRAM_VOICE_BOTS(すべてのボットなら *)・TELEGRAM_VOICE_TESTERS=* で誰でも試せるようにできます。本番では使わないでください — 本番サーバーは TELEGRAM_VOICE_BOTS を警告とともに無視します。

通話中継(coturn)

  • Linux のサーバーなら、Compose の pabal-turn(COMPOSE_PROFILES=turn)を使います。.env の TURN_SECRET(openssl rand -hex 32)を決め、ルーター・ファイアウォールで TURN_PORT(3478、udp・tcp)と TURN_MIN_PORT〜TURN_MAX_PORT(49160〜49200、udp)を開けます。範囲を広げるときは、ルーターと .env を一緒に変えます。
  • macOS の Docker Desktop では coturn をコンテナで動かさないでください。すべての発信者が 192.168.65.1 に見えるため中継の許可が合わず、見かけは正常なのに音声がすべて捨てられます。Mac のサーバーでは coturn を直接インストールします(deploy/turnserver.conf.example、docs/turn-relay-guide.md)。
  • ゲートウェイに渡す中継のアドレスはサーバーの LAN アドレスです:サーバーの TELEGRAM_VOICE_TURN_HOST とゲートウェイの VOICE_TURN_HOST に同じ値を入れます。host.docker.internal では届かず、127.0.0.1 では割り当てが完了しませんでした。アプリには公開アドレス(TURN_HOST)がそのまま渡ります。
  • 外部のネットワークから deploy/check_relay.py --data で、音声が実際に中継を渡るかを確かめます。STUN が答えて割り当てができても、音声が渡らないことがあります。

Vertex AI のキー

  1. Google Cloud のプロジェクトで Agent Platform API(旧名 Vertex AI API、aiplatform.googleapis.com)を有効にします。請求先アカウントがリンクされている必要があります。Gemini Developer API(AI Studio のキー)は使いません。
  2. サービス アカウントを 1 つ作り、ロールは roles/aiplatform.user 1 つだけを与えます(さらに絞るなら aiplatform.endpoints.predict の権限 1 つだけのカスタムロール)。本番用のキーは開発用とは別に作ります。
  3. JSON キーをダウンロードしてリポジトリと .env の外、たとえば /srv/pabal/pabal_voice/secrets/vertex-sa.json に置き、ゲートウェイのユーザーだけが読めるようにします(Linux: chown 10001:10001、chmod 400)。誰でも読めると、ゲートウェイは起動を拒否します。
  4. リアルタイムのモデルは us-central1(米国アイオワ)にしかありません(2026-09-26 確認)。通話の音声が米国に送られることをプライバシーポリシーと確認画面が伝える必要があり、アプリの確認画面に書かれた地域とゲートウェイの地域(VOICE_VERTEX_REGION)が一致してはじめて、サーバーはそのゲートウェイに通話を渡します。
  5. やること:予算アラート、同時セッションの割り当て(ゲートウェイの容量の 2 倍)、そして Vertex のデータキャッシュ(有効だと入出力を最大 24 時間メモリに置く)— 確認画面は「Google は返答を作ったあと音声も文字も保管しない」と伝えるので、キャッシュを無効にするまではその言葉と合いません。おすすめ:一般公開の前にデータキャッシュを無効にし(リポジトリの docs/voice-bots/vertex-setup.md の 9 節)、キーのロールを予測の権限 1 つに絞ってください(同じドキュメントの 10 節)。

リンクのシークレットは、サーバーとゲートウェイが同じ値をファイルから読みます。リンクは暗号化されないので、2 つのコンテナだけが接続する専用のネットワークにだけ置きます — ほかのスタックと共用する ssemiya-net や LAN には置かないでください。ポートは公開しません。

(umask 077; openssl rand -hex 32 > /tmp/voice-link.secret)
sudo install -m 400 -o 1000 /tmp/voice-link.secret /srv/pabal/pabal_server/keys/voice-link.secret   # サーバー(uid 1000)用
sudo install -d -m 700 -o 10001 /srv/pabal/pabal_voice/secrets
sudo install -m 400 -o 10001 /tmp/voice-link.secret /srv/pabal/pabal_voice/secrets/voice-link.secret # ゲートウェイ(uid 10001)用
shred -u /tmp/voice-link.secret

.env に入れる値です。追加のファイルがこの値で pabal-server に音声の設定(TELEGRAM_VOICE_*)とリンクのネットワークを加えます。

COMPOSE_FILE=docker-compose.yml:docker-compose.voice.yml
COMPOSE_PROFILES=voice                              # ほかのプロファイル(turn など)があればカンマで並べる
VOICE_LINK_SECRET_FILE=/app/keys/voice-link.secret  # コンテナー内のパス。空なら音声は無効
VOICE_TURN_HOST=192.168.1.64                        # サーバーの LAN アドレス(coturn が待ち受ける場所)
VOICE_TESTERS=100001                                # 一般公開の前に発信できる人の id(管理画面でも追加できる)
  • 追加のファイルはサーバーを内部専用のネットワーク pabal-voice-link(10.231.53.0/29 — サーバー .2、ゲートウェイ .3)につなぎ、サーバーはそのネットワーク上の自分のアドレスでだけ待ち受けます(0.0.0.0 だと一緒につながる共有ネットワークでも開きます)。この範囲が LAN・VPN・ほかの Docker ネットワークと重なるなら、VOICE_LINK_SUBNET・VOICE_LINK_SERVER_IP・VOICE_LINK_GATEWAY_IP の 3 つをまとめて変えます。
  • ループバック以外のアドレスにバインドするには TELEGRAM_VOICE_LINK_ANY_ADDRESS=true が必要で、公開アドレスには決してバインドされません。リンクがどのネットワークを見ているかは、サーバーのログでわかります。
  • 認証前の接続は、アップグレード 3 秒・hello 3 秒、1 つのアドレスから 2 つ、合計 16 接続までしか受け付けません。
  • 音声に関するサーバーの設定の全体は管理と設定 — 音声ボットの環境変数にあります。

ゲートウェイ(pabal-voice)

cd /opt/pabal
docker build -t pabal-voice:latest voice/                                       # このコンピューターの CPU 用
docker buildx build --platform linux/amd64 -t pabal-voice:latest --load voice/  # Intel のサーバー用
cd deploy && docker compose up -d                                         # .env の COMPOSE_FILE・COMPOSE_PROFILES のとおり

イメージはハッシュで固定した Python 3.12 で、コンパイラーも pip もなく、uid 10001 で動きます。ntgcalls は Linux x86_64/aarch64 と macOS arm64 にしかないため、Intel の Mac では Docker でしか動かせません。追加のファイルの pabal-voice はこのイメージをビルドせずに使います(image: pabal-voice:latest)。別のコンピューターでサーバーの CPU 用に作って送るには deploy/ship.sh <user@host> <フォルダー> --voice を使います(直前のイメージは pabal-voice:previous として残ります)。下はそのファイルのゲートウェイのサービスを短くしたものです。

  pabal-voice:                             # deploy/docker-compose.voice.yml から(抜粋)
    image: pabal-voice:latest
    profiles: [voice]
    init: true
    stop_grace_period: 40s                 # VOICE_DRAIN_SECONDS(20)より長く
    security_opt: ["no-new-privileges:true"]
    cap_drop: [ALL]
    mem_limit: ${VOICE_MEMORY:-1g}
    ulimits:
      core: 0
    environment:
      VOICE_LINK_URL: ws://${VOICE_LINK_SERVER_IP:-10.231.53.2}:8095/voice/link
      VOICE_LINK_SECRET_FILE: /run/secrets/voice-link.secret
      VOICE_ENGINE: gemini
      VOICE_VERTEX_KEY_FILE: /run/secrets/vertex-sa.json
      VOICE_VERTEX_REGION: us-central1
      VOICE_TURN_HOST: ${VOICE_TURN_HOST:-}  # サーバーの TELEGRAM_VOICE_TURN_HOST と同じ値
      VOICE_CAPACITY: ${VOICE_CAPACITY:-2}
      VOICE_DAILY_MODEL_MINUTES: ${VOICE_DAILY_MODEL_MINUTES:-60}
      VOICE_SAFETY_THRESHOLD: ${VOICE_SAFETY_THRESHOLD:-MEDIUM}   # テスターの通話: 運営者が確認すべき前提(下記)
      VOICE_BYOK: "false"
      VOICE_STREAM: "false"
    volumes:
      - ${PABAL_HOME:-/srv/pabal}/pabal_voice/secrets:/run/secrets:ro   # リンクのシークレットと Vertex のキー — backup.sh には入らない
      - pabal_voice_state:/var/lib/pabal-voice
    networks:
      pabal-voice-link:                    # サーバーとだけ使うリンクのネットワーク(internal)
        ipv4_address: ${VOICE_LINK_GATEWAY_IP:-10.231.53.3}
      pabal-voice-out: {}                  # 外(Google・中継)へ出る自分のネットワーク
  • ゲートウェイには、リンクのほかに外へ出る経路が必要です:Google(us-central1-aiplatform.googleapis.com:443)と通話中継(UDP)。リンクのネットワークは internal: true で外に出られないので、上のように pabal-voice-out のネットワークをもう 1 つ付けます。
  • Landlock:モデルやストリームモードを使うゲートウェイは、Linux カーネルの Landlock(5.13 以上。Docker の既定の seccomp で使える)がないと起動を拒否します(終了コード 2)。発信者が送ってくるものをネイティブコードで扱うメディアの作業プロセスが、自分自身をロックしてキーのファイルを読めないようにするためです。コンテナの中でシークレットのファイルを /app・/opt・/usr・/lib・/etc の下にマウントしないでください(/run/secrets は問題なし — 上の例のとおり)。VOICE_ALLOW_NO_LANDLOCK=true は開発用の Mac 専用です。macOS の Docker Desktop で Landlock が使えるかは確認していません。
  • メモリ:通話 1 本にメディア 60〜100 MB + エンジン 30〜40 MB。容量 8(VOICE_CAPACITY)と予備 1 組で約 1.3 GB。追加のファイルの既定は容量 2・メモリ 1 GB(VOICE_MEMORY)です。
  • 安全フィルター VOICE_SAFETY_THRESHOLD のゲートウェイの既定は LOW(最も厳しい)ですが、測定では問題のない韓国語の発話 172 個のうち 13 個を英語の拒否に変えました(MEDIUM は 69 個のうち 0 個)。テスターの通話では MEDIUM を明示するのがリポジトリの前提で、既定値を変える前にはレッドチームのテストが必要です。
  • 上限:ゲートウェイの既定は 1 通話 600 秒・40 万トークン・毎分 30 ターン、1 日(UTC)モデル 300 分・800 万トークンで、追加のファイルは 1 日を 60 分・160 万トークンにしています(VOICE_DAILY_MODEL_*。超えると新しい通話は話し中)。OpenAI のエンジンは偽のサーバーでしかテストしておらず無効で、エンジン間の自動切り替えはありません。
  • 言語:ゲートウェイはサーバーにつながるときに、自分が通話できるボットの言語を伝え(0.7.0 からは 9 言語すべて)、サーバーはボットの言語で通話するゲートウェイにだけ、そのボットの通話を渡します — 言語を伝えない古いゲートウェイ(0.6.0)には、韓国語・英語のボットだけが渡ります。案内の録音はイメージの assets/ にあり、どの録音が確認済みかは assets/assets.json が決めます。このファイルがないと AI の案内を載せられないため、通話を受けられません(起動ログに ERROR)。確認が済んでいない録音(いまは韓国語・英語以外のすべての言語)は再生せず、その言語の発信者には英語の案内が流れます。VOICE_DRAFT_NOTICES=true は草案の録音まで再生するテストサーバー専用のスイッチで、VOICE_ALLOW_TEST_URLS=true が一緒になければゲートウェイは起動しません。VOICE_NOTICE_LANG(サーバーが発信者の言語を伝えないときの案内の言語、既定は ko)は録音の言語 10 個(ko en ja zh fr es id vi km ar)のどれか、VOICE_LANGUAGE(ボットのオーナーの設定がない通話のボットの言語、既定は ko-KR)はボットの言語の一覧にある id(音声ボット — 声と言語)のどれかでなければなりません。
  • ログには、リンクのシークレット・キー・音声・文字起こし・指示文は残りません。SIGTERM を受けると新しい通話を受けず、進行中の通話に VOICE_DRAIN_SECONDS(既定 20 秒)を与えてから終えます。

確認

docker compose logs pabal-server | grep -E "Voice|voice"
docker compose logs pabal-voice | head -20
INFO  TelegramServer - Voice bots on: gateways connect to ws://10.231.53.2:8095/voice/link; bots 0 listed, 2 calls each at most
INFO  VoiceGatewayLink - Voice gateway link listening on ws://10.231.53.2:8095/voice/link (ネットワーク)
INFO  TelegramServer - Voice bot callers: patched apps only (desktop pabal-bc1+, Android 62151+, iOS 5+), recipients google.vertex.gemini-live@us-central1, testers 0 listed + the admin page's
INFO  VoiceGatewayLink - Voice gateway pabal-voice-1 (boot 823c9600d05e811847ce95a8f1ba9efa) connected: new session, capacity 2, features […], audio to google.vertex.gemini-live@us-central1
  • Voice bots are off: … はリンクのシークレットがない・読めない・32 バイトではないとき、行末の — but without a TURN relay every bot call is busy は中継がないときです。
  • ERROR Voice bots are on, but no bot is callable: the privacy policy … has no id="bot-calls" section が出たら、まずサイトのポリシーにボット通話の節を掲載する必要があります。
  • ゲートウェイがつながると、サーバーのログの features に bot_settings・end_call・context・tools と、オーナーのキーで動かせるゲートウェイなら byok、文字起こしの案内音声があれば transcripts が表示されます(順序はそのつど異なる)。audio to は、アプリの確認画面が伝える送り先と同じでなければなりません。
  • ゲートウェイ側の最初の行に starting as pabal-voice-1 (capacity 2, … engine gemini, audio to google.vertex.gemini-live@us-central1)、モデルの行に session resumption off; no failover、そして media workers sandbox themselves with Landlock (ABI N) が出ている必要があります。recordings for the callers' notices in: Korean, English は、発信者の案内を再生できる言語です(草案まで再生するテストサーバーなら (drafts included: …) が付きます)。
  • 次に、管理画面の音声(음성)タブ(管理と設定 — 音声タブ)でゲートウェイがつながったかを見て、テスターを入れ、ボットを許可します。

マスターキー(自前のキー · ストリームモード)

ボットのオーナーのモデルのキーとストリームモードのシークレットは、このキーで包んで保存します。バックアップと一緒にコピーされない場所に置く必要があります。

sudo install -d -m 700 -o 1000 -g 1000 /etc/pabal/voice                 # /srv/pabal の外、700
sudo sh -c 'umask 077; openssl rand -hex 32 > /etc/pabal/voice/voice-master.key'
sudo chown 1000:1000 /etc/pabal/voice/voice-master.key
      TELEGRAM_VOICE_KEYS_MASTER_FILE: /app/secrets/voice-master.key
    volumes:
      - /etc/pabal/voice:/app/secrets:ro
  • データフォルダーや RSA キーのフォルダーの中やそのそばにある場合や、フォルダーがほかのユーザーに少しでも開かれている場合(700 でない場合)、サーバーはこれを使いません(Voice keys off: …)。ファイルがなければ、最初のキーを受け取るときにサーバーが作ります(読み取り専用のマウントなら、上のように自分で作ってください)。
  • バックアップには入れず、オフラインのコピーを別に 1 つ置きます。失うと、すべてのボットのオーナーがキーを設定し直す必要があります。交換するときは、TELEGRAM_VOICE_KEYS_PREVIOUS_MASTER_FILE に古いキーを渡して起動し、ログを確認してから古いファイルを削除します。
  • マスターキーがあっても、自前のキーは、アプリが自前のキーのボットの確認画面(第 2 版)を表示するようになったあとで TELEGRAM_VOICE_OWN_KEYS_APPS_READY=true を渡すまで受け付けません。ストリームモードは、ストリーム用の確認画面を含むアプリのビルドを TELEGRAM_VOICE_STREAM_MIN_BUILD_DESKTOP・_ANDROID・_IOS で伝えてはじめて電話でき(既定 0 = 誰もかけられない)、ゲートウェイに VOICE_STREAM=true と、このデプロイの公開 IP(VOICE_STREAM_DENY_ADDRESSES)が必要です。

MCP の部品(任意)

ボットのオーナーが登録した MCP サーバーを通話中に呼ぶ部品です。既定では無効で、deploy/docker-compose.mcp.yml(プロファイル mcp)で 3 つのコンテナを追加します。

コンテナ役割
pabal-mcp(uid 1101)MCP クライアント。サーバーとはソケットファイルでだけつながり、内部ネットワークが 1 つだけなので、送信プロキシを通してだけ外に出ます。読み取り専用、権限なし、512 MB
pabal-mcp-signer(uid 1102)署名器。ネットワークがまったくなく、ソケットでだけ答え、公開の発行者ドキュメントを書きます
pabal-mcp-egress(uid 1103)送信プロキシ。443 だけで、Pabal 自身のアドレスには行きません
docker compose -f docker-compose.yml -f docker-compose.mcp.yml --profile mcp up -d
  • 有効にするには TELEGRAM_MCP_EGRESS_OWN_ADDRESSES(Pabal の公開アドレス、必須)とキーのファイル(リンクのシークレット、K_grant、インストールキー — どれも /srv/pabal の外の 700 のフォルダー)が必要で、サイトにポリシーの bot-calls-mcp 節と規約のボットオーナーの節が必要です。1 回だけ行う設定は、docker-compose.mcp.yml の冒頭のコメントに順番に書いてあります。ボールトと署名鍵はバックアップに入れません。
  • アプリが確認画面にボットの MCP の送り先を表示するようになってから、TELEGRAM_MCP_APPS_READY=true を渡します。それまでは登録だけされ、どの通話にも MCP ツールは載りません。
  • MCP のリクエストが出ていく専用の送信元アドレスを別に用意するのは、コードの条件ではなく運用上の判断です(発信者の言葉が第三者のサーバーへ出ていく出口なので)。
  • 有効になると、ログに MCP tools on: pabal-mcp dials …; issuer …; ports …; own addresses N; clock by NTP …。deploy/ship.sh <サーバー> <フォルダー> --mcp は 3 つのイメージを作って送り、override ファイルをコピーするだけで、何も有効にしません(Linux の Docker で動かしたことはない)。

Apple Watch の入口(任意、準備中)

Apple Watch アプリは MTProto の代わりにサーバーのウォッチ API(HTTPS JSON、/api/watch/v1/)でログインして電話をかけ、通話の音声は音声のエッジ(pabal-voice-edge、WSS)を通ってゲートウェイに届きます。通話中継(coturn)は使いません。既定では無効です。

ウォッチの音声のエッジには、まだ外から届きません

deploy/docker-compose.voice.yml には音声のエッジ pabal-voice-edge(プロファイル voice-watch、シークレットなし、内部専用のネットワーク)がありますが、サーバーのウォッチ API の設定(TELEGRAM_WATCH_*)とリバースプロキシのウォッチ用のパスはまだないため、外のウォッチからは届きません。以下は設定値の案内で、本番の構成で動かしたことはありません(開発サーバーでのみ試験)。

設定既定値意味
TELEGRAM_WATCH_API_PORT—ウォッチ API のポート。なければウォッチ API は無効
TELEGRAM_WATCH_API_HOST、TELEGRAM_WATCH_API_ANY_ADDRESS127.0.0.1、—待ち受けるアドレス。ループバック以外なら ANY_ADDRESS=true が必要で、前に TLS プロキシを置きます
TELEGRAM_WATCH_AUDIO_URL—ウォッチに伝える音声のエッジのアドレス。パスは正確に wss://<ホスト>/api/watch/v1/audio。なかったり間違っていたりすると、ウォッチの通話は無効
TELEGRAM_WATCH_ALLOW_PLAIN—ws:// のアドレスを許可。開発専用(自宅での試験)
TELEGRAM_WATCH_SESSION_DAYS7ウォッチのログインが続く日数(1〜365)
TELEGRAM_WATCH_AUTH_PER_MINUTE30サーバー全体のログインコード要求の上限(1 分あたり)。アドレスごとの上限は別にある
TELEGRAM_VOICE_MIN_BUILD_WATCHOS1確認画面の全体を表示する最初のウォッチアプリのビルド
  • ゲートウェイ:VOICE_DIRECT=true と VOICE_EDGE_DIR(エッジと共有するソケットのフォルダー、既定は /run/pabal-voice-edge)。ウォッチの通話は 16 kHz のエンジン(Gemini)かエコーでのみ動きます。
  • 音声のエッジ:同じイメージの 2 つ目のサービスとして python -m pabal_voice.edge を動かします(VOICE_EDGE_HOST・VOICE_EDGE_PORT、既定は 127.0.0.1・8096、VOICE_EDGE_DIR)。エッジはインターネットから来たものを最初にほどく場所なので、シークレットを 1 つも付けず、プロキシとゲートウェイ以外からは届かない内部ネットワークに置きます。
  • リバースプロキシはウォッチの 2 つのパスだけを渡します — /api/watch/v1/audio(WebSocket、エッジへ)と /api/watch/v1/(ウォッチ API へ)。ウォッチ API 側では、接続元のアドレスを X-Forwarded-For の末尾に付けるようにします(サーバーはその値でアドレスごとのログインの上限を数えます)。エッジ側では X-Forwarded-For・X-Real-IP を削除します。
  • ウォッチでは登録できず、ツール・MCP・自前のキー・ストリームモード・ボットのオーナーの文字起こしを使うボットにはかけられません(WATCH_NOT_YET)。

会話を残す(任意、準備中)

電話をかける人が確認画面で選ぶと、通話の会話をその人のチャットにだけ文字で残す機能です(アプリの使い方 — 音声ボット)。運営者の許可スイッチはなく、以下がすべてそろうと有効になります。

  • プライバシーポリシーの id="bot-calls-keep-text" 節(草案:リポジトリの docs/voice-bots/p0/privacy-*.draft.html)。この節そのものが有効にするスイッチです。アプリは help.getAppConfig の pabal_voice_keep_text で、チェックボックスを表示するかどうかを知ります。
  • ゲートウェイ 0.9.0 の VOICE_KEEP_TEXT:モデルのエンジンなら既定は true。エコーのエンジン(ストリーム専用のゲートウェイを含む)で true なら設定エラーとして起動しません。
  • チェックボックスを含むアプリのビルド。チェックボックスの文言は、法的な確認を終えてからリリースビルドに載ります。ウォッチには確認前の文言を表示せず、TELEGRAM_VOICE_KEEP_TEXT_UNREVIEWED=true は確認前の文言を表示する社内の試験サーバー専用のスイッチです。
  • 有効なサーバーでは、ストリームモード以外のすべてのボット通話の voice_call ended が最大 10 秒遅れ、通話記録のあとのメッセージ番号を 30 個確保します(発信者の選択をボットのオーナーが知ることができないように)。
  • 通話が終わったあと書き込みを待つ会話の文章はメモリにだけあるため、その間にサーバーを再起動すると、その通話の文章は残りません。

ボットからの電話(任意、法的な確認待ち)

音声ボットが、そのボットに電話を許可した人に自分から電話をかける機能です(音声ボット — ボットからの電話)。ボットへの電話より条件が多く、法的な確認を終える前にはオンにしないでください。有効にする順番:

  1. サーバー:証拠の鍵のフォルダー(TELEGRAM_VOICE_EVIDENCE_KEY_DIR — 700、データ・鍵のフォルダーのようなバックアップされるフォルダーの外)を scripts/voice-evidence-key.py の new で作り(取り替えは rotate、点検は check)、許可画面の入った最初のアプリのビルドを TELEGRAM_VOICE_OUTBOUND_MIN_BUILD_ANDROID のような発信の最小ビルドに設定します(環境変数)。.env に書いただけの TELEGRAM_VOICE_EVIDENCE_KEY_DIR はサーバーに届きません — deploy/docker-compose.voice.yml の pabal-server の environment(コンテナー内のパス)と volumes(ホストのそのフォルダー)に加えてください。
  2. アプリ:許可画面の入ったアプリのビルドを出します。アプリの許可のリンクのルールは pabal.me しか知らないので、別のサーバーならアプリのビルドのリンクのルールにそのサーバーのホストを加えないと、ボットが送ったリンクから画面は開きません(メニューと設定の一覧はそのまま使えます)。
  3. 法的な手続き:プライバシーポリシーの id="bot-calls-outbound" 節と利用規約の id="voice-bot-owners-outbound" 節(ボットオーナーが規約に同意するには voice-bot-owners 節も)を掲載します。下書きはリポジトリの docs/voice-bots/p0/ にあります。許可画面の文言と発信の案内録音(notice-out・apology-rule)の確認と承認もこのときです。
  4. スイッチ:管理画面の音声タブのボットからの電話で国と上限を決め、確認の文言でオンにします(管理画面 — 音声タブ)。ボットごとに運営者による発信の審査が別にあります。
  • ゲートウェイ:VOICE_OUTBOUND=true のゲートウェイだけがボットの電話をかけます。モデルのエンジンなら、発信の案内録音 4 つが承認されていないと起動せず、下書きの録音(VOICE_OUTBOUND_DRAFT_NOTICES)はテストサーバー専用です。
  • 本番の .env には TELEGRAM_VOICE_CALLME_UNREVIEWED・TELEGRAM_VOICE_TEST_DIAL のようなテスト・開発専用の名前を置かないでください。
  • ボットからの電話もエンドツーエンド暗号化ではありません — 音声はこのサーバーのゲートウェイと AI の提供者を通ります。

13. 位置情報メッセージの地図画像(任意)

位置情報を送ると、Android アプリとデスクトップ(macOS・Windows)アプリは吹き出しの中の地図を自分では描かず、その地点の地図画像をサーバーに求めます(upload.getWebFile)。サーバーはその画像を Google の Maps Static API から取得して渡します。有効にしないと、Android はプレースホルダーの画像を、デスクトップは空の枠にピンだけを表示します。iPhone は端末上で地図を描くのでこの設定とは関係がなく、位置情報の送信そのものと、タップすると開く大きな地図はどちらでも使えます。

  1. Google Cloud コンソールで請求先アカウントにつながったプロジェクトを選び、Maps Static API を有効にします。
  2. API キーを作り、アプリケーションの制限は IP アドレス(サーバーがインターネットに出るときの公開 IP)、API の制限は Maps Static API だけにします。このキーはサーバーにだけ置き、アプリには入れません。
  3. .env に入れ、docker compose up -d pabal-server でサーバーを再起動します。
  4. ログで確認します。無効なら Maps in location messages are off: … とその理由が出ます(キーの値はログに出ません)。
STATIC_MAPS_KEY=AIza…               # キーの値はチャット・ログ・リポジトリに残さない
STATIC_MAPS_PER_DAY=300             # 1 日に新しく取得する画像の数(0 = 制限なし)
STATIC_MAPS_PER_USER_HOUR=120       # 1 人が 1 時間に新しく取得させられる画像の数
INFO  TelegramServer - Maps in location messages from Google Static Maps (120 new per person an hour, 300 a day)
  • 費用:Maps Static API は月 1 万件まで無料です。サーバーは取得した画像を 1 日覚えておき、同じ地図を求める端末にもう一度渡します。もう一度渡した画像は上限に数えません。既定の上限(1 日 300 件)なら月に約 9 千件で、無料の範囲に収まります。上限を超えたリクエストは FLOOD_WAIT で拒否され、その間アプリはプレースホルダーの画像を表示します。
  • プライバシーポリシー:共有された位置の座標(緯度・経度)が Google に送られます — 誰が送ったかは送りません。管理画面 → 設定 → 運営者情報の委託先と国外移転に Google LLC(Maps Static API、米国)を書き入れてください。
  • ログに Map picture not made: Google Static Maps answered 403: … が出たら、キーの制限(IP・API)や請求先アカウントを確認します。

運用

状態・ログ

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 さえ削除しなければ大丈夫です。

元に戻す:直前のイメージに戻しても、新しい版がデータベースにすでに書いたものはそのままです。新しい版の記録を古い版が読めないことがあるので(たとえば音声ボットのコードが入った版は、音声を無効にしていてもチャットの通話記録のような新しいイベントを書きます)、戻すときはアップデート直前のバックアップの DB も一緒に復元します(復元)。

バックアップ

./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
docker compose exec -T pabal-postgres psql -U pabal -d pabal -c "ALTER DATABASE pabal SET search_path = public"   # データベースの既定のパス — 復元では戻らないので設定し直す
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、秘密鍵がバックアップ以外の場所にない
  • (プッシュを有効にした場合)firebase-service-account.json の権限は 600、リポジトリや .env に内容が入っていない
  • (iPhone のプッシュを有効にした場合)AuthKey_<キー ID>.p8 の権限は 600、リポジトリや .env に内容が入っていない
  • ファイアウォール:22(可能なら運営者の IP のみ)・80・443・8443 のみ
  • 外部から https://ドメイン/admin/ が 404
  • 管理画面:テスト番号(테스트 번호)が無効、コードの配信方式(코드 전달 방식)が SMS/メール、テスト送信(테스트 발송)が成功
  • TELEGRAM_WEBHOOK_ALLOW_LOCAL を有効にしていない(ボットの Webhook が内部ネットワークへ送られないように)
  • (音声ボットを有効にした場合)リンクのシークレット・Vertex のキー・マスターキーを、それぞれ読むユーザーだけが読める(400/600)。マスターキーは /srv/pabal の外にあり、バックアップに入らない。リンクのポートは公開しておらず、リンクのネットワークはサーバーとゲートウェイだけが使う。本番サーバーに TELEGRAM_VOICE_NOTICE_CHECK・TELEGRAM_VOICE_TESTERS=* がない
  • 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 の値、テスト送信(테스트 발송)
Android に通知が届かない① ログに Push notifications are off → FCM_CREDENTIALS のパス・ファイルの権限 ② Push device registered がない → アプリが Firebase の設定なし(pabal-placeholder)でビルドされたか、新しいアプリをまだ開いていない ③ sent なのに表示されない → スマートフォンの通知の許可・バッテリーの設定、アプリを「強制停止」していないか ④ SENDER_ID_MISMATCH … Registrations are kept → サーバーの鍵とアプリの google-services.json が別々のプロジェクトのもの ⑤ answered 404 without naming the token → FCM_PROJECT_ID が誤っている ⑥ refused an access token → 鍵が無効化されたか、別のプロジェクトの鍵(5 分間は再確認しない)
iPhone に通知が届かない① ログに iPhone push notifications are off → APNS_KEY のパス・ファイルの権限・ファイル名(AuthKey_<キー ID>.p8) ② type 1 の登録がない → アプリで通知を許可していないか、シミュレーター ③ 登録はあるのに何も送られない → そのビルドが APNS_MIN_BUILD より低い(登録の行の build)、または通知をオフにした会話 ④ APNs refused the provider token → キーが無効化されたか、APNS_TEAM_ID がキーのチームと違う(5 分間は再送しない) ⑤ DeviceTokenNotForTopic … Registrations are kept → APNS_TOPIC がアプリのバンドル ID と違う ⑥ sent なのに「You have a new notification」だけが表示される → 通知拡張が復号できなかった(アプリを再インストールした場合は、一度開いて登録し直す) ⑦ 着信が通知でしか来ない → APNS_VOIP_MIN_BUILD が空か、ビルドがそれより低い(意図した動作)
管理画面に入れない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 日の上限がありますが、それ以外のリクエスト全体に対するレート制限はまだありません。公開直後は、ログとダッシュボード(대시보드)を頻繁に確認してください。
  • プッシュ通知が届くのはスマートフォンのアプリだけです — Android(手順 10)と iPhone(手順 11)。iPhone では APNS_MIN_BUILD 以上のビルドにだけ届き(Pabal 1.0 (3) は対象外)、着信の CallKit 画面は APNS_VOIP_MIN_BUILD を設定してはじめて有効になります。macOS・Windows アプリは起動している間に受け取ります。
  • 会話ごとの「通知オフ」はサーバーのメモリにしかありません。通知をオフにした会話のプッシュは、Android には急ぎでない優先度で送られ(端末が自分で除外します)、iPhone には送られませんが、サーバーを再起動すると、どちらも通常どおり送られます(アプリは設定を変えたときにだけサーバーに知らせ直します)。閉じていたアプリが起動してサーバーに接続するまで 20 秒以上かかると、発信側ではその通話が応答なしで終わることがあります。
  • メッセージの添付は写真とファイルが使え、アプリ内で再生される動画・音声メッセージはまだです。チャンネル・スーパーグループ、グループ通話、2 段階認証もまだです。
  • 認証キーは DB に平文で保存されています(サーバーが復号するために必要)。DB とバックアップを、RSA 鍵と同じくらい厳重に守ってください。

ファイル

ファイル役割
deploy/docker-compose.yml本番構成(pabal-server · pabal-postgres · pabal-caddy、バインドマウント)
deploy/CaddyfileHTTPS、管理用パスのブロック、サイト・ドキュメント・Bot API の転送
deploy/.env.example設定のひな形 → deploy/.env
deploy/backup.shDB のダンプ + 鍵・データの保存、古いバックアップの整理
Dockerfileサーバーのイメージ(ビルド → JRE 21、uid 1000)
voice/音声ゲートウェイ pabal-voice(ソース、Dockerfile、README.md、リンクの仕様 PROTOCOL.md)
deploy/docker-compose.mcp.yml音声ボットの MCP の部品の 3 つのコンテナ(プロファイル mcp)
deploy/docker-compose.voice.yml音声ボットの部品を加えるファイル — サーバーの音声の設定とリンクのネットワーク、ゲートウェイ pabal-voice(プロファイル voice)、ウォッチの音声のエッジ pabal-voice-edge(プロファイル voice-watch)
deploy/ship.sh別のコンピューターでサーバーのイメージ(--voice ならゲートウェイのイメージも)をサーバーの CPU 用に作り、SSH で読み込ませて配置ファイルをコピーする
deploy/voice_readiness.py音声を有効にする前の点検(Vertex のキー・法的な節・アプリのビルド・中継 …) — シークレットは出力しない
deploy/turnserver.conf.example、deploy/check_relay.pyMac のサーバーに直接入れる coturn の設定のひな形、中継の確認ツール