記事一覧へ戻る
TRANSMISSION / CYAN読了 35 分

StunDeck 実践:fnOS FPK、Docker、Cloudflare 302

fnOS に StunDeck FPK を手動導入する方法と Docker 導入を解説し、Cloudflare の最小権限 Token、プロキシ DNS、Single Redirect で固定ドメインを動的 STUN 公開先へ追従させます。

#StunDeck#fnOS#FPK#Docker#STUN#UPnP#Cloudflare#302#Homelab

解説動画 / 16:9

StunDeck 完全ガイド:Docker、Cloudflare 302/307、STUN 診断

中国語ナレーションと中国語字幕付きの約 6 分の操作解説です。導入、初期設定、最小権限 Token、Redirect サービス、UPnP/NAT-PMP、診断、外部回線検証までを扱います。

6:15中国語ナレーション10 チャプター

再生ボタンから完全版をご覧いただけます。シーク、ピクチャーインピクチャー、全画面表示に対応しています。

この記事の中心は、STUN を単独で動かすことではありません。利用者は常に一つの固定ドメインへアクセスし、StunDeck が公開 IP または動的ポートの変化を検出したら Cloudflare Single Redirect を更新します。Cloudflare は 302(または 307)を返し、クライアントは現在の公開マッピングへ移動します。

導入は Docker から始めますが、順番が重要です。StunDeck の導入 → Cloudflare Token の作成 → Cloudflare 接続の保存 → Redirect サービスの作成 → STUN/UPnP の起動 → 第一段と第二段の検証の順に進めます。サービス作成前に Cloudflare を設定しないと、接続を選択できず、自動同期も検証できません。

実測結果は次のとおりです。

  • Debian 13.6、amd64;
  • Docker Engine 29.7.1、Docker Compose 5.3.1;
  • StunDeck イメージは公開コミット bcb5a1a に対応;
  • Docker サービスに常駐プロキシはなく、コンテナ内の proxy 変数もすべて空;
  • NATMap がマッピングを取得し、UPnP が第一段の待受ポートを開放;
  • 別回線の独立ホストから動的エンドポイントへ接続し HTTP 200 を確認し、固定入口からは HTTP 302 と Location を確認;
  • 実際の認証情報、Token、LAN アドレス、公開 IP、動的ポートは本文と画像に掲載していません。

STUN は中継ではありません。対称型 NAT や制限の強い CGNAT を公開入口へ変換することはできません。この記事は外部回線テストまで成功した環境の記録であり、すべての回線で同じ結果を保証するものではありません。

fnOS に検証済み FPK をインストールする

ストア状況(2026-08-26):StunDeck は fnOS アプリセンターの公開審査へ提出済みです。承認されて一般公開されるまでは、ここで提供する正式版 FPK を手動インストールできます。fnOS のパッケージ形式は FPK であり、Android APK ではありません。

現在の正式版は linux/amd64linux/arm64 に対応する StunDeck v0.1.1 です。このブログに検証済みのコピーを置き、GitHub Release も予備の入手先として案内します。

インストール手順:

  1. FPK をダウンロードし、ローカルで SHA-256 を照合します。
  2. fnOS の「アプリセンター → 設定 → アプリを手動インストール」を開きます。
  3. stundeck-fpk-0.1.1.fpk を選択してタイムゾーンを設定します。Secure Cookie は HTTPS リバースプロキシ経由で利用する場合だけ有効にします。
  4. 初回インストール時に 20000–59999 から未使用ポートが一つ選ばれて保存され、アップグレードや再起動で変更されません。
  5. fnOS デスクトップから StunDeck を開き、管理者を作成してから Cloudflare Token、マッピングサービス、Webhook を追加します。

FPK は不変タグのイメージ ghcr.io/nciae-zyh/stundeck:v0.1.1 を取得するため、初回インストール時に NAS から GHCR へ接続できる必要があります。Cloudflare Token、Webhook Secret などの秘密情報がインストーラーへ同梱されたり、収集されたりすることはありません。

