SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Настройка wg-easy: WireGuard с веб-интерфейсом в Docker

Разверните WireGuard через wg-easy в Docker Compose. Узнайте, как правильно настроить параметры NET_ADMIN, sysctls и порты для корректной работы VPN и генерации QR-кодов.

Что вы создаете

wg-easy — это WireGuard с веб-интерфейсом, работающий в одном Docker контейнере. Он управляет интерфейсом WireGuard за вас и добавляет браузерный интерфейс для создания клиентов. Каждый созданный клиент получает файл конфигурации и QR-код, поэтому телефон подключается к VPN, просто отсканировав код с экрана.

Сам туннель является обычным WireGuard. Модуль ядра передает пакеты, поэтому пропускная способность такая же, как при ручной настройке. Вы выигрываете в управлении жизненным циклом клиентов: добавление, отключение и удаление пиров происходит без редактирования конфигурационных файлов по SSH. Вы теряете прямой контроль над конфигурацией, что является темой ручной настройки WireGuard на VPS.

Вам потребуется Linux-хост с правами root, которым вы можете управлять, например, KVM VPS с публичным IP-адресом или доменным именем, указывающим на него, процессор архитектуры x86_64 или arm64, а также Docker Engine с плагином Compose. Контейнерная виртуализация, использующая общее ядро хоста, такая как OpenVZ или LXC, обычно не может загрузить модуль WireGuard, и контейнер не сможет поднять интерфейс.

В версии 15 настройки вынесены из переменных окружения

Большинство руководств, которые вы найдете, написаны для wg-easy 14, где WG_HOST задавался как адрес сервера, а PASSWORD_HASH — как bcrypt-хеш пароля администратора, причем оба параметра передавались через переменные окружения. Версия 15 представляет собой полную переработку. В официальных примечаниях к миграции прямо указано, что v15 не использует те же переменные окружения, что и v14, и что большинство из них теперь настраиваются через панель администратора в веб-интерфейсе.

Поэтому WG_HOST и PASSWORD_HASH больше не выполняют никаких функций. Если вы скопируете старый файл compose, контейнер запустится, проигнорирует эти строки, а затем предложит создать учетную запись администратора в браузере. Это не ошибка. Это новый процесс настройки.

По состоянию на октябрь 2026 года основным тегом для фиксации версии является 15. Фиксируйте мажорную версию, а не используйте latest, так как мажорное обновление меняет формат конфигурации на диске и не предполагает корректного отката. Переход на версию 15 заменил JSON-файл из v14 на базу данных 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 mounts и именованными томами, так как права доступа в них работают по-разному.

Зачем нужны 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 заставляет ядро пересылать пакеты, которые не адресованы самому устройству. Без этого параметра клиент подключается, рукопожатие проходит успешно, но затем все пакеты в интернет отбрасываются, из-за чего ping 1.1.1.1 выдает тайм-аут, хотя VPN выглядит подключенным.

net.ipv4.conf.all.src_valid_mark=1 — это параметр, который часто вызывает вопросы. WireGuard помечает свои исходящие пакеты, чтобы они не направлялись обратно в туннель. Строгая фильтрация обратного пути (reverse path filtering) обнаруживает пакет, чей адрес источника не соответствует ожидаемому маршруту, и отбрасывает его. Этот 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 при загрузке объясняется, что именно гарантирует эта политика.

Веб-интерфейс ожидает подключений на TCP 51821. При первом посещении отображается страница настройки, где вы создаете учетную запись администратора и подтверждаете адрес хоста, который клиенты будут использовать для связи с сервером. Этот адрес хоста попадает в строку Endpoint каждой конфигурации клиента, поэтому он должен быть публичным IP-адресом или DNS-именем вашего VPS. Если адрес указан неверно, QR-код, который вы передадите на телефон, будет указывать на недоступный узел, и «рукопожатие» (handshake) никогда не завершится.

Еще один момент касательно этого порта: wg-easy не поддерживает TLS самостоятельно и не отклоняет обычный HTTP. Через http:// интерфейс все равно загружается, отображая предупреждающий баннер. По умолчанию меняется только сессионный cookie, который получает флаг Secure, поэтому браузер, обращающийся к интерфейсу по обычному HTTP на публичном адресе, отбрасывает его, и авторизация не сохраняется. INSECURE=true удаляет этот флаг и больше ничего не делает. Размещение обратного прокси-сервера перед приложением для терминирования TLS, даже с самоподписанным сертификатом, не требует никаких изменений в wg-easy.

