SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

wg-easyをDockerで構築する方法とv15の注意点

Docker Composeでwg-easyを動かす手順です。WireGuardのポート、NET_ADMIN、sysctls、v15で環境変数が無効になった変更、スマホのQRコード接続を解説します。

構築するもの

wg-easy は、1 つの Docker コンテナとして動作する Web インターフェース付きの WireGuard です。WireGuard インターフェースを管理し、クライアントを作成するためのブラウザー UI を提供します。作成した各クライアントには設定ファイルと QR コードが生成されるため、スマートフォンは画面にカメラを向けるだけで VPN に参加できます。

トンネル自体は通常の WireGuard です。パケットはカーネルモジュールが処理するため、スループットは手作業で構築した場合と同じです。得られるのは、SSH 経由で設定ファイルを編集せずに、ピアの追加、無効化、削除を行えるクライアント管理機能です。その代わり、設定ファイルを直接制御できなくなります。この点については、VPS で手動構築する WireGuard で説明します。

管理可能な Linux ホストが必要です。たとえば KVM VPS などで、パブリック IP アドレスまたはそのホストを指すドメイン名、x86_64 または arm64 CPU、Compose plugin 付きの Docker Engine、root アクセスが必要です。OpenVZ や LXC など、ホストのカーネルを共有するコンテナ仮想化では、通常 WireGuard モジュールをロードできません。そのため、コンテナはインターフェースの起動に失敗します。

バージョン 15 では設定が環境変数から移動しました

多くのガイドは wg-easy 14 向けに書かれています。このバージョンでは、WG_HOST にサーバーアドレスを設定し、PASSWORD_HASH に管理者パスワードの bcrypt ハッシュを設定します。どちらも環境変数として指定します。バージョン 15 は全面的に書き直されています。公式の移行手順には、v15 は v14 と同じ環境変数を使用せず、その多くが Web UI の管理パネルに移動したと明記されています。

そのため、WG_HOST と PASSWORD_HASH は機能しなくなりました。古い compose ファイルをコピーすると、コンテナは起動しますが、それらの行を無視し、ブラウザーで管理者アカウントを作成するよう求めます。これはバグではありません。新しいセットアップ手順です。

2026 年 10 月時点で固定すべきメジャータグは 15 です。latest は使用せず、メジャーバージョンを固定してください。メジャーアップグレードではディスク上の設定形式が変更され、正常にロールバックできないためです。15 への移行により、v14 の JSON ファイルは SQLite データベースに置き換えられました。

Compose ファイル

スタック用のディレクトリを作成し、その中に公式の Compose ファイルを書き込みます。これは上流のファイルです。コメントアウトされた任意の行は除外しています。

sudo mkdir -p /etc/docker/containers/wg-easy
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml \
  https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml

内容は次のとおりです。

volumes:
  etc_wireguard:

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    networks:
      wg:
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a
    volumes:
      - etc_wireguard:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv6.conf.all.forwarding=1
      - net.ipv6.conf.default.forwarding=1

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
        - subnet: fdcc:ad94:bacf:61a3::/64

etc_wireguard は、サーバーキーと作成したすべてのクライアントを保持する名前付きボリュームです。これらは SQLite データベース wg-easy.db に保存されます。このボリュームをバックアップしてください。バックアップがないと、再構築時にすべてのピアが失われます。これらのファイルをホストのファイルシステム上で確認したい場合は、bind mount に置き換えてください。実行する前に、bind mount と名前付きボリュームの違いを確認してください。権限の動作が異なるためです。

NET_ADMIN、SYS_MODULE、sysctl が必要な理由

デフォルトでは、コンテナはネットワークスタックを操作できません。以下の各行は、それぞれ特定の制限を解除します。

NET_ADMIN により、コンテナは wg0 インターフェースを作成し、アドレスを割り当て、ルートを書き込めるようになります。これがないと、インターフェースの起動中に ip link add wg0 type wireguard が Operation not permitted を返すため、コンテナは起動後に終了します。