設定と永続化ディレクトリは fnOS が管理し、実データは ${TRIM_PKGVAR}/data に保存されます。アップグレード、移行、再インストールの前に stundeck.dbmaster.key必ず一組でバックアップしてください。master.key がなければ、データベース内の暗号化された Cloudflare Token、Webhook Secret、TOTP Secret は復元できません。アンインストール時は標準でデータを保持し、明示的に「完全削除」を選んだ場合だけ削除します。

まず中心となる通信経路を理解する

クライアントは常に固定ドメインへアクセス
  └─ Cloudflare プロキシ DNS(オレンジ色の雲)
       └─ Single Redirect が 302 / 307 + Location を返す
            └─ 現在の公開 IP:動的ポート
                 └─ ルーターの UPnP / NAT-PMP / 手動転送
                      └─ StunDeck + NATMap
                           └─ LAN 内ターゲット

固定されるのは入口ドメインであり、公開エンドポイントではありません。Cloudflare が処理するのは第一段だけです。第二段には Tunnel、中継、WAF、Access は適用されず、ブラウザは Location にある公開 IP と動的ポートへ直接接続します。

成功条件は二つに分けて確認します。

  1. 固定ドメインが正しい 302/307 を返し、DNS と Single Redirect が同期していること;
  2. Location をたどった先が正常応答し、STUN、ゲートウェイ開放、ファイアウォール、LAN 内サービスが機能していること。

第一段だけ成功しても、サービスが外部から利用可能とは限りません。

1. 前提条件

  • Debian 13/12 または Docker が公式サポートする Linux;
  • amd64 または arm64;
  • 識別可能な LAN アドレス;
  • LAN 内ターゲットへ接続できること;
  • 必要に応じて UPnP または手動ポート転送を設定できるルーター;
  • 機密データを持たず、すぐ停止できるテストサービス。

Lucky の公式 STUN ドキュメントは NAT1 を対象とし、LAN ホストでは UPnP、ポート転送、DMZ などが追加で必要だと説明しています。NAT タイプの表示名は製品ごとに違うため、ログと実際の外部回線テストを最終根拠にします。

NAS、ルーター、StunDeck 自体の管理画面を最初の公開テストに使わないでください。静的ページや一時 HTTP サービスから始めます。

2. Docker Engine と Compose を導入する

以下は Docker のDebian 公式手順に沿っています。Compose は旧式の単体バイナリではなく docker-compose-plugin を使います。

sudo apt update
sudo apt install -y ca-certificates curl

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<'EOF'
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: trixie
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install -y \
  docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

環境が異なる場合は確認して trixieamd64 を変更します。

. /etc/os-release && echo "$VERSION_CODENAME"
dpkg --print-architecture

ダウンロード用プロキシは一時的に使う

今回の作業では Docker の GPG URL への直結が一度リセットされました。インストール時だけ使うプロキシを /etc/environment や Docker の systemd サービスへ残してはいけません。

PROXY_URL=http://192.168.50.9:7890

sudo curl --proxy "$PROXY_URL" -fsSL \
  https://download.docker.com/linux/debian/gpg \
  -o /etc/apt/keyrings/docker.asc

sudo env http_proxy="$PROXY_URL" https_proxy="$PROXY_URL" apt update
unset PROXY_URL

公式リポジトリが不安定な場合は、Docker 公式手順の “Install from a package” を使い、対応する .deb を検証してローカルインストールします。不明なミラーのバイナリは使いません。

sudo docker version
sudo docker compose version
sudo systemctl is-enabled docker
sudo systemctl is-active docker
sudo systemctl show docker -p Environment

今回の最終出力は Environment= でした。コンテナ側は後でもう一度独立して確認します。

実際のコマンド出力を基に作成しました。ホスト名、ログイン情報、LAN アドレス、一時ダウンロードプロキシは削除済みです。

