Установка wg-easy с веб-интерфейсом в Docker
Развертывание WireGuard через wg-easy в Docker Compose. Инструкция по настройке прав NET_ADMIN, параметров sysctls и портов для корректной работы VPN на KVM VPS.
Что вы создаете
wg-easy — это WireGuard с веб-интерфейсом, работающий в одном Docker контейнере. Он управляет интерфейсом WireGuard и предоставляет браузерный UI для создания клиентов. Для каждого созданного клиента генерируется файл конфигурации и QR-код, поэтому телефон подключается к VPN путем сканирования кода с экрана.
Сам туннель является обычным WireGuard. Пакеты обрабатываются модулем ядра, поэтому пропускная способность такая же, как при ручной настройке. Вы получаете удобное управление жизненным циклом клиентов: добавление, отключение и удаление пиров без необходимости редактировать конфигурационные файлы по SSH. Вы теряете прямой контроль над конфигурацией, что является темой ручной настройки WireGuard на VPS.
Вам потребуется KVM VPS с публичным IPv4-адресом, Docker Engine с плагином Compose и права root. Контейнерная виртуализация, использующая общее ядро хоста, например 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::/64etc_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 15 отклоняет обычные HTTP-запросы, если не установлена переменная INSECURE=true. Доступ по HTTPS с недоверенным сертификатом или завершение TLS на обратном прокси-сервере перед приложением — допустимые варианты. Доступ по http:// с настройками по умолчанию невозможен.
Не открывайте порт веб-интерфейса в интернет
Файл 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 здесь в безопасности, так как передача по обычному 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 за обратным прокси-сервером?
Да. Разместите прокси перед TCP 51821, завершите там TLS и установите INSECURE=true для контейнера, чтобы он принимал обычный HTTP-трафик от прокси. Оставьте UDP 51820 опубликованным напрямую, так как VPN-трафик использует протокол UDP и не проходит через HTTP-прокси.