Не публикуйте порт интерфейса в интернет

Файл compose публикует порт 51821 на всех интерфейсах. Это страница входа в систему, которая может маршрутизировать ваш трафик, поэтому она не должна быть открыта для всего мира. Публикация порта в Docker записывает правила в цепочку DOCKER, которая обрабатывается до ufw, поэтому правило запрета ufw не закроет его. Эту ловушку важно понимать отдельно, и в почему опубликованные порты Docker игнорируют ufw это подробно описано.

Простое решение — привязать интерфейс к 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 лишь удаляет флаг Secure из сессионного cookie, чтобы браузер сохранял авторизацию на этом адресе с обычным HTTP, что здесь безопасно, так как этот сегмент трафика не покидает интерфейс loopback.

Откройте UDP 51820 и проверьте оба межсетевых экрана

Для работы WireGuard необходимо, чтобы порт UDP 51820 был доступен из Интернета. Docker публикует этот порт, но многие провайдеры устанавливают перед VPS отдельный сетевой экран, о котором Docker не знает. Откройте порт в обоих местах. Если вы управляете межсетевым экраном хоста с помощью ufw, базовые правила ufw для VPS станут более простым решением, чем ручная настройка nftables.

Убедитесь, что контейнер действительно ожидает соединений:

sudo ss -ulnp | grep 51820

Вы должны увидеть активный UDP-сокет. Если в этой строке ничего нет, значит, контейнер не смог поднять интерфейс, а sudo docker compose logs wg-easy укажет причину ошибки.

Создание клиента и сканирование на телефоне

В интерфейсе создайте клиента и присвойте ему имя, по которому вы сможете его опознать, например, название устройства. wg-easy выделит следующий свободный адрес туннеля и сгенерирует пару ключей. В строке каждого клиента доступен QR-код и файл .conf для скачивания.

Установите официальное приложение WireGuard на телефон, выберите добавление туннеля через QR-код и наведите камеру на код, отображаемый на экране. Туннель появится под именем, которое вы ввели. Включите его, и в строке клиента в интерфейсе начнут отображаться счетчики передачи данных и время последнего рукопожатия (handshake). Как только телефон подключен к туннелю, он получает доступ к сервисам, которые вы не публиковали в Интернете. Именно так телефон может загружать данные на собственный фотосервер из любого места, при этом сервер не будет иметь ни одного открытого во внешний мир порта. Этот же метод работает для медиаконтента: библиотека Jellyfin, стилизованная под видеопрокат 90-х, — приятное дополнение, доступное из номера отеля, оставаясь при этом таким же приватным, как и в вашей локальной сети. Оповещения работают в обратном направлении через тот же туннель: собственный сервер ntfy может отправить сообщение на телефон в момент сбоя задачи резервного копирования, не отвечая при этом на запросы из публичного Интернета.

Если после включения клиента рукопожатие не происходит, значит, он не может связаться с сервером. Проблема кроется в UDP 51820: либо в настройках файрвола провайдера, либо в адресе конечной точки, указанном в конфигурации. Если рукопожатие есть, но интернет не работает, проблема, скорее всего, в пересылке пакетов (forwarding) или DNS.

На компьютере скачайте файл .conf и импортируйте его в клиент WireGuard, вместо того чтобы вводить данные вручную. Приватный ключ в этом файле генерируется и отображается только один раз. Обращайтесь с этим файлом так же, как с приватным ключом SSH.

Когда стоит отказаться от графического интерфейса

wg-easy — подходящий инструмент, пока вашими пирами являются люди и телефоны. Интерфейс работает быстрее, чем редактирование файлов конфигурации, а отзыв доступа для потерянного телефона занимает один клик.