3. Compose ファイルを作る

sudo install -d -m 0755 /opt/stundeck
cd /opt/stundeck

/opt/stundeck/compose.yaml

services:
  stundeck:
    image: ghcr.io/nciae-zyh/stundeck:latest
    network_mode: host
    restart: unless-stopped
    read_only: true
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:size=16m,mode=1777
    volumes:
      - stundeck-data:/var/lib/stundeck
    environment:
      STUNDECK_LISTEN: 0.0.0.0:8080
      STUNDECK_DATA_DIR: /var/lib/stundeck
      STUNDECK_SECURE_COOKIES: "false"
      TZ: Asia/Shanghai
      HTTP_PROXY: ""
      HTTPS_PROXY: ""
      ALL_PROXY: ""
      http_proxy: ""
      https_proxy: ""
      all_proxy: ""

volumes:
  stundeck-data:

network_mode: host により NATMap と UPnP が実際の Linux ネットワークスタックを利用できます。コンテナは read-only、全 capability 削除、no-new-privileges 有効で、永続化対象は /var/lib/stundeck だけです。proxy 変数は明示的に空へ固定します。

初期 LAN HTTP では STUNDECK_SECURE_COOKIES: "false"、HTTPS リバースプロキシ配下では "true" にします。Docker Desktop を正式な STUN ゲートウェイにせず、privileged: true も追加しません。

4. 起動と確認

cd /opt/stundeck
sudo docker compose config --quiet
sudo docker compose pull
sudo docker compose up -d --no-build
sudo docker compose ps

今回、約 10 秒で healthy になりました。

curl -fsS http://127.0.0.1:8080/api/v1/health

sudo docker inspect stundeck-stundeck-1 \
  --format '{{range .Config.Env}}{{println .}}{{end}}' \
  | grep -i proxy

health API は status: ok を返す必要があります。2 番目のコマンドに 6 個の proxy 変数が表示されても、値はすべて空でなければなりません。イメージ取得時の一時 daemon proxy は StunDeck 起動前に削除し、Docker を再起動します。

実環境で healthy、health OK、コンテナ内 proxy 値なしを確認しました。ホストとコンテナの識別情報は削除しています。

5. 初回セキュリティ設定

LAN から http://LAN_SERVER_IP:8080 を開きます。

  1. 一意の管理者名と 12 文字以上の強いパスワードを作る;
  2. アクセスモードは「LAN」を選ぶ;
  3. リバースプロキシ名が決まっている場合だけ許可 Host を設定;
  4. Security 画面で Host allowlist と認証アプリ 2FA を設定;
  5. 8080 の管理画面を STUN で直接公開しない。

使い捨ての空データ環境で撮影した実画面です。ユーザー名とパスワードは入力していません。

stun-lab-20260802.sparkles-editor.com は公開許可済みの Host 例です。使い捨て環境へ入力しただけで保存していません。DNS レコード、Zone/Rule ID、Token、TOTP secret、管理者情報は含まれません。

遠隔管理には Cloudflare Tunnel、VPN、または認証付き HTTPS リバースプロキシを優先します。

6. Cloudflare の最小権限 API Token を先に作る

Single Redirect は Cloudflare を通過する通信だけを処理します。入口ドメインは対象 Zone に存在し、**プロキシ DNS(オレンジ色の雲)**である必要があります。DNS 値、Zone ID、Ruleset ID、他のドメインは画像へ掲載しません。

Cloudflare のユーザーメニューから プロフィール → API Token を開き、「Token を作成」を選びます。

「カスタム Token を作成」を選びます。Global API Key は使いません。

StunDeck Redirect · sparkles-editor.com のような識別しやすい名前を付け、Zone 権限を次の 3 件だけ追加します。

