メインコンテンツへスキップ
見出し画像
Photo bynacoco37

ローカルのProxmox上のJitsiを、VPS+WireGuardで公開するまでの実録ログ〜Cloudflare Tunnelで詰まり、prosody 13で沼った一日〜

    Jitsi Meet(セルフホストのビデオ会議)を立てた。ただし普通の構成じゃない。サーバーは自宅のProxmoxVE9上のLXCで、グローバルIPは持っていない。これを手持ちのVPS経由で外部公開したい、というのが今回のテーマだ。

    当初は「Cloudflare Tunnelで引っ張ればいいや」と軽く考えていた。それが大間違いの始まりで、結局その日は朝から晩まで溶けた。GPG鍵の権限、nginxのhttp2記法、prosody 13のLua非互換、設定の上書き……新しめの環境(Debian 12 / Ubuntu 24)特有のエッジケースを片っ端から踏んだ。

    この記事は、そのハマった順の実況ログだ。完成形のキレイな手順書ではない。同じ構成を組もうとしている人が、同じ罠を踏む前に「あ、これ記事で見たやつだ」となれば本望。

    対象読者は、自分でVPSやLXCを触る中〜上級者。コマンドはそのまま載せるが、なぜそうするかの判断部分を厚めに書いた。


    最終的な構成(先に結論)

    ハマりログに入る前に、たどり着いた完成形を置いておく。

    ブラウザ
       │
       ├─ Web (TCP 443) ──→ VPS:nginxリバプロ ──wg──→ LXC:nginx (Jitsi web)
       │                     (Let's Encrypt終端)
       │
       └─ メディア (UDP 10000) ──→ VPS:nftables DNAT ──wg──→ LXC:JVB:10000
    
       WireGuard:  VPS 10.66.0.1  ⇄  LXC 10.66.0.2
    • ローカル: Proxmox(ホストはUbuntu 24)上のDebian 12 LXCでJitsi稼働

    • VPS: Ubuntu 24.04、グローバルIPを持つ公開窓口

    • Web (TCP 443): VPSのnginxリバプロ → WireGuard越しにLXCへ

    • メディア (UDP 10000): VPSのnftables DNAT → WireGuard越しにLXCのJVBへ

    • DNS: jitsi01.example.com をVPSのグローバルIPに向け、Cloudflareは grey cloud(DNS only)

    • prosody: 公式リポジトリの13系(Lua 5.4で動かす)


    罠その0:そもそもCloudflare Tunnelでは無理だった

    最初の計画は「ローカルのLXCにJitsiを入れて、Cloudflare TunnelでURLを引っ張る」だった。jitsi01.example.com を予定。

    ところがここで根本的な問題にぶつかる。Jitsiの通信は2系統あって、Cloudflare Tunnelが運べるのは片方だけなのだ。

    • Web UI・シグナリング・WebSocket → HTTPS/WebSocketなのでTunnelで通る

    • 実際の映像・音声(JVB) → デフォルトで UDP 10000 を使う。CloudflareはUDPのプロキシをサポートしていない

    つまりTunnelだけで組むと、画面と会議室は開けるが、参加者同士で映像・音声が一切繋がらない。JVBのログに No socket found for ...:10000/udp が出る、あの定番のパターンに直行する。

    ここで方針転換。手持ちのVPS(グローバルIPあり)を表玄関にして、WireGuardでローカルLXCと繋ぐことにした。Cloudflare Tunnelと違ってWireGuardはUDPもそのまま流せるので、Jitsiから見れば「グローバルIPを持った普通のサーバー」として振る舞える。

    教訓: WebRTC系を自宅サーバーで公開するなら、UDPをどう通すかを最初に設計する。HTTP(S)しか通らないトンネルだとメディアで詰む。


    STEP 1:WireGuardでVPSとLXCを繋ぐ

    VPSとLXCの両方にWireGuardを入れる。LXC側からVPSへ接続を張る形にして、NAT越えする。

    VPS側 /etc/wireguard/wg0.conf

    [Interface]
    Address = 10.66.0.1/24
    ListenPort = 51820
    PrivateKey = <VPSの秘密鍵>
    
    [Peer]
    PublicKey = <LXCの公開鍵>
    AllowedIPs = 10.66.0.2/32

    LXC側 /etc/wireguard/wg0.conf

    [Interface]
    Address = 10.66.0.2/24
    PrivateKey = <LXCの秘密鍵>
    
    [Peer]
    PublicKey = <VPSの公開鍵>
    Endpoint = <VPSのグローバルIP>:51820
    AllowedIPs = 10.66.0.1/32
    PersistentKeepalive = 25

    PersistentKeepalive = 25 がNATの内側からトンネルを維持する肝。これがないと無通信時に切れる。

    両側で systemctl enable --now wg-quick@wg0 して、双方向で ping が通り、wg show に latest handshake が出れば成功。ここは一発で通った。

    Ubuntu 24.04(カーネル6.8)もProxmoxホストもWireGuard標準同梱なので、土台で困ることはなかった。非特権LXCでもtunデバイス問題に引っかからずに上がった。


    STEP 2:VPSでUDP 10000を転送(nftables DNAT)

    VPSが受けたUDP 10000を、WireGuard越しにLXCへDNATする。VPSはUbuntu 24なので、firewalldではなくnftablesで書く。

    まずVPSの外向きNIC名を確認。ip -br addr で見たら ens10 だった(eth0 ではない、ここ環境依存)。

    IP転送を有効化

    echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-jitsi.conf
    sudo sysctl --system

    /etc/nftables.conf にDNATテーブルを追記

    table ip jitsi_nat {
        chain prerouting {
            type nat hook prerouting priority dstnat; policy accept;
            iif "ens10" udp dport 10000 dnat to 10.66.0.2:10000
        }
        chain postrouting {
            type nat hook postrouting priority srcnat; policy accept;
            ip daddr 10.66.0.2 udp dport 10000 masquerade
        }
    }

    forward-port の戻り経路のためにmasqueradeが要る。適用は sudo nft -f /etc/nftables.conf。

    ポイント: NIC名は決め打ちしないこと。eth0 だと思い込んでいると、最近のUbuntuは ens10 だったりして転送が一切効かない。


    STEP 3:VPSにWebのリバースプロキシ

    Webは443で受けて、WireGuard越しにLXCのnginx(自己署名で443待ち受け)へ流す。TLSはVPS側のLet's Encryptで終端する。

    ここで 罠その1 を踏む。

    罠その1:nginxの http2 on; がUbuntu 24では使えない

    vhostを置いて nginx -t したら、これが出た。

    [emerg] unknown directive "http2" in ...:3

    http2 on; という単独ディレクティブの書き方は nginx 1.25.1以降の新記法。Ubuntu 24.04標準のnginxは1.24.0で、1バージョン届かない。

    対処は旧記法に戻すだけ。

    # NG(1.24では使えない)
    listen 443 ssl;
    http2 on;
    
    # OK(旧記法、1.24では正規の書き方)
    listen 443 ssl http2;

    「nginxを公式リポジトリ版に上げる」手もあるが、他の本番サイトも動いているVPSで、Jitsiのためだけにnginxを入れ替えるのはリスクに見合わない。旧記法で十分HTTP/2は効く。

    最終的なvhostはこんな形。WebSocket(xmpp-websocket / colibri-ws)のupgrade処理を忘れると、会議室には入れるが映像が出ない or 再接続を繰り返すので注意。

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
    
    server {
        listen 443 ssl http2;
        server_name jitsi01.example.com;
    
        ssl_certificate     /etc/letsencrypt/live/jitsi01.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/jitsi01.example.com/privkey.pem;
        include             /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;
    
        location / {
            proxy_pass https://10.66.0.2:443;
            proxy_ssl_verify    off;          # LXCは自己署名なので検証しない
            proxy_ssl_server_name on;
    
            proxy_set_header Host              jitsi01.example.com;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
    
            proxy_http_version 1.1;
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
    
            proxy_read_timeout  900s;
            proxy_send_timeout  900s;
        }
    }

    map ブロックは server の外(http直下)に置く。conf.d/ に専用ファイルで分けておくと、後でvhostだけ消したいときに巻き込まずに済む。


    STEP 4:LXCをDebian 12で作り直す(13は見送り)

    当初、LXCを**Debian 13(trixie)**で立てていた。が、ここで爆弾が。

    Jitsiが「out-of-the-boxでサポート」と明記しているのは Debian 11/12、Ubuntu 22.04以降。trixieは新しすぎて、Jitsiのstableリポジトリが依存関係(特にOpenJDKやprosody周り)を完全には想定していない可能性がある。

    apt-get install --simulate jitsi-meet で依存解決をシミュレートして勝算を見る手もあったが、「Jitsiは依存地獄にハマると数時間溶ける」と分かっていたので、実績のあるDebian 12で作り直すことにした。

    WireGuardの鍵を作り直して公開鍵をVPS側に貼り替えるだけなので、トンネルは数分で復活。VPS側は一切変更不要だった(VPSは 10.66.0.2 しか見ていないので、LXCのLAN IPが何だろうと無関係)。

    教訓: 母艦の安定性は最新さより優先。特にJitsiみたいに依存コンポーネントが多いものは、枯れたディストリに乗せた方が結局速い。


    STEP 5:Jitsi本体のインストール、ここから怒涛のハマり

    Debian 12のLXCで、リポジトリ追加 → nginx → apt install jitsi-meet という王道の流れ。対話プロンプトでhostnameは jitsi01.example.com、SSLは自己署名を選択(本物の証明書はVPS側が持つので)。Let's Encryptスクリプトは実行しない(LXCはNAT内側でHTTP-01チャレンジを受けられない)。

    ……が、その前のリポジトリ追加で早速詰まる。

    罠その2:GPG鍵の権限が600で _apt が読めない

    Jitsiのリポジトリを追加して apt update したら、これ。

    NO_PUBKEY B4D2D216F1FD7806
    E: リポジトリ ... は署名されていません。

    鍵は正しく登録したはず。gpg --show-keys でも鍵は見える。.list の signed-by= のパスも正常。なのに弾かれる。この矛盾でしばらく悩んだ。

    正体は権限だった。

    $ ls -l /usr/share/keyrings/jitsi-keyring.gpg
    -rw------- 1 root root 2241 ...   # ← 600

    gpg --dearmor した時、WireGuardの鍵生成で設定した umask 077 が効いていて、鍵ファイルが600で作られていた。apt のリポジトリ署名検証は _apt という非rootユーザーで走るので、root専用(600)の鍵を読めない。これが「rootの gpg --show-keys では見えるのに apt update(_aptユーザー)では NO_PUBKEY」という矛盾の真相。

    対処は権限を付けるだけ。

    chmod 644 /usr/share/keyrings/jitsi-keyring.gpg
    apt update   # 通った

    教訓: umask 077 の作業セッションでGPG鍵を作ると _apt が読めずにハマる。keyring系の鍵は644であることを確認する。この後prosodyやnginxの鍵を入れる時も、最初から chmod 644 を織り込んだ。

    本体のインストール自体は、Debian 12なので依存で詰まることなく完了。4サービス(jitsi-videobridge2 / jicofo / prosody / nginx)が active (running) になった。

    ただし、prosodyのログにこれが出ていた。

    Error initializing module 'speakerstats_component' ... no file '/usr/lib/prosody/prosody/util/queue.lua'
    Error initializing module 'room_metadata_component' ... no file '/usr/lib/prosody/prosody/util/datetime.lua'

    この時点では「補助機能だし後回しでいいか」と判断して、先に進んだ。これが後の大沼の入り口だった。


    STEP 6:ひとまず会議が通った(メディア成功)

    NAT_HARVESTERを設定して、JVBに「公開IPはVPSだよ」と教える。

    /etc/jitsi/videobridge/sip-communicator.properties

    org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS=192.168.99.255   # LXCのLAN IP
    org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS=116.80.999.255  # VPSのグローバルIP

    これでJVBが VPS_IP:10000 をICE候補として広告 → ブラウザはVPS:10000へUDP → VPSがwg越しにLXCへ転送、という経路が成立する。

    DNSは jitsi01.example.com をVPSのグローバルIPに向け、Cloudflareは**grey cloud(DNS only)**に。ここをオレンジ(プロキシ)のままにすると、結局UDP 10000がCloudflareで止まって振り出しに戻るので注意。

    そしてスマホ(5G=完全な外部経路)とPCで会議室に入ったら、映像・音声とも通った。一番の懸念だったメディア経路(UDP 10000 → WireGuard → JVB)がクリアできた瞬間。Cloudflare Tunnelでは絶対に越えられなかったUDPの壁を、VPS + WireGuardで突破できた。

    ここで終われば平和だった。だが「prosodyのモジュールエラーを消したい」と欲を出したのが運の尽き。


    STEP 7:prosodyの沼(ここが本編)

    prosodyのモジュールエラーの原因は、Debian標準のprosody 0.12.3がJitsiのモジュールと噛み合っていないこと。Jitsi公式は「prosody公式リポジトリから新しい版を入れる」ことを前提にしているし、最近のJitsiはprosody 13系を前提にしている(Debian標準の0.12のままだと逆に噛み合わない)。

    なのでprosody公式リポジトリを追加して入れ替える。バックアップは取っておいた(これが後で効く)。

    cp -r /etc/prosody /etc/prosody.bak.$(date +%Y%m%d)

    沼1:候補が13.0.5だった(メジャー跳躍)

    リポジトリ追加後に apt-cache policy prosody を見たら、候補が 13.0.5-1~bookworm1。予想していた「0.12系の最新」ではなく、0.12 → 13.0 へのメジャー跳躍だった(prosodyは最近 0.12 の次を「13.0」とするバージョニングに変わった)。

    「新しすぎて今度は逆にJitsiが追いつかないのでは」と一瞬怯んだが、調べたらJitsi側が「apt から常に最低でもprosody 13を入れる」方針に変わっていて、13.0.4向けのパッチもJitsi側で取り込み済み。13で正解と確認できた。

    沼2:prosodyが起動しない(Lua 5.1非互換)

    apt install prosody で13.0.5に上げて再起動したら、起動失敗。

    $ prosodyctl check config
    Prosody is no longer compatible with Lua 5.1

    prosody 13はLua 5.1と非互換。prosody の起動shebangが #!/usr/bin/env lua で、env lua が拾う /usr/bin/lua が lua5.1 を指していたのが原因。

    $ readlink -f /usr/bin/lua
    /usr/bin/lua5.1                       # ← これが元凶
    $ ls -l /etc/alternatives/lua-interpreter
    ... -> /usr/bin/lua5.1

    lua5.4 は入っているのに、alternatives経由のリンクが5.1を向いていた。対処は実体リンクを5.4へ張り替えるだけ。

    ln -sf /usr/bin/lua5.4 /etc/alternatives/lua-interpreter
    lua -v   # → Lua 5.4.4 になればOK

    これでprosodyは起動し、prosodyctl check config も All checks passed, congratulations! になった。やれやれ、と思った。

    沼3:会議が「あなたは切断されました」になる

    prosody入れ替え後、jicofo / jvb を再起動して4サービス active。なのにブラウザで会議に入ると*「あなたは切断されました」*。

    prosodyのログは無音(起動以降ログが出ない)。これは「prosodyの中で失敗」ではなく「リクエストがprosodyまで届いていない」サイン。jicofoのログを見たら、犯人が分かった。

    host-unknown
    This server does not serve auth.jitsi01.example.com

    jicofoは focus@auth.jitsi01.example.com でprosodyにログインしようとするのに、prosody側に auth.jitsi01.example.com のVirtualHostが存在しないと言われている。

    沼4:設定の Include 行が消えていた(真犯人)

    調べていくと、Jitsiのvhost定義(auth.jitsi01... を含む)は /etc/prosody/conf.avail/jitsi01.example.com.cfg.lua にちゃんと残っていた。conf.d/ のシンボリックリンクも無傷。

    問題はメイン設定。

    $ grep -n 'Include' /etc/prosody/prosody.cfg.lua
    (何も出ない)

    apt install prosody がメイン設定 prosody.cfg.lua を13系の新デフォルトで上書きし、その新デフォルトに Include "conf.d/*.cfg.lua" が無かった。だからprosodyは conf.d/ を一切読まず、せっかくリンクされたJitsiのvhostを完全に無視していた。これが host-unknown の真相。

    対処は1行足すだけ。

    echo 'Include "conf.d/*.cfg.lua"' >> /etc/prosody/prosody.cfg.lua

    prosodyctl check config のロード一覧に conf.d/jitsi01.example.com.cfg.lua が現れ、再起動後のjicofoログから host-unknown が消えて、JVBの登録に成功。

    Added new videobridge: Bridge[jid=jvbbrewery@internal.auth.jitsi01.example.com/...]
    BaseBrewery.start: Joined the room.

    PCとスマホで再テストして、映像・音声とも復活。prosody 13移行という大手術を、4つの沼(メジャー跳躍の判断 → Lua 5.1 → 切断 → Include漏れ)を抜けてようやく乗り越えた。

    教訓:

    prosodyのメジャーアップグレードは設定ファイルを上書きしうる。` apt install` 中の上書き確認は安易にYを押さない。 入れ替え前にバックアップ。 prosodyctl check config のロード一覧で「自分のvhostが読まれているか」を必ず確認する。

    STEP 8:表玄関を締める(ufw)

    VPSは表玄関。メディアが通って満足していたが、VPSは全開放状態だった(ufwのルールは登録済みだが本体が非アクティブ)。ここを締める。

    ufw有効化の2大事故は「自分が締め出される」「Jitsiのメディアが止まる」。順番を守る。

    1. SSHポートの確認(最重要)。今回はSSHが 44444 だった。これがallowに入っていることを確認してから有効化。22ではなく44444で繋いでいるので、ここを外すと即締め出し。

    2. forwardポリシーをACCEPTに。ufwを有効化するとforwardがデフォルトDROPになり、UDP 10000転送が止まる。/etc/default/ufw の DEFAULT_FORWARD_POLICY="ACCEPT" を確認。

    3. 必要ポートが揃っているか: SSH(44444)、80/443、51820/udp(WireGuard)、10000/udp(メディア)、既存サービス(25/465等)。

    有効化する瞬間は、今のSSHセッションを閉じず、別ウィンドウで新規SSHが通るか試すのが鉄則。弾かれても生きているセッションで ufw disable すれば戻せる。

    そして見落としがちなのが、ufwとnftablesの共存。ufwもnftablesを使うので、独自の jitsi_nat テーブルが生き残っているか確認する。

    sudo nft list table ip jitsi_nat   # DNAT/masqueradeが残っていればOK

    幸い、ufw有効化後も jitsi_nat は生存していた。会議の映像・音声も通る。表玄関が締まった。


    STEP 9:再起動耐性(理論上クリア)

    最後に、VPS再起動でこの構成が自動復活するかの確認。チェックは3点。

    systemctl is-enabled nftables        # enabled(設定を自動ロード)
    grep -A8 'jitsi_nat' /etc/nftables.conf  # テーブルが永続化されているか
    systemctl is-enabled wg-quick@wg0    # enabled(WireGuard自動起動)

    3点とも揃っていれば、理論上は再起動しても自動復活する。ただしufwとnftables.serviceの起動順序の綾で、再起動後に片方が消える可能性はゼロではない(/etc/nftables.conf 冒頭の flush ruleset がufwのルールを消す、等)。なので最終的には実際に再起動して検証するのが確実。

    ただVPSは他の本番サービス(メール等)も乗っているので、再起動は影響の少ないタイミングで。設定上は揃っているので、検証は「念のための最終確認」という位置づけ。

    もし再起動後に jitsi_nat が消えていたら、sudo nft -f /etc/nftables.conf で即再投入できる。


    まとめ:踏んだ罠カタログ

    一日かけて踏んだ罠を並べておく。どれも「新しめの環境で、新旧バージョン差のエッジケースを踏む」という共通パターンだった。

    1. Cloudflare TunnelはUDP非対応 → WebRTCのメディアが通らない。VPS+WireGuardに方針転換

    2. GPG鍵の権限 600→644 → umask 077 のせいで _apt が読めず NO_PUBKEY

    3. nginxのhttp2記法 → Ubuntu 24の1.24では http2 on; が使えず、旧記法 listen 443 ssl http2; が必要

    4. prosody 13 + Lua 5.1非互換 → env lua がalternatives経由で5.1を掴んでいた。/etc/alternatives/lua-interpreter を5.4へ

    5. prosody設定のInclude漏れ → apt が prosody.cfg.lua を上書きし Include "conf.d/*.cfg.lua" が消えて host-unknown

    特にprosody 13まわり(4と5)は、Jitsiの自動インストールが内部でやっていることを理解していないと、ログだけ見ても何が起きているか分かりにくい。host-unknown → 「vhostが読まれていない」 → 「Include行が消えている」とたどり着くまでが今回の山場だった。

    完成形だけ見ればシンプルな構成だが、そこに至る道には地雷がそこそこ埋まっている。同じことをやる人の地雷探知機になれば幸いです。


    (この記事はその日の作業ログをもとに、時系列で再構成したものです。IPアドレスやドメインは実環境のものを一部伏せ字・改変しています。)

    終わりに

    普段は X で、Claude Code や VPS 運用、個人開発の試行錯誤について発信しています。よかったらフォローしてください 👇

    X: @kumapress1

    質問や「こんなハマり方したよ」みたいな話もお待ちしてます。

     
     
    まもなく還暦な子供3人の現役プログラマーです。学生時代は紙テープでプログラム作ってました。。もうほとんど博物館ww長く一人で法人をやってますんで、いまは、インフラからWebサービス、アプリ開発まで一人で対応しています! ここでは生活の中でハマったことなどを投稿させていただきます!

    あなたへのおすすめ