はじめに
「CTI(電話システム)やIVR(自動音声応答)を開発してみたい。」
そう思って調べると、たいてい行き着くのは Twilio などのSaaSか、高価なPBX機材です。
もちろんSaaSは便利ですが、
「まずはロジックだけ確認したい」
「自分のサーバー内で完結させたい」
「ランニングコストをかけたくない」
という場合、いきなり契約するのは少しハードルが高いですよね。
そこで今回は、手持ちのVPSとDockerだけを使って、
「スマホからサーバーに電話をかけ、キー操作(DTMF)と録音データをPostgreSQLに叩き込む」
システムを爆速で構築しました。
外部回線(090など)は使わず、スマホをSIPクライアントとして内線接続するため、通信費も契約もゼロです。
作ったもの
スマホから電話をかけると、以下のフローで処理が進みます。
- 着信: サーバー(VPS)上のAsteriskが応答。
- IVR: 「ピ!」という発信音の後、ユーザーがキーパッドで数字(会員番号など)を入力。
- 録音: 通話内容をwavファイルとして保存。
- DB保存: 通話終了後、Pythonスクリプトが自動起動し、「発信者番号」「入力された数字」「録音ファイルパス」をPostgreSQLにINSERT。
環境
- Server: ConoHa VPS (Ubuntu 24.04)
- Container: Docker Compose
- VoIP: Asterisk (PJSIP)
- DB: PostgreSQL 15
- Client: Zoiper (iPhone/Androidアプリ)
実装のポイント
数々のハマりどころ(無音、権限、ネットワーク)を回避した「完全版」コードを紹介します。
1. ディレクトリ構成
cti_asterisk/
├── docker-compose.yml
├── Dockerfile
├── asterisk_config/
│ ├── pjsip.conf # スマホ接続設定
│ └── extensions.conf # 電話の着信フロー
├── scripts/
│ └── handler.py # DB保存用Pythonスクリプト
├── init.sql # DB初期化SQL
└── recordings/ # 録音データ保存先
2. init.sql(テーブル定義)
PostgreSQL起動時に自動実行される初期化SQLです。
CREATE TABLE IF NOT EXISTS call_logs (
id SERIAL PRIMARY KEY,
caller_id VARCHAR(50),
dtmf_input VARCHAR(50),
recording_path TEXT,
call_datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
3. docker-compose.yml
最大のポイントは network_mode: host です。
SIP(信号)やRTP(音声)はNAT越えでトラブルになりやすいため、Dockerのネットワーク分離を行わずホストのネットワークを直接使います。
※PostgreSQLもhostモードにすることで、Asteriskからlocalhostで確実に接続できるようにしています。
Note:
depends_onは起動順序を制御するだけです。初回起動時など、DBの初期化が完了する前にAsteriskが接続しようとするとエラーになる場合があります(数秒待てば繋がります)。
version: '3.8'
services:
asterisk:
build: .
container_name: cti_asterisk
network_mode: host
volumes:
- ./asterisk_config/pjsip.conf:/etc/asterisk/pjsip.conf
- ./asterisk_config/extensions.conf:/etc/asterisk/extensions.conf
- ./recordings:/var/spool/asterisk/monitor
- ./scripts:/var/lib/asterisk/agi-bin
environment:
# DB接続情報は環境変数で渡す
- DB_HOST=localhost
- DB_PORT=5432
- DB_NAME=cti_db
- DB_USER=cti_user
- DB_PASSWORD=****
restart: unless-stopped
depends_on:
- postgres
postgres:
image: postgres:15
container_name: cti_postgres
network_mode: host # ここもhostモードにしてlocalhost接続を確実にする
environment:
POSTGRES_DB: cti_db
POSTGRES_USER: cti_user
POSTGRES_PASSWORD:****
TZ: Asia/Tokyo
volumes:
- ./postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
4. Dockerfile(ハマりポイント回避)
公式の軽量イメージには**「音声ファイル」が入っていません**。これがないと、Asteriskは無音になるだけでなく、入力待機処理をスキップしてしまいます。
必ず asterisk-core-sounds-en-wav をインストールしましょう。
FROM andrius/asterisk:latest
USER root
# 1. Pythonと「音声ファイル(sounds)」をインストール
# ※これがないと無音&DTMF入力無視になります
RUN apt-get update && \
apt-get install -y python3 python3-pip asterisk-core-sounds-en-wav && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 2. DB接続用ライブラリ
RUN pip3 install psycopg2-binary --break-system-packages
# 3. ログ用ディレクトリの作成と権限設定
RUN mkdir -p /var/log/asterisk/cdr-csv && \
chown -R asterisk:asterisk /var/log/asterisk
# 4. 録音フォルダの権限調整
# ホスト側の権限設定も必要ですが、コンテナ内でもディレクトリを準備しておきます
RUN mkdir -p /var/spool/asterisk/monitor && \
chown -R asterisk:asterisk /var/spool/asterisk/monitor
USER asterisk
5. pjsip.conf(スマホ接続設定)
ここにも罠があります。スマホアプリ(Zoiper等)からのキー入力を正しく認識させるには、dtmf_mode=rfc4733 の指定が必須です。
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
; --- スマホ用 (内線100) ---
[100]
type=endpoint
context=internal
disallow=all
allow=ulaw
allow=alaw
allow=gsm
auth=100
aors=100
; 【重要】キー入力を認識させる設定
dtmf_mode=rfc4733
[100]
type=auth
auth_type=userpass
password=100pass
username=100
[100]
type=aor
max_contacts=1
; --- システム着信用 (内線200) ---
[200]
type=endpoint
context=ivr
disallow=all
allow=ulaw
allow=alaw
allow=gsm
aors=200
[200]
type=aor
contact=sip:200@127.0.0.1:5060
6. extensions.conf(着信フロー)
pjsip.conf で context=ivr と指定しているため、着信は [ivr] セクションに入ります。そこから処理の実体である [internal] へ飛ばす構成にします。
[general]
static=yes
writeprotect=no
; --- 着信の入り口 ---
[ivr]
; pjsip.confで context=ivr に設定された着信はここに来る
exten => 200,1,NoOp(=== Incoming call to IVR ===)
same => n,Goto(internal,200,1)
; --- 処理の実体 ---
[internal]
; 内線200番の処理フロー
exten => 200,1,NoOp(=== Call Start ===)
same => n,Answer()
same => n,Wait(1)
; 数字入力(beep音とともに4桁まで入力待ち。5秒タイムアウト)
same => n,Read(DTMF_INPUT,beep,4,,5)
; 録音開始(ファイル名は 日時_発信者_入力値.wav)
same => n,Set(RECORDING_FILE=${STRFTIME(${EPOCH},,%Y%m%d_%H%M%S)}_${CALLERID(num)}_${DTMF_INPUT}.wav)
same => n,MixMonitor(${RECORDING_FILE},b)
; サンキューメッセージ再生
same => n,Playback(demo-thanks)
same => n,Wait(1)
same => n,Hangup()
; 通話終了時に必ず実行される処理
exten => h,1,NoOp(=== Saving to DB ===)
same => n,AGI(handler.py,${CALLERID(num)},${DTMF_INPUT},${RECORDING_FILE})
7. scripts/handler.py(DB保存)
Asteriskから渡された引数をPostgreSQLに保存するシンプルなスクリプトです。
#!/usr/bin/env python3
import sys
import os
import psycopg2
# 環境変数から設定を取得
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_USER = os.getenv('DB_USER', 'cti_user')
DB_PASSWORD = os.getenv('DB_PASSWORD', 'cti_password')
DB_NAME = os.getenv('DB_NAME', 'cti_db')
def main():
# 引数: 1=発信者, 2=入力数字, 3=録音ファイル
caller_id = sys.argv[1] if len(sys.argv) > 1 else ''
dtmf_input = sys.argv[2] if len(sys.argv) > 2 else ''
rec_path = sys.argv[3] if len(sys.argv) > 3 else ''
try:
conn = psycopg2.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME)
cur = conn.cursor()
cur.execute(
"INSERT INTO call_logs (caller_id, dtmf_input, recording_path) VALUES (%s, %s, %s)",
(caller_id, dtmf_input, rec_path)
)
conn.commit()
cur.close()
conn.close()
except Exception as e:
sys.stderr.write(f"Error: {e}\n")
if __name__ == '__main__':
main()
動作確認手順
1. 権限設定(ハマりポイント)
Docker起動前に、ホスト側で実行権限と書き込み権限を付与しておきます。
# AGIスクリプトに実行権限を付与
chmod +x scripts/handler.py
# 録音データ保存先に書き込み権限を付与
mkdir -p recordings
chmod 777 recordings
注意:
chmod 777は開発・検証環境で手っ取り早く動作させるための設定です。本番運用時は、適切な所有者権限(uid/gid)を設定し、パーミッションを絞ることを推奨します。
2. ポート開放(超重要)
ConoHaなどのVPSでは、「セキュリティグループ(ConoHa管理画面)」と「OS内部のファイアウォール(UFW)」の両方を開ける必要があります。ここが最大のハマりポイントです。
# Ubuntuのファイアウォール(UFW)で許可
sudo ufw allow 5060/udp
sudo ufw allow 10000:20000/udp
sudo ufw reload
# 確認
sudo ufw status
※ConoHaの管理画面でも同様に UDP 5060, 10000-20000 の受信(Inbound)を許可してください。
3. クライアント設定
スマホアプリ「Zoiper」などを使います。
- ドメイン: VPSのIPアドレス
- ポート: 5060
- ユーザー: 100
- パスワード: ****
- トランスポート: UDP
4. いざ発信
アプリから「200」に発信します。
「ピ!」と鳴ったら、キーパッドで「1234」と入力してみます。
5. 結果確認
Docker内のDBを確認します。
docker exec -it cti_postgres psql -U cti_user -d cti_db -c "SELECT * FROM call_logs;"
結果:
id | caller_id | dtmf_input | recording_path | call_datetime | created_at
----+-----------+------------+----------------+-------------------------+-------------------------
1 | 100 | 1234 | .../xxx.wav | 2025-12-08 14:00:00... | 2025-12-08 14:00:00...
しっかりと入力した「1234」がDBに保存されました!
まとめ
Twilioなどの契約なしでも、VPSとDockerさえあればここまで本格的なCTIシステムが構築できました。
ハマりポイント(音声ファイルなし、DTMF設定、権限、ネットワークモード)さえ押さえておけば、構築自体は爆速です。
今回は内線(閉じたネットワーク)での実験でしたが、このAsteriskをTwilioのSIPトランクに繋げば、そのまま「050番号で外線着信できる自作留守電アプリ」に進化します。
「電話 × Web」の開発はハードルが高そうに見えますが、Dockerを使えば意外と簡単に試せるのでおすすめです。
次回の展望:Gemini APIで「電話のAI化」を加速させる
今回は「内線通話」で基礎を作りましたが、ここからが本当の楽しみです。
次回以降、このDocker環境をベースに以下のような拡張を予定しています。
1. Twilioと接続して「050番号」を持つ
現在の構成(Asterisk)に Twilio Elastic SIP Trunking を接続します。
これにより、「外から自分の050番号にかけると、サーバーのDockerが応答し、要件を聞いて録音してくれる」 という、完全自作のスマート留守電システムが完成します。
2. Google Gemini 2.5 Flashで「音声直接分析」
ここが次回の目玉です。
通常、AI留守電を作るには「Whisperで文字起こし」→「LLMで要約」という2段階の手順が必要です。
しかし、Gemini 2.5 (Flash/Pro) はマルチモーダル対応しており、**「音声ファイル(.wav)をそのまま理解」**できます。
録音されたデータを直接Gemini APIに投げるだけで、
- 「文字起こし」
- 「要件の要約」
- 「緊急度の判定(セールスか、緊急連絡か)」
をたった一度のリクエストで完了させます。
「電話システム(Asterisk) × マルチモーダルAI(Gemini)」。
この組み合わせなら、Pythonコードもシンプルになり、爆速で最強の留守電解析システムが作れます。
(Gemini連携編の記事も執筆予定ですので、ぜひフォローしてお待ちください!)