ResourcePermissionLevel用途
ZoneZoneRead対象 Zone の検索と検証
ZoneDNSEditプロキシ DNS レコードの作成・更新
ZoneSingle RedirectEdit302/307 Rule の作成・更新

Zone Resources は Include → Specific zone → sparkles-editor.com に限定します。すべての Zone を許可しないでください。自動 DNS を使わない場合は DNS Edit を外し、入口レコードを自分でプロキシ状態に保ちます。

Client IP フィルターは管理側の出口が固定の場合だけ使います。家庭回線の現在の公開 IP を固定すると、変更後に StunDeck が同期できなくなることがあります。運用周期に合わせて有効期限も設定できます。

「概要を表示する」を押し、対象 Zone に次の権限だけがあることを確認します。

最後に「Token を作成」を押します。平文は一度しか表示されません。パスワードマネージャーへ保存して StunDeck に貼り付け、Compose、Markdown、Git、ログ、スクリーンショット、チャットへ残さないでください。不要な長期 Credential を作らないため、この記事の画像は確認画面までで、Token 値は作成・表示していません。

7. StunDeck に Cloudflare 接続を保存して検査する

サービスを作る前に StunDeck の Cloudflare ページを開きます。

  1. 識別しやすい接続名を入力;
  2. 新しい API Token を貼り付け;
  3. 「権限を検査」を実行;
  4. Zone、DNS、Single Redirect がすべて成功してから保存。

再構築後の実画面です。Token、アカウント情報、DNS 値、Resource ID は含まれません。

Credential はローカル master key で暗号化されます。復元可能なバックアップには SQLite と master.key の両方が必要です。

8. 302 を中心とした Redirect サービスを作る

停止可能な HTTP テストサービスを次のように設定します。

名前:Home NAS(例)
LAN ターゲット:192.168.1.20(例)
ターゲットポート:80(例)
プロトコル:TCP
待受ポート:0(自動)
ルーター開放:UPnP(推奨)
公開方式:Cloudflare Redirect
Cloudflare 接続:前の手順で保存した接続
入口ドメイン:stun-lab-20260802.sparkles-editor.com
ターゲット方式:HTTP
Redirect status:302 Temporary
DNS 自動管理:有効
Path を保持:有効
Query を保持:有効

実画面の文書用フォームです。LAN アドレスとポートはプレースホルダーで、Cloudflare 接続は保存せず、フォームも送信していません。上部の実サービスは裁切しています。

302 と 307 の選び方

StatusClient の動作推奨用途
302 Temporary一時 Redirect。一部 Client は POST を GET へ変更Web ページ、Download、GET 中心のサービス。本稿の既定値
307 Temporary RedirectHTTP method と request body を保持API、Webhook、Upload、POST の意味を維持するサービス

どちらも一時 Redirect です。変動する STUN 先に 301/308 は使いません。

Path と Query の保持を有効にすると /files?a=1 が現在のターゲットへ引き継がれます。HTTPS ターゲットには証明書がカバーする origin hostname も必要です。公開 IP へ直接 HTTPS Redirect すると通常は証明書検証に失敗します。

DNS 自動管理は StunDeck 自身が作成し、管理マークを付けたレコードだけを更新します。同名の非管理レコードがある場合、上書きせず同期を停止します。

9. サービスを開始して STUN / UPnP を診断する

正常な遷移は次のとおりです。

停止 → 探索中 → ルーター開放済み → Cloudflare 同期済み

StunDeck はトラバーサル待受を同じローカル待受ポートへマッピングし、NATMap が検出した公開 IP とポートを Single Redirect のターゲットへ書き込みます。

  • Docker daemon とコンテナの proxy が空;
  • NATMap が実行可能;
  • LAN ターゲットへ到達可能;
  • 選択したプロトコルの Binding が成功;
  • 正しい UPnP IGD または NAT-PMP gateway を検出;
  • ルーター mapping が作成済み;
  • 公開 mapping の更新時刻が新しい;
  • Cloudflare DNS と Redirect Rule が同期済み、または所有権競合を明示。

