wg-easy у Docker: WireGuard із вебінтерфейсом
Налаштуйте WireGuard через wg-easy і Docker Compose: порти, NET_ADMIN, sysctls, зміни у версії 15 та QR-коди для підключення телефонів.
Що ви налаштовуєте
wg-easy — це WireGuard із вебінтерфейсом, який працює в одному Docker-контейнері. Він керує інтерфейсом WireGuard і додає браузерний інтерфейс для створення клієнтів. Для кожного створеного клієнта генеруються файл конфігурації та QR-код. Щоб під’єднати телефон до VPN, достатньо навести його камеру на екран.
Сам тунель є звичайним WireGuard. Пакети передає kernel module, тому пропускна здатність така сама, як у конфігурації, створеної вручну. Ви отримуєте зручне керування життєвим циклом клієнтів: можна додавати, вимикати й видаляти peer без редагування конфігураційного файла через SSH. Натомість ви втрачаєте прямий контроль над цим конфігураційним файлом. Це описано в розділі налаштування WireGuard вручну на VPS.
Вам потрібні KVM VPS із публічною IPv4-адресою, Docker Engine із Compose plugin і доступом root. Віртуалізація контейнерів зі спільним kernel хоста, наприклад OpenVZ або LXC, зазвичай не може завантажити модуль WireGuard. Через це контейнер не зможе підняти інтерфейс.
Версія 15 перенесла налаштування з environment variables
Більшість доступних інструкцій написано для wg-easy 14. У них WG_HOST задається як адреса сервера, а PASSWORD_HASH — як bcrypt-хеш пароля адміністратора. Обидва значення передаються як environment variables. Версія 15 — це переписана версія. В офіційних примітках щодо міграції прямо зазначено, що v15 не використовує ті самі environment variables, що й v14, а більшість із них перенесено до панелі адміністратора у web UI.
Тому WG_HOST і PASSWORD_HASH більше нічого не роблять. Якщо скопіювати старий compose-файл, контейнер запуститься, проігнорує ці рядки, а потім запропонує створити обліковий запис адміністратора у браузері. Це не помилка. Це новий процес початкового налаштування.
Станом на July 2026 для фіксації слід використовувати major tag 15. Фіксуйте major version замість latest, оскільки оновлення major version змінює формат конфігурації на диску, і коректно повернутися до попередньої версії не вдасться.
Файл compose
Створіть каталог для стека та запишіть у нього офіційний файл compose. Це upstream-файл без змін.
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 — це іменований том, у якому зберігаються ключ сервера та всі створені вами клієнти. Створіть резервну копію цього тому. Інакше після повторного складання всі ваші peers буде видалено. Якщо ви хочете бачити ці файли у файловій системі хоста, замініть його на 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 у режимі лише для читання дає контейнеру змогу завантажити kernel module WireGuard, якщо host ще не завантажив його. Модуль міститься в kernel host, а не всередині image, тому каталог host має бути доступним у контейнері. У сучасному kernel модуль зазвичай уже вбудований. Це можна перевірити командою sudo modprobe wireguard && echo ok на host.
net.ipv4.ip_forward=1 вмикає в kernel пересилання пакетів, адресованих не самому host. Без цього client підключається, handshake проходить успішно, але потім усі пакети до інтернету відкидаються, тому ping 1.1.1.1 завершується за тайм-аутом, хоча VPN виглядає підключеним.
net.ipv4.conf.all.src_valid_mark=1 — параметр, який часто викликає запитання. WireGuard позначає власні вихідні пакети, щоб вони не маршрутизувалися назад у tunnel. Строга reverse path filtering бачить пакет, адреса джерела якого не відповідає очікуваному маршруту, і відкидає його. Цей sysctl повідомляє kernel приймати позначені пакети. Саме це не дає full tunnel зламати власну маршрутизацію.
Запустіть його та створіть обліковий запис адміністратора
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -fВикористовуйте docker compose up і docker compose down, а не start і stop. Upstream попереджає, що start для контейнера, створеного з іншими параметрами, залишає мережу в непослідовному стані. Якщо потрібно відновлювати стек після перезавантаження, restart: unless-stopped уже забезпечує це, а в розділі поведінка compose-сервісів під час завантаження пояснено, що саме гарантує ця політика, а чого вона не гарантує.
Вебінтерфейс прослуховує TCP-порт 51821. Під час першого відкриття він показує сторінку налаштування, де потрібно створити обліковий запис адміністратора та підтвердити адресу хоста, через яку клієнти підключатимуться до сервера. Ця адреса хоста потрапляє в рядок Endpoint конфігурації кожного клієнта, тому це має бути публічна IP-адреса або DNS-ім’я VPS. Якщо вказати неправильну адресу, QR-код, переданий на телефон, вестиме до недоступної адреси, і рукостискання не завершиться.
Є ще один важливий момент щодо цього порту: wg-easy 15 відмовляється працювати через звичайний HTTP, якщо не встановити INSECURE=true. Підключення через HTTPS із недовіреним сертифікатом або завершення TLS на reverse proxy перед ним — обидва варіанти підходять. Підключення через http:// із параметрами за замовчуванням — ні.
Не публікуйте порт UI в інтернеті
Файл compose публікує порт 51821 на всіх інтерфейсах. Це сторінка входу до системи, яка може маршрутизувати ваш трафік, тому вона не має бути відкритою для всього інтернету. Публікація порту в Docker додає правила до ланцюжка DOCKER, який обробляється раніше за ufw, тому правило ufw deny не закриває цей порт. Цей нюанс варто зрозуміти окремо. Докладне пояснення наведено в матеріалі чому опубліковані Docker порти ігнорують ufw.
Просте рішення — прив’язати UI до loopback і підключатися до нього через SSH tunnel:
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 interface.
Відкрийте UDP-порт 51820 і перевірте обидва firewall
WireGuard потребує доступного з інтернету UDP-порту 51820. Docker публікує цей порт, але багато провайдерів розміщують перед VPS окремий мережевий firewall, про який Docker нічого не знає. Відкрийте порт в обох місцях. Якщо ви керуєте firewall хоста за допомогою ufw, базові правила ufw для VPS налаштувати простіше, ніж вручну писати правила nftables.
Перевірте, чи контейнер справді прослуховує порт:
sudo ss -ulnp | grep 51820Ви маєте побачити UDP-сокет у стані прослуховування. Якщо в цьому рядку нічого немає, контейнер не зміг підняти інтерфейс, а sudo docker compose logs wg-easy покаже причину.
Створіть клієнта та відскануйте його на телефоні
У UI створіть клієнта та задайте йому ім’я, яке ви зможете розпізнати пізніше, наприклад назву пристрою, якому він належить. wg-easy автоматично призначить наступну вільну адресу тунелю та згенерує для вас пару ключів. У кожному рядку клієнта доступні QR-код і файл .conf для завантаження.
Встановіть на телефоні офіційний застосунок WireGuard, виберіть додавання тунелю з QR-коду та наведіть камеру на код на екрані. Тунель з’явиться під заданим вами ім’ям. Увімкніть його, після чого в рядку клієнта в UI почнуть відображатися лічильники переданих даних і час останнього handshake. Після підключення телефона до тунелю він зможе отримувати доступ до сервісів, які ніколи не публікувалися в інтернеті. Завдяки цьому телефон може завантажувати фотографії на self-hosted фотосервер з будь-якого місця, навіть якщо цей сервер не має жодного відкритого для всього світу порту. Та сама схема підходить для медіа: бібліотека Jellyfin, перетворена на відеопрокат із 90-х, зручно переглядати з готельного номера, поки вона залишається такою ж приватною, як у вашій LAN. Сповіщення працюють у зворотному напрямку через той самий тунель: self-hosted сервер ntfy може надіслати повідомлення на цей телефон одразу після збою завдання резервного копіювання, не приймаючи жодного запиту з публічного інтернету.
Клієнт, у якого після ввімкнення немає handshake, взагалі не досягає сервера. Причина, імовірно, у UDP 51820: його може блокувати firewall провайдера або адреса endpoint, вбудована в конфігурацію. Якщо handshake є, але інтернет не працює, перевірте forwarding або DNS.
На desktop завантажте файл .conf та імпортуйте його в клієнт WireGuard, а не вводьте конфігурацію вручну. Приватний ключ у цьому файлі генерується один раз і показується один раз. Захищайте цей файл так само, як приватний ключ SSH.
Коли UI вже недостатньо
wg-easy добре підходить, доки вашими peers є люди та телефони. UI швидший за редагування конфігураційних файлів, а відкликання доступу для втраченого телефона займає один клік.
Обмеження з’являться, коли вам знадобиться те, чого UI не моделює. Маршрутизація site-to-site, за якої AllowedIPs peer охоплює цілу віддалену підмережу, а не одну адресу, зазвичай стає першою перешкодою. Наступними є split tunnel із правилами маршрутизації для окремих peers або конфігурація, згенерована вашим інструментом provisioning. На цьому етапі ручне налаштування не складніше — воно просто інше, а посібник зі звичайного WireGuard показує, як створити такий самий тунель за допомогою wg0.conf. Якщо ви взагалі не хочете запускати control plane, у матеріалі WireGuard порівняно з Tailscale описано керований варіант. Чи буде такий компроміс виправданим, залежить від того, до чого фактично може отримати доступ coordination server. Перед тим як надати йому доступ до мережі, варто прочитати модель довіри Tailscale. Наступним зазвичай виникає питання вартості. Що насправді покриває безкоштовний план Tailscale — цього достатньо, щоб домашній користувач або невелика команда нічого не платили. Після цього оплата залежить від кількості користувачів, а не пристроїв. Це інша модель витрат, ніж VPS, за який ви вже платите. Тому перед міграцією команди перевірте скільки коштує Tailscale після переходу межі безкоштовного плану. Повний тунель, який ви щойно створили, має там прямий аналог: оголошення VPS як exit node Tailscale дає той самий маршрут назовні через сервер. Різниця в тому, що його схвалюють в admin console, а не записують у конфігурацію кожного клієнта. Для підмережі також є аналог. Оголошення всієї приватної мережі з VPS передає цю мережу кожному пристрою в tailnet без ручного редагування AllowedIPs для кожного peer, через яке ви перейшли від UI. Якщо вам потрібні dashboard і автоматична mesh routing, але не coordination server іншої організації, запуск власного сервера NetBird на VPS залишає control plane на обладнанні, яким ви володієте. Водночас доведеться самостійно налаштувати DNS і TLS, чого wg-easy від вас не вимагав.
Якщо незнайомою частиною був саме наведений вище синтаксис compose, а не WireGuard, у матеріалі Основи Docker Compose на VPS пояснюються формат файлу та повсякденні команди.
FAQ
Чому wg-easy ігнорує мої WG_HOST і PASSWORD_HASH?
Ці змінні належать wg-easy 14. Версія 15 була переписана, і upstream переніс майже всю конфігурацію до панелі адміністратора у web UI. Контейнер не читає жодну з цих змінних, тому запускається у штатному режимі, а під час першого відвідування просить створити обліковий запис адміністратора. Укажіть адресу хоста, доступну клієнтам, на цій сторінці налаштування.
Чи потрібен SYS_MODULE, якщо ядро вже має WireGuard?
Ні. SYS_MODULE і монтування /lib/modules потрібні, щоб контейнер міг завантажити модуль, якщо на хості його немає. На хості, де sudo modprobe wireguard уже виконується успішно, ця capability не використовується. Її видалення є розумним кроком для посилення захисту, а NET_ADMIN все одно потрібен.
Клієнт підключається, але інтернету немає. У чому проблема?
Handshake без трафіку майже завжди означає проблему з forwarding. Переконайтеся, що net.ipv4.ip_forward=1 і net.ipv4.conf.all.src_valid_mark=1 досі є у compose-файлі, оскільки під час ручного редагування копії їх часто втрачають. Якщо forwarding увімкнено, перевірте DNS-сервер, який отримав клієнт. Тунель, що передає весь трафік через VPN, але використовує DNS-сервер, до якого він більше не може підключитися, у браузері виглядає так само, як непрацююче з’єднання.
Як створити резервну копію клієнтів?
Усі дані зберігаються в іменованому volume etc_wireguard, у файлі wg0.json. У UI також є кнопка резервного копіювання, яка експортує ті самі дані. Скопіюйте цей файл за межі сервера до будь-якого оновлення. Відновлення виконується завантаженням файлу під час кроку налаштування нового контейнера.
Чи можна запускати wg-easy за reverse proxy?
Так. Розмістіть proxy перед TCP 51821, завершуйте TLS на ньому та встановіть INSECURE=true у контейнері, щоб він приймав звичайне HTTP-з’єднання від proxy. UDP 51820 залиште опублікованим безпосередньо, оскільки VPN-трафік використовує UDP і не проходить через HTTP proxy.