対象読者

  • 安くNextCloudを構築したい方
  • ポート解放ができないけど自宅サーバーを公開したい方
  • NextCloudでの通話環境の構築に挫折した方

概要

自宅サーバーとVPSを用いてインターネット経由の通話ができる環境を格安で構築できた。 遠方に住むFPS仲間の友人とのゲーム通話にはDiscordを使用していたが、代わりに自前の通話を試してみた。 画面共有しても遅延にシビアなFPS出の通話として問題なかったので、良い環境が構築できたのだと思う。

素のNextCloudと全部設定済みのdockerイメージAIOによる構築に失敗した末に docker-composeで通話環境を実現した。 これに2週間も要した。

  • 試行錯誤で得た設定の勘所のサマリ
  • 最終的なdocker-compose構成の具体的な設定内容
  • 泥臭い試行錯誤

の順に書く。

最終的なdocker-compose構成図は以下。

  graph TD
    Users((通話端末/PC・スマホ))

    subgraph VPS[ConoHa VPS / グローバルIP]
        direction TB
        Caddy[Caddy / Reverse Proxy]
        subgraph VPS_UDP[UDP Relay]
            TURN[coTURN / TURN Server]
        end
    end

    subgraph Home[自宅 N100ミニPC / ポート解放不可]
        direction TB
        subgraph Docker[Docker Compose]
            NC[Nextcloud App]
            Signaling[Signaling Server / HPB]
            Janus[Janus Gateway / Host Mode]
            DB[(PostgreSQL)]
            Redis[(Redis)]
        end
    end

    %% Web & Signaling Route
    Users -- "① Web & Signaling (HTTPS/WS)" --> Caddy
    Caddy -- "② Tailscale経由" --> NC
    Caddy -- "② Tailscale経由" --> Signaling

    %% Media Route (The Core of this Setup)
    Users -- "③ 通話データ (UDP)" --- TURN
    TURN -- "④ Tailscale経由" --- Janus

    %% Internal Connections
    NC --- DB
    NC --- Redis
    Signaling --- NC
    Signaling --- Janus

    classDef vps fill:#f1f8e9,stroke:#2e7d32,stroke-width:2px;
    classDef home fill:#fffde7,stroke:#fbc02d,stroke-width:2px;
    classDef focus fill:#ffebee,stroke:#c62828,stroke-width:3px;
    classDef comp fill:#f5f5f5,stroke:#616161,stroke-width:1px;

    class VPS,Caddy,TURN,VPS_UDP vps;
    class Home,Docker home;
    class Janus,TURN focus;
    class NC,Signaling,DB,Redis comp;
docker-composeによる最終構成の設定ファイル群自宅VMでは、
  • docker-compose
  • signaling server
  • janus
  • postgress

VPSでは、

  • TURNサーバー
  • caddy の設定ファイルを添付する。

自宅VM上の設定ファイル