Вы столкнетесь с ограничениями, когда вам потребуется функционал, не предусмотренный интерфейсом. Маршрутизация между сайтами (site-to-site), где AllowedIPs пира охватывает целую удаленную подсеть, а не один адрес, — это обычно первое препятствие. Раздельное туннелирование (split tunnels) с правилами маршрутизации для каждого пира или конфигурация, созданная вашим инструментом автоматизации, — следующие шаги. В этот момент ручная настройка становится не сложнее, а просто другой, и руководство по стандартному WireGuard показывает тот же туннель, построенный с помощью wg0.conf. Если вы хотите полностью отказаться от запуска плоскости управления, сравнение WireGuard и Tailscale описывает управляемый вариант. Является ли это выгодным обменом, зависит от того, к чему может получить доступ сервер координации, и модель доверия Tailscale стоит прочитать до того, как вы доверите ему свою сеть. Стоимость — обычно следующий вопрос, и что на самом деле покрывает бесплатный тариф Tailscale — это до шести пользователей бесплатно, только для некоммерческого использования, поэтому домохозяйство ничего не платит, а команда малого бизнеса не подходит под условия, независимо от количества человек. После этого порога оплата рассчитывается за пользователей, а не за устройства, что отличается от оплаты VPS, за который вы уже платите, поэтому стоимость Tailscale после выхода из бесплатного тарифа — это цифра, которую нужно проверить перед миграцией команды. Полноценный туннель, который вы только что построили, имеет там прямой аналог, так как объявление VPS в качестве exit node в Tailscale дает вам тот же маршрут наружу через сервер, утвержденный в консоли администратора вместо записи в конфигурации каждого клиента. Ограничение подсетей также имеет аналог, так как объявление целой частной сети с VPS передает эту сеть каждому устройству в tailnet без редактирования AllowedIPs для каждого пира, что и заставило вас отказаться от графического интерфейса. Если вам нужна панель управления и автоматическая mesh-маршрутизация, но вы не хотите использовать чужой сервер координации, запуск собственного сервера NetBird на VPS сохраняет плоскость управления на вашем оборудовании ценой настройки DNS и TLS, которые wg-easy от вас не требовал.

Если синтаксис compose выше был для вас незнакомым, а не сам WireGuard, основы Docker Compose на VPS объясняют формат файла и повседневные команды.

FAQ

Почему wg-easy игнорирует мои переменные WG_HOST и PASSWORD_HASH?

Эти переменные относились к wg-easy 14. Версия 15 — это полная переработка, и разработчики перенесли почти всю конфигурацию в панель администратора в веб-интерфейсе. Контейнер не считывает эти переменные, поэтому он запускается в обычном режиме и просит создать учетную запись администратора при первом посещении. Укажите адрес хоста для клиентов на этой странице настройки.

Нужен ли мне SYS_MODULE, если в моем ядре уже есть WireGuard?

Нет. SYS_MODULE и монтирование /lib/modules нужны для того, чтобы контейнер мог загрузить модуль, если его нет на хосте. На хосте, где команда sudo modprobe wireguard выполняется успешно, эта возможность не используется. Удаление этой настройки — разумный шаг для повышения безопасности, а NET_ADMIN в любом случае остается обязательным.

Клиент подключается, но интернета нет. В чем проблема?

«Рукопожатие» без передачи трафика почти всегда означает проблемы с пересылкой пакетов (forwarding). Убедитесь, что net.ipv4.ip_forward=1 и net.ipv4.conf.all.src_valid_mark=1 все еще присутствуют в файле compose, так как при ручном копировании они часто теряются. Если пересылка включена, проверьте DNS-сервер, который получил клиент. Туннель, который отправляет весь трафик через VPN, но указывает на недоступный DNS-сервер, выглядит в браузере как неработающее соединение.

Как сделать резервную копию клиентов?

Все данные хранятся в именованном томе etc_wireguard. В версии 15 это означает базу данных SQLite по пути /etc/wireguard/wg-easy.db внутри контейнера. Документация v15 не описывает встроенную функцию резервного копирования или экспорта, поэтому делайте резервную копию самого тома: выполните docker compose down, скопируйте содержимое тома за пределы сервера, а затем снова запустите стек с помощью docker compose up -d. Делайте это перед любым обновлением. Файл wg0.json, о котором упоминают старые руководства, относится к wg-easy 14. Это то, что экспортирует кнопка резервного копирования в v14, и в v15 он нужен только для загрузки в мастере настройки при миграции старой установки.

Можно ли запустить wg-easy за обратным прокси-сервером?

Да. Разместите прокси перед портом веб-интерфейса, терминируйте там TLS и оставьте INSECURE не заданным. Контейнер принимает обычный HTTP-трафик от прокси как есть, а официальные примеры для Caddy и Traefik не задают переменную INSECURE. Поскольку браузер общается с прокси по HTTPS, вам нужно, чтобы сессионный cookie сохранил свой флаг Secure, который является единственным, что удаляет INSECURE=true. Оставьте UDP 51820 опубликованным напрямую, так как VPN-трафик использует UDP и не проходит через HTTP-прокси.

Была ли эта статья полезной?