서버 운영
서버 설치
파발 서버를 인터넷에 공개된 서버 한 대에 도커 컴포즈로 설치하고 운영하는 방법.
파발 서버를 인터넷에 공개된 서버 한 대에 설치하는 방법입니다. 도커 컴포즈로 파발 서버 · 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/에는 모든 계정과 대화가 있습니다. 백업 1순위입니다. - 회색 화살표는 저장입니다: 서버는 키·사진·설정·로그를, PostgreSQL 은 이벤트를, Caddy 는 인증서를 각자의 디렉터리에 씁니다.
준비물
| 항목 | 내용 |
|---|---|
| 서버 | Ubuntu 24.04 LTS, 시작 기준 2 vCPU · 4 GB RAM · 40 GB SSD. 사용자·메시지가 늘면 메모리부터 늘리세요 |
| 공인 IPv4 | 고정 IP. 앱이 서버 주소를 IP 로 가지고 빌드되고, 서버도 앱에 IP 로 알려 줍니다 |
| 도메인 | DNS 를 바꿀 수 있는 도메인 하나 (A 레코드) |
| 가입 코드 전달 수단 | SMS(Twilio · Solapi · 웹훅) 또는 이메일(SMTP) 계정 하나 |
| 맥 | 앱을 이 서버용으로 빌드할 맥 (윈도우 앱까지 만들려면 윈도우 PC 도 한 대) |
| Firebase 프로젝트 (선택) | 안드로이드 앱에 푸시 알림을 보낼 때. Google 계정으로 무료로 만듭니다(10단계) |
포트
| 포트 | 누가 | 열기 | 설명 |
|---|---|---|---|
| 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, 컴포즈 파일의 포트에 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
# 도커 (공식 설치 스크립트)
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 이상
도커가 여는(ports:) 포트는 ufw 규칙과 상관없이 열립니다. 그래서 컴포즈 파일은 공개할 포트(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, (선택) firebase-service-account.json(안드로이드 푸시용 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 | 계정·대화·메시지·로그인·봇·웹훅 설정 전부 | 서비스 전체 |
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"
FCM_CREDENTIALS=
FCM_PROJECT_ID=
PUSH_PREVIEWS=true
| 값 | 넣을 것 | 비고 |
|---|---|---|
DOMAIN | 웹사이트 도메인 | 앱이 만드는 링크(pabal.me/사용자명), 문서의 주소, 링크 미리보기도 이 주소 |
PUBLIC_IP | 서버의 공인 IPv4 | 클라우드라면 콘솔에 보이는 공인 IP (서버 안의 사설 IP 가 아님) |
POSTGRES_PASSWORD | 무작위 값 | 처음 켜기 전에 정하세요. PostgreSQL 은 데이터 디렉터리를 처음 만들 때만 이 값을 씁니다 |
PABAL_HOME | /srv/pabal | 3단계의 디렉터리 |
PABAL_SOURCE | .. | 서버 이미지를 빌드할 저장소. 컴포즈 파일이 저장소의 deploy/ 안에 있으면 .., 밖에 두면 그 소스 경로 |
PABAL_NETWORK | ssemiya-net | 세 컨테이너가 함께 붙는 도커 네트워크. 미리 만들어 둡니다(6단계) |
MTPROTO_PORT | 8443 | 바꾸면 방화벽과 앱 빌드도 같이 |
ADMIN_TOKEN | 비워 둠 | 비우면 처음 켤 때 pabal_server/data/admin-token 에 만들어짐. 직접 정하려면 16자 이상 |
JAVA_OPTS | 기본값 | 메모리가 모자라면 -Xmx 를 올리세요 (서버 메모리의 절반 정도까지) |
FCM_CREDENTIALS | 비워 둠 | 안드로이드 푸시 알림. 켤 때 /app/keys/firebase-service-account.json(10단계). 비우면 푸시 없이 돕니다 |
FCM_PROJECT_ID | 비워 둠 | 키 파일의 project_id 가 아닌 Firebase 프로젝트로 보낼 때만 |
PUSH_PREVIEWS | true | false 면 알림에 메시지 내용 없이 보낸 사람만 보입니다 |
운영용 컴포즈 파일은 테스트 번호를 끈 채(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 또는 이메일로. "관리 화면" 방식은 운영자가 코드를 하나하나 알려 줘야 합니다.
- SMS 또는 이메일(SMTP) 값을 넣고 저장 → 테스트 발송으로 실제로 오는지 확인.
- 테스트 번호가 꺼져 있는지 확인. 켜면 누구나
+99966…번호로 로그인할 수 있습니다. - 필요하면 새 가입 허용을 끄고 시작해 초대할 사람만 먼저 받습니다.
9. 앱을 이 서버에 연결
앱은 서버 IP · 포트 · 공개키를 몸속에 넣고 빌드됩니다. 이 서버의 공개키로 다시 빌드해야 이 서버에 붙습니다. 맥에서:
# 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/)에 데이터를 두고, 그곳에 쓸 수 없으면 진짜 텔레그램 데스크톱과 같은 데이터 폴더를 씁니다. 앱은 사용자 폴더(예: ~/Applications/Pabal/)에 두게 안내하세요. 일반 배포용 릴리스 빌드(데이터 폴더 분리, Apple 서명·공증)는 별도 작업입니다.
안드로이드·아이폰·윈도우 앱은 저장소의 docs/mobile-apps-guide.md 에 같은 방식(서버 주소·공개키를 넣는 스크립트 → 빌드)이 있습니다. 안드로이드 앱에 푸시 알림을 넣으려면 빌드하기 전에 10단계의 Firebase 앱 설정이 있어야 합니다.
10. 안드로이드 푸시 알림 (선택)
안드로이드는 화면에서 벗어난 앱을 곧 재웁니다. 푸시를 켜지 않으면 앱을 열어 둔 동안에만 메시지가 뜨고 전화가 울립니다. 푸시는 Google 의 Firebase 클라우드 메시징(FCM)으로 보내고(무료), 알림 내용(보낸 사람 이름, 메시지 미리보기)은 서버가 그 휴대폰만 가진 키로 암호화하므로 Google 은 읽지 못합니다.
다이어그램 설명
- 세 영역: 운영자가 만드는 Firebase 프로젝트 → 앱을 빌드하고 서버를 켜는 곳 → 휴대폰과 Google. Firebase 프로젝트 하나에서 파일 두 개를 받습니다.
- 회색 화살표는 한 번만 하는 준비입니다. 초록 칸
google-services.json은 앱 안에 들어가 모든 APK 에 실리므로 비밀이 아니고, 빨간 칸 서비스 계정 키는 서버의keys/에만 두는 비밀입니다. 두 파일은 같은 프로젝트에서 받아야 합니다. - 파란 화살표는 알림이 가는 길입니다: ① 앱을 처음 열면 휴대폰이 FCM 토큰과 그 휴대폰만 가진 키를 서버에 등록하고, ② 새 메시지나 전화가 생기면 서버가 알림을 그 키로 암호화해 FCM 에 넘기고, ③ FCM 이 휴대폰을 깨우면 앱이 풀어서 알림을 띄우고 서버에 다시 붙어 놓친 것을 받습니다.
- Google 이 보는 것은 기기 토큰과 보낸 시각·우선순위 같은 전달 정보뿐입니다. 보낸 사람 이름과 메시지는 암호문이라 읽지 못합니다.
- 앱을 보고 있는 휴대폰에는 푸시를 보내지 않습니다 — 이미 연결로 받고 있기 때문입니다. 알림을 끈 대화방의 푸시는 급하지 않은 우선순위로 보내고, 앱이 스스로 걸러 냅니다.
서버의 키로는 같은 Firebase 프로젝트로 빌드된 앱에만 푸시를 보낼 수 있습니다. 다른 사람이 빌드한 앱(예: Pabal.me 에서 받은 앱)은 그 사람의 Firebase 프로젝트에 묶여 있고, 애초에 그 사람 서버의 주소와 공개키로 빌드되어 있습니다. 여러분 서버의 앱은 여러분이 빌드하고, Firebase 프로젝트도 여러분이 만듭니다.
Firebase 프로젝트 만들기
- Firebase 콘솔에서 프로젝트 추가. 이름은 아무것이나 괜찮고, Google 애널리틱스는 필요 없습니다.
- 프로젝트 설정 → 일반 → 내 앱 → Android 앱 추가. 패키지 이름에
me.pabal.android(앱 빌드 스크립트가 앱에 붙이는 이름)를 넣습니다. 인증서 지문(SHA-1)은 푸시에 필요하지 않습니다. - 그 앱의
google-services.json을 내려받습니다. 앱에 넣는 설정이며 비밀이 아닙니다. - 프로젝트 설정 → 서비스 계정 → 새 비공개 키 생성으로 JSON 파일을 받아 이름을
firebase-service-account.json으로 바꿉니다. 이 파일은 비밀입니다 — 저장소·채팅·.env에 내용을 넣지 마세요. - 프로젝트 설정 → 클라우드 메시징에서 Firebase Cloud Messaging API(V1) 가 사용 설정되어 있는지 봅니다(새 프로젝트는 보통 켜져 있습니다).
앱에 앱 설정 넣기
안드로이드 앱을 빌드하기 전에 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 <안드로이드 소스> \
--host 203.0.113.10 --port 8443 --key keys/production.pem.pub
python3 scripts/android/apply_branding.py <안드로이드 소스>
# 끝에 "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 에 한 줄을 넣습니다. 컨테이너 안에서 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)으로 나가는 연결이 열려 있어야 합니다.
확인
- 휴대폰에 새로 빌드한 앱을 설치(또는 업데이트)하고 한 번 엽니다. 서버 로그에
Push device registered: user … type 2 token … (push key 256 bytes)가 찍힙니다. 토큰은 지문으로만 남습니다. - 최근 앱 목록에서 앱을 밀어 닫습니다. 설정의 "강제 종료"는 쓰지 마세요 — 안드로이드가 앱을 다시 직접 열 때까지 푸시를 전하지 않습니다.
- 다른 계정으로 메시지를 보내면 로그에
Push MESSAGE_TEXT to user …: sent가 찍히고 휴대폰에 알림이 뜹니다.
- 개인정보 처리방침: 관리 화면 → 설정 → 운영자 정보의 처리 위탁과 국외 이전에 Google LLC(Firebase 클라우드 메시징, 미국)를 적어 넣으세요. 방침 본문에는 FCM 을 거친다는 문장이 이미 있습니다. Google Play 의 "데이터 보안" 양식에도 같은 내용을 적습니다.
- 키가 새어 나갔다면 Firebase 콘솔에서 그 키를 삭제하고 새로 받아 파일을 바꾼 뒤 서버를 다시 켭니다. 앱은 다시 빌드하지 않아도 됩니다.
아이폰은 Firebase 가 아니라 Apple 개발자 계정의 APNs 키(.p8)로 따로 켭니다 — 바로 아래 11단계. 맥·윈도우 앱은 푸시 없이, 켜져 있는 동안 받습니다.
11. 아이폰 푸시 알림 (선택)
아이폰도 화면에서 벗어난 앱을 곧 멈춥니다. 푸시를 켜지 않으면 앱을 열어 둔 동안에만 메시지와 전화를 받습니다. 푸시는 Apple 푸시 알림 서비스(APNs)로 보내고, 안드로이드와 같은 방식으로 알림 내용은 그 아이폰만 가진 키로 암호화합니다. Apple 이 볼 수 있는 알림 문구는 앱 이름과 "새 알림이 있습니다" 같은 내용 없는 말뿐이고, 아이폰 앱 속 알림 확장(Notification Service Extension)이 암호문을 풀어 보낸 사람과 메시지로 바꿔 보여 줍니다.
- 등록: 앱은 켜질 때마다 계정마다 두 가지를 등록합니다 — 알림용(type 1)과 전화용(VoIP, type 9). 서버는 등록과 함께 앱의 언어(겉 문구를 쓸 말)와 빌드 번호를 기억합니다.
- 메시지는 알림으로 갑니다. 알림을 끈 대화방에는 아무것도 보내지 않습니다. 다른 기기에서 읽거나 지운 것은 소리 없는 백그라운드 푸시로 알려, 떠 있던 알림을 앱이 치웁니다.
- 전화는 VoIP 로 보내면 잠긴 화면에서도 CallKit 전화 화면이 뜹니다. 다만 VoIP 를 받은 앱은 곧바로 CallKit 에 전화를 알려야 하고, 못 하면 iOS 가 앱을 죽이고 되풀이되면 VoIP 를 더 보내 주지 않습니다. 그래서 그렇게 만든 빌드부터만
APNS_VOIP_MIN_BUILD로 켜고, 그 전에는 전화도 알림으로 갑니다.
Firebase 와 달리 앱에 넣을 설정 파일은 없습니다 — 앱은 이미 APNs 를 쓸 수 있게 빌드되어 있고(알림 권한·App Group·알림 확장), 서버에 키만 두면 됩니다. 다만 서버의 키로는 그 키를 만든 팀이 서명하고 번들 ID 가 APNS_TOPIC 과 같은 앱에만 푸시를 보낼 수 있습니다. 여러분 서버의 아이폰 앱을 여러분의 Apple 개발자 계정으로 서명했다면, APNs 키도 그 계정에서 만듭니다.
APNs 키 만들기
- Apple Developer → Certificates, IDs & Profiles → Keys → +. 앱에 서명한 팀의 계정이어야 합니다.
- 이름을 적고 Apple Push Notifications service (APNs) 에 체크합니다. 환경은 Sandbox & Production, 범위는 팀 전체(Team Scoped)로 둡니다 — 알림과 VoIP 두 곳에 보내기 때문입니다.
- 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 | 앱에 서명한 팀 ID | 파발의 앱은 4Z8R468M2D. 직접 빌드·서명한 앱이면 그 팀 |
APNS_TOPIC | 앱의 번들 ID | 파발의 앱은 me.pabal.ios. 전화(VoIP)는 여기에 .voip 를 붙인 곳으로 갑니다 |
APNS_MIN_BUILD | 4 | 이보다 낮은 빌드에는 아무것도 보내지 않습니다(0 이면 모두). 4 는 푸시를 시험하기 전에 만든 파발 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: …에 이유가 나옵니다(파일 내용은 로그에 나오지 않습니다). 안드로이드(FCM)와는 따로 켜고 끕니다 — 한쪽만 켜도 됩니다. - 컨테이너에서
api.push.apple.com(443, HTTP/2)으로 나가는 연결이 열려 있어야 합니다. Xcode 로 기기에 직접 설치한 디버그 빌드는api.sandbox.push.apple.com으로 갑니다(앱이 등록할 때 알려 줍니다).
확인
- 실제 아이폰에서 앱을 한 번 열고 알림을 허용합니다. 서버 로그에
Push device registered: user … type 1 token … (push key 256 bytes, build …)와type 9줄이 찍힙니다. 시뮬레이터로는 확인할 수 없습니다 — APNs 토큰도 CallKit 도 없습니다. - 앱을 위로 밀어 닫습니다.
- 다른 계정으로 메시지를 보내면 로그에
Push MESSAGE_TEXT to user …: sent가 찍히고 잠금 화면에 보낸 사람과 내용이 뜹니다.
- 개인정보 처리방침: 관리 화면 → 설정 → 운영자 정보의 처리 위탁과 국외 이전에 Apple Inc.(Apple 푸시 알림 서비스, 미국)를 적어 넣으세요. 방침 본문에는 APNs 를 거친다는 문장이 이미 있습니다. App Store Connect 의 앱 개인정보 설문에서 기기 ID(푸시 토큰) 항목도 확인합니다.
- 키가 새어 나갔다면 개발자 계정에서 그 키를 Revoke 하고 새로 만들어 파일을 바꾼 뒤 서버를 다시 켭니다. 앱은 다시 빌드하지 않아도 됩니다.
12. 음성 봇 (선택)
음성 봇은 사람이 앱에서 봇에게 전화를 걸면 AI 음성 모델이 받는 기능입니다(봇 주인을 위한 설명은 음성 봇). 서버에 부품 두 가지가 더 붙어야 하고, 법률 문서와 앱 빌드가 갖춰져야 켜집니다. 기본은 꺼져 있습니다.
음성 부품은 deploy/docker-compose.yml 이 아니라 덧붙이는 파일 deploy/docker-compose.voice.yml 에 있습니다 — 서버의 음성 설정과 링크 전용 망, 게이트웨이 pabal-voice(프로파일 voice), 워치 소리 입구 pabal-voice-edge(프로파일 voice-watch). .env 가 이 파일을 부르지 않으면 음성 설정 자체가 없습니다. 이 구성은 에코 엔진으로 돌려 보았지만 운영 서버에서 음성을 켠 적은 아직 없고, 음성 봇 통화를 운영 중계(TURN)로 끝까지 해 본 적도 없습니다 — 전 구간 시험은 도커 망 안의 시험용 중계로 했습니다(scripts/voice-e2e/run.sh). 켜기 전에 저장소의 deploy/voice_readiness.py 로 빠진 것을 점검할 수 있습니다. 공개하기 전의 조건(법률 검토, 제공자의 서면 답변, 앱 배포)은 운영자가 판단할 일이고, 코드는 그중 일부만 막습니다.
| 부품 | 하는 일 | 없으면 |
|---|---|---|
| pabal-voice (게이트웨이, 이미지 0.11.0) | 봇 대신 통화의 한쪽 끝이 되어 키 교환을 하고, 소리를 AI 모델과 주고받습니다. 파이썬 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 도) · 안드로이드 62151 · 아이폰 5 이상 · 애플 워치 1 이상(TELEGRAM_VOICE_MIN_BUILD_*) | 그 사람에게 통화 버튼이 없고, 걸어도 통화 중 |
(선택) 애플 워치: 서버의 워치 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)
- 리눅스 서버라면 컴포즈의
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로 보여 중계 허락이 맞지 않고, 겉으로는 멀쩡한데 소리가 모두 버려집니다. 맥 서버에서는 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 키
- Google Cloud 프로젝트에서 Agent Platform API(옛 이름 Vertex AI API,
aiplatform.googleapis.com)를 켭니다. 결제 계정이 연결돼 있어야 합니다. Gemini Developer API(AI Studio 키)는 쓰지 않습니다. - 서비스 계정을 하나 만들고 역할은
roles/aiplatform.user하나만 줍니다(더 좁히려면aiplatform.endpoints.predict권한 하나짜리 커스텀 역할). 운영용 키는 개발용과 따로 만듭니다. - JSON 키를 받아 저장소·
.env밖, 예를 들어/srv/pabal/pabal_voice/secrets/vertex-sa.json에 두고, 게이트웨이의 사용자만 읽게 합니다(리눅스:chown 10001:10001,chmod 400). 누구나 읽을 수 있으면 게이트웨이가 시작을 거부합니다. - 실시간 모델은
us-central1(미국 아이오와)에만 있습니다(2026-09-26 확인). 통화 소리가 미국으로 간다는 것을 개인정보처리방침과 확인 창이 말해야 하고, 앱의 확인 창에 적힌 지역과 게이트웨이의 지역(VOICE_VERTEX_REGION)이 같아야 서버가 그 게이트웨이에 통화를 줍니다. - 할 일: 예산 알림, 동시 세션 할당량(게이트웨이 용량의 두 배), 그리고 Vertex 의 데이터 캐시(켜져 있으면 입출력을 메모리에 최대 24시간 둡니다) — 확인 창은 "Google 은 대답을 만든 뒤 소리와 글을 보관하지 않는다"고 말하므로, 캐시를 끄기 전에는 그 말과 맞지 않습니다. 권장: 공개 전에 데이터 캐시를 끄고(저장소
docs/voice-bots/vertex-setup.md9절), 키의 역할을 예측 권한 하나로 좁히세요(같은 문서 10절).
링크와 서버 설정
링크 비밀은 서버와 게이트웨이가 같은 값을 파일로 읽습니다. 링크는 암호화되지 않으므로 두 컨테이너만 붙는 전용 망에만 둡니다 — 다른 스택과 함께 쓰는 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·다른 도커 망과 겹치면VOICE_LINK_SUBNET·VOICE_LINK_SERVER_IP·VOICE_LINK_GATEWAY_IP셋을 함께 바꿉니다. - 루프백이 아닌 주소에 묶으려면
TELEGRAM_VOICE_LINK_ANY_ADDRESS=true가 있어야 하고, 공인 주소에는 절대 묶이지 않습니다. 서버 로그가 링크가 보는 망을 알려 줍니다. - 인증 전 연결은 업그레이드 3초·hello 3초, 한 주소에서 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/ # 인텔 서버용
cd deploy && docker compose up -d # .env 의 COMPOSE_FILE·COMPOSE_PROFILES 대로
이미지는 해시로 고정한 파이썬 3.12, 컴파일러·pip 없음, uid 10001 로 돕니다. ntgcalls 는 리눅스 x86_64/aarch64 와 macOS arm64 만 있어 인텔 맥에서는 도커로만 돌립니다. 덧붙이는 파일의 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망을 하나 더 붙입니다. - Landlock: 모델이나 원음 모드를 쓰는 게이트웨이는 리눅스 커널의 Landlock(5.13 이상, 도커 기본 seccomp 에서 됨)이 없으면 시작을 거부합니다(종료 코드 2). 발신자가 보내는 것을 네이티브 코드로 다루는 미디어 작업 프로세스가 키 파일을 읽지 못하게 스스로 잠그기 때문입니다. 컨테이너 안에서 비밀 파일을
/app·/opt·/usr·/lib·/etc아래로 마운트하지 마세요(/run/secrets는 괜찮음 — 위 예처럼).VOICE_ALLOW_NO_LANDLOCK=true는 개발용 맥 전용입니다. macOS Docker Desktop 에서 Landlock 이 되는지는 확인하지 않았습니다. - 메모리: 통화 하나에 미디어 60~100 MB + 엔진 30~40 MB. 용량 8(
VOICE_CAPACITY)과 예비 한 쌍이면 약 1.3 GB. 덧붙이는 파일의 기본은 용량 2·메모리 1 GB(VOICE_MEMORY)입니다. - 안전 필터
VOICE_SAFETY_THRESHOLD의 게이트웨이 기본은LOW(가장 엄격)인데, 측정에서 멀쩡한 한국어 말 172개 중 13개를 영어 거절로 바꿨습니다(MEDIUM은 69개 중 0개). 시험자 통화는MEDIUM을 명시하는 것이 저장소의 가정이며, 기본값을 바꾸기 전에는 레드팀 시험이 필요합니다. - 한도: 게이트웨이의 기본은 한 통화 600초·40만 토큰·분당 30턴, 하루(UTC) 모델 300분·800만 토큰이고, 덧붙이는 파일은 하루를 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개(koenjazhfresidvikmar) 중 하나,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: …). 파일이 없으면 첫 키를 받을 때 서버가 만듭니다(읽기 전용 마운트면 위처럼 직접 만드세요). - 백업에 넣지 말고, 오프라인 사본을 하나 따로 둡니다. 잃으면 모든 봇 주인이 키를 다시 정해야 합니다. 교체할 때는
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)로 세 컨테이너를 더합니다.
| 컨테이너 | 하는 일 |
|---|---|
pabal-mcp (uid 1101) | MCP 클라이언트. 서버와는 소켓 파일로만 잇고, 내부 망 하나뿐이라 송신 프록시로만 바깥에 나갑니다. 읽기 전용, 권한 없음, 512 MB |
pabal-mcp-signer (uid 1102) | 서명기. 네트워크가 전혀 없고 소켓으로만 답하며, 공개 발급자 문서를 씁니다 |
pabal-mcp-egress (uid 1103) | 송신 프록시. 443 으로만, 파발 자신의 주소로는 가지 않습니다 |
docker compose -f docker-compose.yml -f docker-compose.mcp.yml --profile mcp up -d
- 켜려면
TELEGRAM_MCP_EGRESS_OWN_ADDRESSES(파발의 공인 주소들, 필수)와 키 파일들(링크 비밀,K_grant, 설치 키 — 모두/srv/pabal밖 700 폴더)이 있어야 하고, 사이트에 방침의bot-calls-mcp절과 약관의 봇 주인 절이 있어야 합니다. 한 번 할 설정은docker-compose.mcp.yml맨 위 주석에 순서대로 있습니다. 볼트와 서명 키는 백업에 넣지 않습니다. - 앱이 확인 창에 봇의 MCP 받는 곳을 보여 주게 된 뒤
TELEGRAM_MCP_APPS_READY=true를 줍니다. 그 전에는 등록만 되고 어떤 통화도 MCP 도구를 싣지 않습니다. - MCP 요청이 나갈 전용 송신 주소를 따로 두는 것은 코드의 조건이 아니라 운영 결정입니다(발신자의 말이 제3자 서버로 가는 출구이므로).
- 켜지면 로그에
MCP tools on: pabal-mcp dials …; issuer …; ports …; own addresses N; clock by NTP ….deploy/ship.sh <서버> <폴더> --mcp는 세 이미지를 만들어 싣고 override 파일을 복사할 뿐 아무것도 켜지 않습니다(리눅스 도커에서 돌려 본 적은 없음).
애플 워치 입구 (선택, 준비 중)
애플 워치 앱은 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_ADDRESS | 127.0.0.1, — | 듣는 주소. 루프백이 아니면 ANY_ADDRESS=true 가 있어야 하고, 앞에 TLS 프록시를 둡니다 |
TELEGRAM_WATCH_AUDIO_URL | — | 워치에게 알려 줄 소리 입구 주소. 경로는 정확히 wss://<호스트>/api/watch/v1/audio. 없거나 틀리면 워치 통화 꺼짐 |
TELEGRAM_WATCH_ALLOW_PLAIN | — | ws:// 주소를 허용. 개발 전용(집 안 시험) |
TELEGRAM_WATCH_SESSION_DAYS | 7 | 워치 로그인이 유지되는 날(1~365) |
TELEGRAM_WATCH_AUTH_PER_MINUTE | 30 | 서버 전체의 로그인 코드 요청 한도(분당). 주소마다의 한도는 따로 있음 |
TELEGRAM_VOICE_MIN_BUILD_WATCHOS | 1 | 확인 창 전체를 보여 주는 첫 워치 앱 빌드 |
- 게이트웨이:
VOICE_DIRECT=true와VOICE_EDGE_DIR(입구와 함께 쓰는 소켓 폴더, 기본/run/pabal-voice-edge). 워치 통화는 16 kHz 엔진(Gemini)이나 에코로만 됩니다. - 소리 입구: 같은 이미지의 두 번째 서비스로
python -m pabal_voice.edge를 돌립니다(VOICE_EDGE_HOST·VOICE_EDGE_PORT, 기본127.0.0.1·8096,VOICE_EDGE_DIR). 입구는 인터넷에서 온 것을 처음 푸는 곳이라 비밀을 하나도 붙이지 말고, 프록시와 게이트웨이 말고는 닿지 않는 내부 망에 둡니다. - 리버스 프록시는 워치 두 경로만 넘깁니다 —
/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개를 미리 잡습니다(발신자의 선택을 봇 주인이 알 수 없게). - 통화가 끝난 뒤 쓰기를 기다리는 대화 글은 메모리에만 있어, 그사이 서버를 다시 켜면 그 통화의 글은 남지 않습니다.
봇이 거는 전화 (선택, 법률 검토 대기)
음성 봇이 그 봇에게 전화를 허락한 사람에게 먼저 전화를 거는 기능입니다(음성 봇 — 봇이 거는 전화). 받는 전화보다 조건이 많고, 법률 검토를 마치기 전에는 켜지 마세요. 켜는 순서:
- 서버: 증거 키 폴더(
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(호스트의 그 폴더)로 더하세요. - 앱: 허락 창이 든 앱 빌드를 냅니다. 앱의 허락 링크 규칙은
pabal.me만 알기 때문에, 다른 서버라면 앱 빌드의 링크 규칙에 그 서버의 호스트를 넣어야 봇이 보낸 링크로 창이 열립니다(메뉴와 설정 목록은 그대로 됩니다). - 법률: 개인정보처리방침의
id="bot-calls-outbound"절과 이용약관의id="voice-bot-owners-outbound"절(봇 주인이 약관에 동의하려면voice-bot-owners절도)을 게시합니다. 초안은 저장소의docs/voice-bots/p0/에 있습니다. 허락 창 글과 발신 안내 녹음(notice-out·apology-rule)의 검토와 승인도 이때입니다. - 스위치: 관리 화면 음성 탭의 봇이 거는 전화에서 나라와 한도를 정하고 확인 문구로 켭니다(관리 화면 — 음성 탭). 봇마다 운영자의 발신 검토가 따로 있습니다.
- 게이트웨이:
VOICE_OUTBOUND=true인 게이트웨이만 봇의 전화를 겁니다. 모델 엔진이면 발신 안내 녹음 넷이 승인되어 있어야 시작하고, 초안 녹음(VOICE_OUTBOUND_DRAFT_NOTICES)은 시험 서버 전용입니다. - 운영
.env에는TELEGRAM_VOICE_CALLME_UNREVIEWED·TELEGRAM_VOICE_TEST_DIAL같은 시험·개발 전용 이름을 두지 마세요. - 봇이 거는 전화도 종단 간 암호화가 아닙니다 — 소리가 이 서버의 게이트웨이와 AI 제공자를 거칩니다.
13. 위치 메시지의 지도 그림 (선택)
위치를 보내면 안드로이드 앱과 데스크톱(맥·윈도우) 앱은 말풍선 속 지도를 스스로 그리지 않고 서버에 그 지점의 지도 그림을 달라고 합니다(upload.getWebFile). 서버는 그 그림을 Google 의 Maps Static API 에서 받아 전해 줍니다. 켜지 않으면 안드로이드는 자리표시 그림을, 데스크톱은 빈 칸에 핀만 보여 줍니다. 아이폰은 휴대폰에서 직접 그리므로 이 설정과 상관없고, 위치 보내기 자체와 위치를 누르면 열리는 큰 지도는 그대로 됩니다.
- Google Cloud 콘솔에서 결제 계정이 연결된 프로젝트를 고르고 Maps Static API 를 사용 설정합니다.
- API 키를 만들고, 애플리케이션 제한은 IP 주소(서버가 인터넷으로 나갈 때 쓰는 공인 IP), API 제한은 Maps Static API 하나로 둡니다. 이 키는 서버에만 있고 앱에는 들어가지 않습니다.
.env에 넣고docker compose up -d pabal-server로 서버를 다시 켭니다.- 로그에서 확인합니다. 꺼져 있으면
Maps in location messages are off: …와 그 이유가 나옵니다(키 값은 로그에 나오지 않습니다).
STATIC_MAPS_KEY=AIza… # 키 값은 채팅·로그·저장소에 남기지 마세요
STATIC_MAPS_PER_DAY=300 # 하루에 새로 받는 그림 수 (0 = 제한 없음)
STATIC_MAPS_PER_USER_HOUR=120 # 한 사람이 한 시간에 새로 받게 할 수 있는 그림 수
INFO TelegramServer - Maps in location messages from Google Static Maps (120 new per person an hour, 300 a day)
- 비용: Maps Static API 는 한 달 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에 내용이 들어가 있지 않음 - (아이폰 푸시를 켰다면)
AuthKey_<키 ID>.p8권한 600, 저장소·.env에 내용이 들어가 있지 않음 - 방화벽: 22(가능하면 운영자 IP 만)·80·443·8443 만
- 바깥에서
https://도메인/admin/이 404 - 관리 화면: 테스트 번호 꺼짐, 코드 전달 방식 SMS/이메일, 테스트 발송 성공
TELEGRAM_WEBHOOK_ALLOW_LOCAL을 켜지 않음 (봇 웹훅이 내부망으로 가지 못하게)- (음성 봇을 켰다면) 링크 비밀·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 값, 테스트 발송 |
| 안드로이드에 알림이 안 옴 | ① 로그에 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 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 인데 "새 알림이 있습니다"만 보임 → 알림 확장이 풀지 못함(앱을 다시 설치했다면 한 번 열어 다시 등록) ⑦ 전화가 알림으로만 옴 → 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) |
알려진 한계
- 서버 한 대 구성입니다. 켜질 때 모든 계정·대화·메시지를 메모리에 올려 둡니다. 여러 대로 나누는 수평 확장은 아직 안 되고, 데이터가 늘면 메모리를 늘려야 합니다.
- 기다리는 가입 코드, 봇의 가져가지 않은 업데이트, 쓰기를 기다리는 대화 남기기의 글은 메모리에 있어 서버를 다시 켜면 사라집니다.
- 가입 코드 요청에는 간격·하루 한도가 있지만, 그 밖의 요청 전체에 대한 속도 제한은 아직 없습니다. 공개 직후에는 로그와 대시보드를 자주 보세요.
- 푸시 알림은 휴대폰 앱에만 갑니다 — 안드로이드(10단계)와 아이폰(11단계). 아이폰은
APNS_MIN_BUILD이상 빌드에만 가고(파발 1.0 (3) 은 빠짐), 전화의 CallKit 화면은APNS_VOIP_MIN_BUILD를 정한 뒤에야 켜집니다. 맥·윈도우 앱은 켜져 있는 동안 받습니다. - 대화방별 "알림 끄기"는 서버 메모리에만 있습니다. 알림을 끈 대화방의 푸시는 안드로이드에는 급하지 않은 우선순위로 나가고(휴대폰이 스스로 걸러 냄) 아이폰에는 나가지 않다가, 서버를 다시 켜면 둘 다 평소처럼 나갑니다(앱은 설정을 바꿀 때만 서버에 다시 알려 줍니다). 꺼져 있던 앱이 깨어나 서버에 붙기까지 20초가 넘게 걸리면 전화는 거는 쪽에서 받지 않은 전화로 끝날 수 있습니다.
- 메시지 첨부는 사진과 파일이 되고, 앱 안에서 재생되는 동영상·음성 메시지는 아직입니다. 채널·슈퍼그룹, 그룹 통화, 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) |
voice/ | 음성 게이트웨이 pabal-voice (소스, Dockerfile, README.md, 링크 규격 PROTOCOL.md) |
deploy/docker-compose.mcp.yml | 음성 봇의 MCP 부품 세 컨테이너 (프로파일 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.py | 맥 서버에 직접 까는 coturn 설정 견본, 중계 확인 도구 |