# docker-compose.yml
services:
  db:
    image: postgres:14-alpine
    restart: always
    volumes:
      - ./db/data:/var/lib/postgresql/data
      - ./db/postgres.conf:/etc/postgresql/postgresql.conf:ro
    environment:
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=xxx
      - POSTGRES_PASSWORD=xxx
      - TZ=Asia/Tokyo
    command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
    networks:
      - nextcloud_net
  redis:
    image: redis:alpine
    restart: always
    networks:
      - nextcloud_net
  app:
    image: nextcloud:stable
    restart: always
    ports:
      - "8080:80"
    volumes:
      - ./data:/var/www/html
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=xxx
      - POSTGRES_PASSWORD=xxx
      - NEXTCLOUD_TRUSTED_DOMAINS= # ドメインとTailscaleのIP
      - OVERWRITEPROTOCOL=https
      - REDIS_HOST=redis
      - TZ=Asia/Tokyo
      - TRUSTED_PROXIES=127.0.0.1 172.16.0.0/12 192.168.0.0/16 signaling tunnel
      - OVERWRITEPROTOCOL=https
      # PHO自体のキャッシュ
      - PHP_OPCACHE_MEMORY_CONSUMPTION=128
      - PHP_OPCACHE_INTERNED_STRINGS_BUFFER=8
      - PHP_OPCACHE_MAX_ACCELERATED_FILES=10000
    depends_on:
      - db
      - redis
    networks:
      - nextcloud_net

  tunnel: # ここは図には含まれない箇所
    container_name: cloudflared
    image: cloudflare/cloudflared:latest
    restart: always
    command: tunnel run --protocol http2 --token xxx # cloudflare tunnelのtoken
    networks:
      - nextcloud_net
    depends_on:
      - app
  janus:
    image: canyan/janus-gateway:latest
    restart: always
    network_mode: "host" # 通話の大量のパケットをいちいちDockerの仮想ネットワークに送らない用にする事でパフォーマンスアップ
    volumes:
      - ./talk/janus.jcfg:/usr/local/etc/janus/janus.jcfg:ro
    environment:
      - TZ=Asia/Tokyo
  signaling:
    image: strukturag/nextcloud-spreed-signaling:latest
    restart: always
    volumes:
      - ./talk/signaling.conf:/config/server.conf:ro
    environment:
      - TZ=Asia/Tokyo
    command: /usr/bin/nextcloud-spreed-signaling --config /config/server.conf
    ports:
      - "8089:8080"
    networks:
      - nextcloud_net
    depends_on:
      - nats
      - janus
    # ホストモードのJanusを見つけるための魔法の一行
    extra_hosts:
      - "host.docker.internal:host-gateway"
  # 2. Signaling serverのための高速通信路
  nats:
    image: nats:2.10-alpine
    restart: always
    networks:
      - nextcloud_net
networks:
  nextcloud_net:
    driver: bridge
    driver_opts:
      com.docker.network.driver.mtu: "1280"  # TailscaleのMTUに合わせる 通話品質のために重要

このcomposeファイルでvolumeに指定したsingaling server と janus の設定は以下

# ./talk/signaling.conf
[http]
listen = 0.0.0.0:8080
path = /standalone-signaling

[app]
debug = false
trustedproxies = 127.0.0.1, 172.19.0.0/12, 192.168.0.0/16, xxx # TailscaleのIP

[sessions]
hashkey = xxx # 32文字のhash
blockkey = xxx # 32文字のhash

[clients]
internalsecret = xxx # 48文字のhash

[backend]
backends = backend-1
allowall = false
timeout = 10
connectionsperhost = 8
skipverify = false

[backend-1]
urls = https://xxx/ # NextCloudドメイン
secret = xxx
maxstreambitrate = 1048576
maxscreenbitrate = 2097152

[nats]
url = nats://nc-nats-1:4222


[mcu]
type = janus
url = ws://xxx:8188/janus # 自宅VMのTailscaleのIP
maxstreambitrate = 1048576
maxscreenbitrate = 2097152

[turn]
servers = turn:xxx:3478?transport=udp,turn:xxx:3478?transport=tcp # TURNサーバーがあるVPSのIP
secret = xxx # TURNサーバーの static-auth-secret
apikey = xxx # signalingサーバーがCoTURNとやり取りする際の使い捨てキーを生成するのに必要
# ./talk/janus.jcfg
general: {
    debug_level = 4
}

nat: {
    stun_server = "xxx" # TURNサーバーがあるVPSのIP ここでTURNサーバーからのパケット送信先が設定できる
    stun_port = 3478
    full_trickle = true
}
# ./db/postgresql.conf

# AIOレベルのパフォーマンス設定 VMのスペックに合わせて調整
shared_buffers = 512MB
effective_cache_size = 1536MB
work_mem = 16MB
maintenance_work_mem = 128MB
max_connections = 100
listen_addresses = '*'

VPS上の設定ファイル

# VPSの /etc/turnserver.conf

