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 は、Web インターフェースを備えた WireGuard で、1 つの Docker コンテナとして動作します。WireGuard インターフェースの管理を自動化し、クライアントを作成するためのブラウザー UI を提供します。作成した各クライアントには設定ファイルと QR コードが生成されるため、スマートフォンのカメラを画面に向けるだけで VPN に接続できます。

トンネル自体は通常の WireGuard です。パケットの転送はカーネルモジュールが行うため、スループットは手動で構築した場合と同じです。SSH で設定ファイルを編集せずに、peer の追加、無効化、削除を行える点が利点です。一方、その設定を直接制御できなくなる点が欠点です。この点については VPS 上での手動 WireGuard 構築 で説明します。

必要なのは、パブリック IPv4 アドレスを持つ KVM VPS、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

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

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

このポートについて、もう1点注意が必要です。wg-easy 15 は、INSECURE=true を設定しない限り平文の HTTP 接続を拒否します。信頼されていない証明書を使って HTTPS でアクセスしても、前段のリバースプロキシで TLS を終端しても問題ありません。デフォルト設定のまま http:// でアクセスすることはできません。

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 によって暗号化され、ポートは他のユーザーから応答可能な状態になりません。平文の HTTP 通信が loopback インターフェースの外に出ないため、ここでは INSECURE=true を安全に使用できます。

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 の操作が速く、紛失したスマートフォンのアクセス権もワンクリックで無効化できます。

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

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

FAQ

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

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

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

いいえ。SYS_MODULE と /lib/modules の mount は、host に module がない場合にコンテナが読み込めるようにするためのものです。sudo modprobe wireguard がすでに成功する host では、この capability は使用されません。これを削除することは、合理的な hardening です。どちらの場合でも NET_ADMIN は必要です。

クライアントは接続しますが、インターネットに接続できません。原因は何ですか?

traffic のない handshake は、ほぼ常に forwarding が原因です。手作業で編集した compose file では削除されやすいため、net.ipv4.ip_forward=1 と net.ipv4.conf.all.src_valid_mark=1 が compose file に残っていることを確認してください。forwarding が有効な場合は、クライアントが受け取った DNS server を確認します。すべての traffic を VPN 経由で送信しながら、到達できなくなった DNS server を指定している tunnel は、ブラウザーでは接続が停止したように見えます。

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

すべてのデータは etc_wireguard named volume に保存されます。Version 15 では、コンテナ内の /etc/wireguard/wg-easy.db にある SQLite database が該当します。v15 の docs にはバックアップまたは export 機能の説明がないため、volume 自体をバックアップします。docker compose down を実行し、volume の内容を server 外の場所へコピーしてから、docker compose up -d で stack を再起動します。これは upgrade の前に実行してください。古い guide に記載されている wg0.json file は wg-easy 14 に属します。v14 のバックアップボタンが export するファイルであり、v15 では、旧 install から移行するときに setup wizard で upload するファイルとしてのみ使用します。

wg-easy を reverse proxy の背後で実行できますか?

はい。proxy を TCP 51821 の前段に配置してそこで TLS を終端し、proxy からの平文 HTTP hop を受け付けるように、コンテナで INSECURE=true を設定します。VPN traffic は UDP であり HTTP proxy を通過しないため、UDP 51820 は直接 publish したままにしてください。