SYS_MODULE と読み取り専用の /lib/modules マウントを組み合わせると、ホストが WireGuard カーネルモジュールをまだロードしていない場合でも、コンテナはそのモジュールをロードできます。モジュールはイメージ内ではなくホストのカーネル上に存在します。そのため、ホストのディレクトリをコンテナから見えるようにする必要があります。最新のカーネルでは、通常このモジュールは組み込み済みです。ホスト上で sudo modprobe wireguard && echo ok を実行すると確認できます。

net.ipv4.ip_forward=1 により、カーネルは自ホスト宛てではないパケットを転送します。これがないと、クライアントは接続し、ハンドシェイクも成功しますが、インターネット宛てのすべてのパケットが破棄されます。そのため、VPN が接続済みに見えても ping 1.1.1.1 はタイムアウトします。

net.ipv4.conf.all.src_valid_mark=1 は特に見落とされやすい設定です。WireGuard は、自身が送信するパケットにマークを付け、それらがトンネルへ戻らないようにします。厳格な逆引きパスフィルタリングでは、送信元アドレスが想定されたルートと一致しないパケットが検出され、破棄されます。この sysctl は、マークされたパケットをカーネルが受け入れるようにします。これにより、フルトンネルが自身の通信を破壊するのを防げます。

起動して管理者アカウントを作成する

cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -f

start と stop ではなく、docker compose up と docker compose down を使用します。start は既存のコンテナだけを再起動するため、compose ファイルに加えた変更(ネットワークや sysctl 設定を含む)が適用されません。一方、up は変更されたものを再作成します。再起動後にスタックを復元したい場合は、restart: unless-stopped で対応できます。compose サービスの起動時の動作では、このポリシーで保証されることと保証されないことを説明しています。

Web UI は TCP 51821 で待ち受けます。初回アクセス時にはセットアップページが表示され、管理者アカウントを作成し、クライアントがサーバーへ接続する際に使用するホストアドレスを確認します。このホストアドレスは、すべてのクライアント設定の Endpoint 行に入ります。そのため、VPS のパブリック IP または DNS 名を指定する必要があります。誤った値を指定すると、スマートフォンに渡した QR コードが到達不能なアドレスを指し、ハンドシェイクが完了しません。

このポートについて、もう 1 点注意が必要です。wg-easy 自体は TLS を提供せず、平文 HTTP も拒否しません。http:// 経由でも UI は読み込まれ、警告バナーが表示されます。デフォルト設定で変わるのは、Secure フラグが付いたセッション Cookie です。そのため、パブリックアドレス上の平文 HTTP で UI にアクセスすると、ブラウザーが Cookie を破棄し、ログイン状態が維持されません。INSECURE=true はこのフラグを削除するだけで、それ以外は変更しません。自己署名証明書を使用する場合でも、TLS を終端するリバースプロキシを前段に置けば、wg-easy 側の変更は不要です。

UI ポートをインターネットに公開しない

compose ファイルは、すべてのインターフェースで 51821 を公開します。これはネットワークトラフィックをルーティングできるホストのログインページであり、全世界に公開すべきではありません。Docker でポートを公開すると、DOCKER チェーンにルールが書き込まれます。このチェーンは ufw より先に評価されるため、ufw の deny ルールではポートを閉じられません。この落とし穴は単独で理解しておく価値があります。Docker の公開ポートが ufw を無視する理由で詳しく説明しています。

簡単な対策は、UI を loopback にバインドし、SSH トンネル経由でアクセスすることです。

    ports:
      - "51820:51820/udp"
      - "127.0.0.1:51821:51821/tcp"
    environment:
      - INSECURE=true

次に、ラップトップで次を実行します。

ssh -L 51821:127.0.0.1:51821 youruser@your.server.address

ラップトップのブラウザーで http://127.0.0.1:51821 を開きます。通信は SSH によって暗号化され、他のホストからはこのポートに応答できません。INSECURE=true はセッション Cookie から Secure フラグを削除するだけです。そのため、ブラウザーはこの平文 HTTP アドレスでログイン状態を維持できます。この構成では、平文 HTTP の通信が loopback インターフェースの外へ出ないため安全です。