listening-port=3478
listening-ip=0.0.0.0
external-ip=xxx # VPSの固定IP
min-port=49152
max-port=65535
fingerprint
#lt-cred-mech # これと use-auth-secret は同時に使えないので注意
use-auth-secret
static-auth-secret=xxx
realm=xxx # VPSドメイン
total-quota=0
bps-capacity=0
stale-nonce=600
cert=/etc/letsencrypt/live/xxx/fullchain.pem # xxx は VPSドメイン
pkey=/etc/letsencrypt/live/xxx/privkey.pem # xxx は VPSドメイン
syslog
allow-loopback-peers
no-multicast-peers
cli-password=xxx
no-rfc5780
response-origin-only-with-rfc5780
verbose
# /etc/caddy/Caddyfile

...

xxx { # TURN server用ドメイン
    handle_path /standalone-signaling* { # /standalone-signaling/*とすると設定できなかったので注意
        reverse_proxy xxx:8089 # 自宅VM のTailscaleのIP
    }

    # それ以外のすべてのリクエストを Nextcloud 本体へ
    handle {
        reverse_proxy xxx:8080 # 自宅VM のTailscaleのIP
    }
    log {
        output file /var/log/caddy/nextcloud_access.log
    }
}

...

設定の勘所・試行錯誤まとめ

忙しい人のために最終構成に至るまでで重要だった部分をまとめる。

  • 素のNextCloudを使うべきではない。 設定ファイルを設定するのが煩雑である。dockerのNextCloudなら環境変数で簡単に設定できる。
  • NextCloudを立てる環境はLXCではなくVMにすべきだ。 TailScaleのMTUの制約に合わせやすいからだ。
  • janusはhost modeにすべきだ。 でないとdockerコンテナへの転送によるオーバーヘッドを抑制できるからだ。
  • ip_forwardをVPSと自宅VMで許可しないと通話パケットが破棄されてしまう
  • NextCloud AIOはVPSリレーには使えない。AIOは単一のVMにホストする場合にのみ使うべき。

以上が試行錯誤で重要だった点である。以下で泥臭い試行錯誤の詳細を書く。


自宅サーバー初心者、NextCloudに出会う

自宅サーバー、カッコいいよね。YouTubeで自宅鯖界隈というものがあるのを知ったニワカなのですが。

流行りの格安中華製N100ミニPC(Trigkey Green-G4)を買ってみたが持て余していた。 N100の省電力性はサーバー向きらしく、自宅サーバーの定番OSのproxmoxをインストールしてみた。 開発用VMをそこに立ててsshで利用するだけだったので、他の活用方法を考えていた。

そしてNextCloudというOSSのSelf-hostedなクラウドがある事を知った。

共同編集や画面共有、チャット、ビデオ通話ができるらしく、学習塾のオンラインプラットフォームとして利用できると思った。 普通はGoogle Workspaceなどに課金しなければならないだろう。

また親がiCloudに課金しているらしく、ストレージを買えばその代替にもできるだろう。 でもそれは今後の宿題という事でまずは高品質な通話環境の構築を目指すことにした。

ConoHa VPSをリバースプロキシとして自宅NextCloudを公開できる VPSリレー

本HPを公開するために最安最低スペックのメモリ512MBのConoHa VPSをレンタルしていた。 Ubuntuの場合、snapdなどのサービスがメモリを食うらしく最小メモリが1GBまでしかなく、Debianを選択した。

それをリバースプロキシとして自宅のNextCloudを公開できる。 これはVPSと自宅proxmox上のコンテナ(自宅VM)をTailScaleで繋ぐ事で実現できる。

NextCloud Talkを使うには4GB ~ 8GB程度のメモリが必要らしい。 ConoHa VPSだけでNextCloudを完結させようとすると月2000 ~ 4000円程度必要になる。 自宅サーバーを併用すれば、VPSは最低スペックで月額400円以下で済む。 Webページへの転送とTURNサーバーには数百MBあれば十分らしい。

なぜConoHa VPSなのかというと、株主優待が使えるから。

LXCでの素のNextCloud構築を断念

NextCloud LXCを選択