10. 固定ドメインの Redirect と第二段を両方検証する

まず curl に Redirect を追従させず、Cloudflare の第一段だけを確認します。

curl -sS -o /dev/null -D - --max-redirs 0 \
  https://stun-lab-20260802.sparkles-editor.com/

HTTP/2 302(307 を選んだ場合は 307)と Location が必要です。Cloudflare Rules でも同じ入口ドメイン、status、active Rule を確認します。

実際の Rule 行です。公開許可済みの入口ドメインと 302 だけを残し、動的エンドポイント、アカウント情報、他 Rule、Resource ID は除外しました。

次にモバイル回線または独立した外部ホストから第二段を確認します。

curl -sS -L --max-time 15 -o /dev/null \
  -w 'status=%{http_code} redirects=%{num_redirects}\n' \
  https://stun-lab-20260802.sparkles-editor.com/

目標は status=200 redirects=1 です。再構築時には新しい動的 mapping へ外部ホストから接続して HTTP 200、固定入口から 302 と Location を確認しました。ただし Rule 画像の旧隔離サービスは後で停止したため、現在は第一段が残っている証拠であり、停止済み第二段の online 保証ではありません。

実際の Location、公開 IP、動的ポートは公開しないでください。

11. 障害切り分け

公開マッピングがあるのに ERR_EMPTY_RESPONSE

  1. StunDeck ホストから LAN ターゲットへ接続できるか;
  2. アプリが 127.0.0.1 のみに bind していないか;
  3. Compose が host network を使用しているか;
  4. サービスの gateway mode が正しいか;
  5. UPnP/NAT-PMP が実際の出口 gateway に設定されたか;
  6. 上位の光回線終端装置や二段目ルーターが遮断していないか;
  7. NAT loopback 非対応 LAN ではなく、別回線から試したか。

STUN が不安定

  • Docker daemon とコンテナの proxy を両方確認;
  • 透過 proxy、policy routing、multi-WAN を一時的に外して比較;
  • 待受ポートを重複させない;
  • 動的マッピングを固定値としてクライアントへ保存しない;
  • 更新時刻と NATMap プロセスを監視;
  • NAT 境界が非対応なら Tunnel、VPN、中継へ切り替える。

固定ドメインが 302/307 を返さない

  1. 入口 DNS が proxy 状態か;
  2. Token が正しい Zone に限定されているか;
  3. Zone Read と Single Redirect Edit があるか;
  4. DNS 自動管理時に DNS Edit があるか;
  5. 同名の非管理 DNS または Redirect Rule と競合していないか;
  6. http_request_dynamic_redirect phase に active Rule があるか。

Cloudflare 入口は開くが第二段が失敗

動的エンドポイントへ直接アクセスします。失敗するなら Redirect Rule ではなく、STUN、gateway mapping、firewall、LAN target を調べます。

12. 更新、バックアップ、削除

SQLite データベースと master.key を必ず一緒にバックアップします。まず実際の volume 名を確認します。

sudo docker volume ls
sudo docker volume inspect stundeck_stundeck-data

更新:

cd /opt/stundeck
sudo docker compose pull
sudo docker compose up -d --no-build
sudo docker compose ps

データを保持して停止:

sudo docker compose down

復元が不要な場合だけ volume を削除します。

sudo docker compose down -v

down -v は管理者、サービス、イベント、暗号化された Cloudflare credential、ローカル master key を削除します。

参考資料

重要なのは 5 層の証拠です。ターゲット到達、STUN Binding 成功、gateway mapping 作成、Cloudflare 固定入口の 302/307、独立した外部クライアントが Redirect を追従した後の成功です。固定ドメインが解決するのは入口の安定性であり、第二段を保護された Tunnel に変えることではありません。

記事はここまで
READER CHANNEL

コメント

00
まだコメントはありません。最初の返信を残してみませんか。