UDP 51820 を開き、両方のファイアウォールを確認する

WireGuard では、インターネットから UDP 51820 に到達できる必要があります。Docker はこのポートを公開しますが、多くのプロバイダーでは VPS の前段に別のネットワークファイアウォールがあり、Docker はその存在を認識しません。両方でポートを開いてください。ufw でホストのファイアウォールを管理している場合は、nftables を手動で記述するよりも、VPS の基本的な ufw ルールを使うほうが簡単です。

コンテナが実際に待ち受けていることを確認します。

sudo ss -ulnp | grep 51820

UDP ソケットが待ち受け状態になっていることを確認できます。その行が表示されない場合、コンテナはインターフェースを起動できていません。sudo docker compose logs wg-easy に原因が表示されます。

クライアントを作成してスマートフォンでスキャンする

UI でクライアントを作成し、後で識別できる名前を付けます。たとえば、そのクライアントを使用するデバイス名にします。wg-easy は次に使用可能なトンネルアドレスを割り当て、鍵ペアを自動生成します。各クライアント行には QR コードと、ダウンロード可能な .conf ファイルがあります。

スマートフォンに公式の WireGuard アプリをインストールし、QR コードからトンネルを追加する操作を選択します。画面上のコードにカメラを向けて読み取ります。入力した名前でトンネルが表示されます。トンネルを有効にすると、UI のクライアント行に転送量カウンターと直近のハンドシェイク時刻が表示されます。スマートフォンがトンネルに接続すると、インターネットに公開していないサービスにも到達できます。そのため、サーバーでポートを一切外部公開していなくても、どこからでもスマートフォンから 自ホストの写真サーバーへアップロードを続けられます。同じ方法はメディアにも使えます。90 年代のビデオ店として再構築した Jellyfin ライブラリなら、LAN 上と同じように非公開のまま、ホテルの部屋から快適に閲覧できます。同じトンネル上では通知を逆方向に送ることもできます。自ホストの ntfy サーバーは、バックアップジョブが失敗した瞬間にスマートフォンへメッセージをプッシュできます。パブリックインターネットからのリクエストに応答する必要はありません。

有効化後もハンドシェイクが表示されないクライアントは、サーバーにまったく到達できていません。原因として UDP 51820 を確認します。プロバイダーのファイアウォール、または設定に埋め込まれたエンドポイントアドレスが問題である可能性があります。ハンドシェイクは表示されるのにインターネットを利用できない場合は、フォワーディングまたは DNS が原因です。

デスクトップでは、.conf ファイルをダウンロードし、再入力せずに WireGuard クライアントへインポートします。このファイルの秘密鍵は 1 回だけ生成され、1 回だけ表示されます。SSH の秘密鍵と同じように、このファイルを厳重に扱ってください。

UI から移行するタイミング

ピアが人やスマートフォンである間は、wg-easy が適したツールです。設定ファイルを編集するより UI のほうが速く、紛失したスマートフォンのアクセスも 1 クリックで無効化できます。

UI で扱えない構成が必要になると、制限に直面します。通常、最初の壁はサイト間ルーティングです。これは、ピアの AllowedIPs が 1 つのアドレスではなく、リモートサブネット全体を対象にする構成です。ピアごとのルーティング規則を使うスプリットトンネルや、プロビジョニングツールが生成する設定も、次の壁になります。その段階では、手動構築は難しくなるのではなく、方法が変わるだけです。通常の WireGuard ガイドでは、wg0.conf から同じトンネルを構築する方法を説明しています。コントロールプレーン自体を運用したくない場合は、WireGuard と Tailscale の比較でマネージドな選択肢を確認できます。これが妥当な交換条件かどうかは、調整サーバーが実際に到達できる範囲によって決まります。ネットワークを預ける前に、Tailscale の信頼モデルも確認してください。次に問題になるのは通常コストです。Tailscale の無料プランに実際に含まれる範囲では、非商用利用に限り最大 6 ユーザーまで無料です。そのため家庭では費用がかかりませんが、小規模企業のチームは人数が少なくても対象外です。それを超えると、課金単位はデバイスではなくユーザーになります。すでに料金を支払っている VPS とは請求の形が異なるため、チームを移行する前に無料プランを超えた場合の Tailscale の料金を確認してください。今構築したフルトンネルには、Tailscale にも直接対応する構成があります。VPS を Tailscale の exit node として広告することで、各クライアントの設定に記述する代わりに、管理コンソールで承認して同じサーバー経由の出口を利用できます。サブネットについても同様です。VPS からプライベートネットワーク全体を広告すると、そのネットワークを tailnet 内のすべてのデバイスに提供できます。これにより、UI から移行するきっかけとなった、ピアごとの AllowedIPs 編集が不要になります。ダッシュボードと自動メッシュルーティングは必要だが、他者の調整サーバーは使いたくない場合は、VPS で独自の NetBird サーバーを運用することで、所有するハードウェア上にコントロールプレーンを置けます。ただし、wg-easy では不要だった DNS と TLS の構築が必要です。