proxmoxのコンテナ構築便利コマンドはProxmox Helper Script というサイトにまとめられている。 公式のものがあるならそれを使うべきだと思う。 NextCloudに関するコマンドが載っていたのでそれを使うことにした。 VMとLXCの両方があったが、軽量でパフォーマンスが良いとされているLXCを使うことにした。

通話ではLXCよりもVMの方がいい

しかし、この判断は間違いだった。LXCではなくVMを使うべきだった。

通話ではMTU(最大転送単位)を適切に設定しないと、パケットがブツ切りになったり届かなかったりして通話品質が下がる。

LXCはproxmox本体とカーネルを共有しており、MTUを設定するにはproxmox本体の設定を変更する必要がある。

一方、VMはproxmox本体と独立しており、自由にMTUを設定できる。 TailScaleにはパケットの暗号化によるオーバーヘッドが生じるためMTUが通常のLANよりも小さいという制約がある。 これに合わせて自宅VMのMTUを変更する必要がある。

  • 通常のLANのMTU: 1500バイト
  • TailScaleのMTU: 1280バイト

こういうネットワーク設定の難しさがあるので、LXC上でdockerを使うのは避けた方がいいらしい。

NextCloudPiの設定は煩雑

まず、VPSのリバースプロキシで、NextCloud用のドメインアクセスをVPSのTailScale IPに流すように設定した。

しかし、VPSのドメイン経由でNextCloudのWeb画面が開けなかった。自宅LXCでの設定が必要だった。

NextCloudではapache2とphpの2種類の設定が要る。

  • /etc/apache2/sites-enabled/ncp.conf で VPSのTailscaleからのアクセスを許可
    • タグ内に追記
      • Require ip 100 # 100.x.x.x は TailscaleのIP
  • /var/www/nextcloud/config/config.php
    • nc-httpsonly: No
      • これでHTTPSで無限リダイレクトループが解消
        • VPSリバースプロキシはhttpで自宅NextCloudに転送(TailScaleを使っているのでhttpsは不要)
        • この設定がYesの場合に「httpsでやり直せ」と突き返して無限ループになっていた
    • nc-trust-proxies に 自宅LXCのTailscale IP と VPSのドメインを追加
    • nc-trust-domains に VPSのTailscale IPを追加

NextCloudPiとは、config.phpの設定を変更できるWeb管理画面付きのNextCloudだ。 apache2の設定変更が必要であり、管理画面だけでは完結しない。 設定を反映するためにapache2やphpを再起動する必要があってなかなか面倒だった。

dockerではapache2を意識しなくて済んだし、config.phpは環境変数で設定できる。 NextCloudの構築にはdockerをおすすめする。

Talkの通話品質がゴミで設定方法も謎 断念

NextCloudをインターネットに公開できたので、ようやくTalkの設定を始められる。

NextCloud版アプリストアみたいなものがあってそこでTalkアプリをインストールできる。 するとNextCloudのWeb画面のヘッダーにTalkのアイコンが出現する。 テスト用にアカウントを作成し、通話をしてみたら成功。チョロいな…。

と思っていたのか?(ブロリー)

Talkはできたものの、遅延ありで籠った感じの低音質で厳しいと思った。 MTU設定のせいだとかはこの時理解していなかった。

NextCloudアプリには設定画面があって、

  • 高機能バックエンド(HPB, High Performance Backend)
  • TURNサーバー

などを設定する事で通話品質が上がるらしい。きっとこの辺をうまく設定すればうまくいくのだろう。

スマホ版のNextCloud Talkアプリがあるのでそれにチャットを送ってみてもプッシュ通知が来なかった。

しかしそれらを設定済みの公式dockerイメージがある事を知った。 数日苦戦していたが解決できなかったので、そちらを試してみることにした。

NextCloud All-In-One(AIO)ではVPSリレーに対応できず断念

AIOのdockerの設定が超簡単

