1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【爆速】Twilio契約不要!Docker×Asteriskで「スマホからサーバーに電話して録音&DB保存」するCTIを自作してみた

Posted at

はじめに

「CTI(電話システム)やIVR(自動音声応答)を開発してみたい。」
そう思って調べると、たいてい行き着くのは Twilio などのSaaSか、高価なPBX機材です。

もちろんSaaSは便利ですが、
まずはロジックだけ確認したい
自分のサーバー内で完結させたい
ランニングコストをかけたくない
という場合、いきなり契約するのは少しハードルが高いですよね。

そこで今回は、手持ちのVPSとDockerだけを使って、
「スマホからサーバーに電話をかけ、キー操作(DTMF)と録音データをPostgreSQLに叩き込む」
システムを爆速で構築しました。

外部回線(090など)は使わず、スマホをSIPクライアントとして内線接続するため、通信費も契約もゼロです。

作ったもの

スマホから電話をかけると、以下のフローで処理が進みます。

  1. 着信: サーバー(VPS)上のAsteriskが応答。
  2. IVR: 「ピ!」という発信音の後、ユーザーがキーパッドで数字(会員番号など)を入力。
  3. 録音: 通話内容をwavファイルとして保存。
  4. 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.confcontext=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」と入力してみます。

IMG_0196.png

IMG_0198.png

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連携編の記事も執筆予定ですので、ぜひフォローしてお待ちください!)

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?