上記の compose 構文が WireGuard ではなく不慣れな部分だった場合は、VPS での Docker Compose の基本でファイル形式と日常的に使うコマンドを説明しています。

FAQ

wg-easy が WG_HOST と PASSWORD_HASH を無視するのはなぜですか?

これらの変数は wg-easy 14 用です。Version 15 では全面的に書き直され、上流プロジェクトは設定のほとんどを Web UI の管理パネルへ移しました。コンテナはどちらの変数も読み取らないため、正常に起動した後、初回アクセス時に管理者アカウントの作成を求めます。クライアント向けのホストアドレスは、セットアップページで設定してください。

カーネルに WireGuard が組み込まれている場合も SYS_MODULE は必要ですか?

いいえ。SYS_MODULE と /lib/modules のマウントは、ホストにモジュールがない場合にコンテナがロードできるようにするためのものです。ホスト上で sudo modprobe wireguard がすでに成功する場合、この capability は使用されません。削除してセキュリティを強化するのは妥当です。ただし、どちらの場合も NET_ADMIN は必要です。

クライアントは接続できますが、インターネットにアクセスできません。何が問題ですか?

トラフィックを伴わないハンドシェイクは、ほぼ常にフォワーディングが原因です。手作業で編集したコピーでは設定が失われやすいため、net.ipv4.ip_forward=1 と net.ipv4.conf.all.src_valid_mark=1 が compose ファイルに残っていることを確認します。フォワーディングが有効なら、クライアントが受け取った DNS サーバーを確認してください。VPN 経由ですべてのトラフィックを送信する一方、到達できなくなった DNS サーバーを指定しているトンネルは、ブラウザー上では接続停止と同じように見えます。

クライアントをバックアップするにはどうすればよいですか?

すべてのデータは etc_wireguard named volume に保存されます。Version 15 では、コンテナ内の /etc/wireguard/wg-easy.db にある SQLite データベースを指します。v15 のドキュメントにはバックアップ機能やエクスポート機能の説明がないため、ボリューム自体をバックアップしてください。docker compose down を実行し、ボリュームの内容をサーバー外の場所へコピーしてから、docker compose up -d でスタックを再起動します。アップグレードの前に必ず実施してください。古いガイドで言及されている wg0.json ファイルは wg-easy 14 用です。v14 のバックアップボタンがエクスポートするファイルであり、v15 では、古いインストールから移行する際にセットアップウィザードでアップロードするファイルとしてのみ使用します。

wg-easy をリバースプロキシの背後で実行できますか?

はい。Web UI のポートの前段にプロキシを置き、そこで TLS を終端し、INSECURE は未設定のままにします。コンテナはプロキシからの平文 HTTP 通信をそのまま受け付けます。公式の Caddy および Traefik の例でも、INSECURE 変数は設定していません。ブラウザーはプロキシへ HTTPS で接続するため、セッション Cookie の Secure フラグは維持する必要があります。このフラグを削除するのは INSECURE=true だけです。UDP 51820 は直接公開したままにしてください。VPN トラフィックは UDP であり、HTTP プロキシを経由しないためです。

この記事は役に立ちましたか?