TalkのHPB以外にもキャッシュ(redis)なども設定済みの全部入り(All-In-One)の公式dockerイメージは AIOというらしい。 このタイミングでLXCからUbuntu ServerのVMに移行した。

なんということだ。 VPSのドメインをAIOの管理画面に入力して立ち上がりを待つだけでNextCloudがVPSからアクセスできた。 TailScaleを使うためにちょっとだけ設定が必要だったが(AIOのGitHubに説明があった)。 感動した。

AIOでもモバイル回線経由の通話に失敗

Talkアプリを試してみたが、LAN内同士なのに遅い。設定は最適化されているはずなのに。

どうやら、VPSリレーのせいで、自宅内同士の通話でも、

PC1 -> wifi -> VPS -> wifi -> 自宅VM(NextCloud) -> wifi -> VPS -> wifi -> PC2

というように遠回りしているようだ。

しかも、モバイル回線経由の通話は繋がらない。

AIOの通話の構成要素

HPBやTURNサーバーが何なのかなどの構成を理解しないと太刀打ちできなさそうだ。 以下はAIOの通話で設定できる構成要素である。

  • signalingサーバー: 高機能バックエンド(HPB)と呼ばれるもので、通話のコネクションを管理する
    • HPBはGo言語製なのでphpのNextCloudより高性能で省リソース(参考)
  • TURN: 直接通信できないネットワークでも通話できるようにパケットを中継する
    • パケット中継のおかげでパケットの遠回りを避けられる
  • STUN: 通話の発信元のIPアドレスを調べる
  • janus: 音声や映像パケットを管理し、多人数通話を負荷分散し、レスポンスを高める

こういう面倒くさい事を任せたくてAIOを使ったわけだが本末転倒である。 しかし、公式が用意した設定の正しさへの安心感を元にトラブルシュートに臨めるのは悪くない。

外部との接続に必要なVPS上のTURNサーバーの準備

LAN外で通話するには外部にTurnサーバー(coTURN)を立ててそこ経由で通話する必要があるらしい。 VPSにcoTURNというTURNサーバーを立てた。

Trickle ICE というサイトでTURNサーバーが正常に稼働しているかテストできる。 テストユーザーを作成し、無事テストができた。 NextCloud Talkでは使い捨ての認証情報を生成して認証するが、 Trickle ICE ではユーザーとパスワードで認証認証する方式なのでそこだけテスト用に設定を変える必要がある。

lt-cred-mech # ここを有効にする
#use-auth-secret        # lt-cred-mechと併用できないので無効化
#static-auth-secret=xxx # 同上

NextCloud TalkはWebRTC という通話技術を利用しており、

  • TCPの3478ポート: 通話のコネクション
  • UDPの49152-65535ポート: 通話パケット

などのポートを許可する必要がある。

ConoHa VPSでは以下のようなセキュリティグループを作成してVPSに適用した。

通信方向イーサタイププロトコルポート範囲備考
InIPv6UDP3478TURN/STUN標準ポート
InIPv4UDP3478TURN/STUN標準ポート
InIPv4TCP3478TCP経由の通信用
InIPv4UDP49152-65535重要: 通話データ(Relay)用
InIPv6TCP3478IPv6環境でのTCP通信用
OutIPv4All全範囲全開放
OutIPv6All全範囲全開放

ルーター設定(ip_forward)

それでもスマホに呼び出し通知はできるが、そこからトークルームに入ることができなかった。 VPS上のTURNサーバーのログを以下で確認する。

journalctl -u coturn -f

以下のエラーが出ていた。

... error 437: Mismatched allocation: wrong transaction ID reason: allocation timeout

さらに、VPSのip_forwardの設定が必要なようだ。 Linuxサーバーではこれを有効にしないと自分宛じゃないパケットを破棄してしまう。

  • TURNサーバーに届く通話パケットの宛先は自宅VM宛なので捨てられてしまう
  • VM上のsignalingサーバーコンテナ宛のパケットは破棄される

よって、VPSと自宅VMの両方で以下を設定する。

# 有効化の手順
sudo vi /etc/sysctl.conf # net.ipv4.ip_forward=1 のコメントアウトを外す
sudo sysctl -p # 設定の反映
sysctl net.ipv4.ip_forward # 1が返ってきたらOK

すると、allocationには成功するようになった。

AIOでは外部TURNサーバーを使用できない

だががまだエラーが続いていて、モバイル回線経由の通話ができない。

... incoming packet ALLOCATE processed, success
... peer 172.20.0.8 lifetime updated: 300
... incoming packet message processed, error 401: Unauthorized

172.20のような見慣れないIPアドレスが出ていた。 これはdockerコンテナのIPアドレスらしい。 でもVPSにdockerコンテナはない。あるのは自宅VM上だ。 なので、TURNサーバーはパケットを遅れずにUnauthorized判定を受けていたようだ。

このコンテナはnextcloud-aio-talkコンテナであり、中身はsignalingサーバーとjanusである。 TURNサーバーにjanusの場所を教えることができれば通話に成功するはず。 そのためにはjanusの設定をいじればいいようだ。

しかし、その希望は打ち砕かれた。 まさにこの問題に関するGitHub上のdiscussion を見つけた。

janusの設定(janus.jcfg)はAIOが生成しているらしく、我々が設定をすることができない。 無理やりその設定を上書きするハックで凌いでいた人がいたようだが、そのハックも通用しなくなったらしい。

つまり、AIOではjanusの設定を変更できない為、 外部TURNサーバーに自宅VMのjanusへパケットを送るように指示できない事が確定した。

オワタ。

自宅サーバーをインターネット公開できる人ならAIOを使えるが…

元々、AIOは1つのサーバー完結する単純なネットワーク構成を前提としているものなのだろう。 なので、自宅VMをインターネットに公開(ポート解放)できる人なら活用できる。

しかし、私の使っている光回線は光TV用回線であり、ポート解放ができない仕様だと判明した。 自宅wifiにはISPのプライベートIPが割り振られている。

まあ、自宅サーバーをインターネット公開するにはセキュリティを熟知している人じゃないと危険なので、 VPSリレーの方が無難ではある。

docker-composeによる構築で通話に成功した最終的な構成

結局、janusを設定できる構成でないと、VPSリレーで通話を実現できない事が確定した。 AIOの構成を手本としてdocker-composeで作るのが正解ということだ。 janusの設定でTURNサーバーから自宅VMへパケットが送れるようになって、モバイル回線での通話に成功した。 長い試行錯誤だった。

自宅VM上の設定ファイル

docker-composeの設定と、自宅VM上のjanusなどの各種設定やVPS上のTURNサーバーやリバースプロキシの設定を以下にまとめた。 リバースプロキシには当初nginxを使っていたが、設定がシンプルなcaddyに移行した。 caddyではWebSocket対応のための記述などが不要で、nginxでの記述量の1/10以下になっている。 複雑なネットワーク設定が絡むので、最初からcaddyを使うことをオススメする。

Cloudflared tunnelの導入

さらに、Cloudflared tunnelを導入し、Webサイトとsignalingサーバーを公開した。

LXC構成時のパケットの遠回りによる遅延を実感したので、 近くのEdgeサーバーを利用して少しでも遅延を減少させようと思ったからだ。

しかし、Cloudflare tunnelで扱えるのはTCPのみであり、通話パケットのUDPには対応していない。 なので、依然として通話にはTailScaleによるVPSリレーを使用しているので、通話の高速化にこれは寄与しない。 Webページの表示の高速化や共同編集などの高速化のためにこれを導入した。

これが本当の最終構成であるが、煩雑であり、通話をTalsScaleによるVPSで実現した事からは蛇足であるため、最終構成の図には載せていない。 なのでCaddyは最終的には使っていない。しかし、NextCloudの構築にはCaddyを使うべきなのは上述の通りである。

結び

以上が私のNextCloud Talkの構築の試行錯誤である。 私と同じ最終構成ではなくても要所での躓きが参考になれば幸いである。