VPN-сервер в Docker Compose: разворачиваем в контейнере
Поднимать VPN-сервер через apt install и ручную правку конфигов — рабочий, но неудобный для повторения путь: при переносе на новый хост всё приходится собирать заново. Docker Compose решает эту задачу — конфигурация фиксируется в одном файле, а сам сервис поднимается одной командой на любой машине с Docker. Разберём, как это устроено для WireGuard, OpenVPN и IKEv2/strongSwan, какие capabilities и sysctls обязательны, и где контейнеризация VPN упирается в реальные ограничения.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Зачем контейнеризировать VPN-сервер
VPN-демон в контейнере работает не так, как обычный веб-сервис: ему нужен доступ к сетевому стеку хоста — создание виртуальных интерфейсов (tun/wg), правка таблиц маршрутизации, работа с iptables/nftables для NAT. По умолчанию Docker всё это запрещает: контейнер живёт в изолированном network namespace с урезанным набором прав.
Тем не менее контейнеризация VPN оправдана в нескольких сценариях:
- Повторяемость.
docker-compose.yml+ volume с конфигами — это весь сервер. Перенос на другую машину —docker compose up -dпосле копирования volume. - Изоляция. VPN-демон не тянет за собой системные зависимости хоста, не конфликтует по версиям библиотек с другими сервисами.
- Несколько VPN на одном сервере. WireGuard, OpenVPN и IKEv2 одновременно, каждый в своём контейнере, без конфликтов конфигурации на уровне ОС.
- Быстрый откат. Сломали конфиг —
docker compose down && docker compose up -dс восстановленным volume, а не переустановка пакета.
Минус один, но он структурный: контейнер добавляет NAT-слой поверх NAT-слоя, который и так делает VPN-сервер для клиентов. Об этом — в разделе про ограничения.
WireGuard в Docker Compose
Для WireGuard проще всего использовать готовый образ linuxserver/wireguard — он сам генерирует конфиги клиентов и поднимает wg-quick внутри контейнера.
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- SERVERURL=vpn.example.com
- SERVERPORT=51820
- PEERS=3
- PEERDNS=auto
- INTERNAL_SUBNET=10.13.13.0
- ALLOWEDIPS=0.0.0.0/0
volumes:
- ./config:/config
- /lib/modules:/lib/modules
ports:
- "51820:51820/udp"
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
SYS_MODULE и монтирование /lib/modules нужны, только если ядро хоста уже содержит модуль WireGuard (начиная с 5.6 — как правило да) и контейнер должен убедиться, что он загружен. Если модуля нет, образ откатится на userspace-реализацию — она рабочая, но нагружает CPU заметнее ядрового варианта, точных цифр без замера на конкретном железе давать не буду.
После docker compose up -d конфиги клиентов появятся в ./config/peer1/peer1.conf и так далее — их же можно превратить в QR-код через docker exec wireguard /app/show-peer 1.
Если пиры не собираются на IP-адресе SERVERURL, стоит свериться с базовой установкой WireGuard без контейнера — логика конфигурации там та же, разница только в способе запуска: WireGuard на VPS.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверOpenVPN в Docker Compose
Для OpenVPN устоявшийся вариант — образ kylemanna/openvpn, который берёт на себя генерацию PKI (Easy-RSA) внутри volume.
Инициализация выполняется один раз, до первого up:
docker volume create --name openvpn-data
docker run -v openvpn-data:/etc/openvpn --rm kylemanna/openvpn \
ovpn_genconfig -u udp://vpn.example.com
docker run -v openvpn-data:/etc/openvpn --rm -it kylemanna/openvpn \
ovpn_initpki
Второй команде понадобится ввести пароль на CA-ключ — Compose здесь не подходит, нужен интерактивный -it. Дальше — обычный docker-compose.yml:
services:
openvpn:
image: kylemanna/openvpn
container_name: openvpn
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun
ports:
- "1194:1194/udp"
volumes:
- openvpn-data:/etc/openvpn
restart: unless-stopped
volumes:
openvpn-data:
external: true
Здесь важна связка devices: /dev/net/tun + cap_add: NET_ADMIN — без первого демон не создаст интерфейс tun0, без второго не сможет прописать в него маршруты. Клиентский .ovpn-файл после выдачи сертификата собирается командой:
docker run -v openvpn-data:/etc/openvpn --rm kylemanna/openvpn \
easyrsa build-client-full CLIENTNAME nopass
docker run -v openvpn-data:/etc/openvpn --rm kylemanna/openvpn \
ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn
Если после подключения клиент не видит интернет, причина почти всегда в отсутствии NAT-правила на самом контейнере или на хосте — тот же класс проблем, что и при установке без Docker, разбор — в статье OpenVPN на сервере: частые ошибки и решения.
IKEv2/strongSwan в Docker Compose
Для IKEv2 удобнее всего образ hwdsl2/ipsec-vpn-server — он собирает strongSwan и xl2tpd в одном контейнере и генерирует сертификаты сам.
services:
ipsec-vpn-server:
image: hwdsl2/ipsec-vpn-server
container_name: ipsec-vpn-server
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
volumes:
- ipsec-vpn-data:/etc/ipsec.d
- /lib/modules:/lib/modules:ro
ports:
- "500:500/udp"
- "4500:4500/udp"
environment:
- VPN_IPSEC_PSK=замените_на_свой_psk
- VPN_USER=vpnuser
- VPN_PASSWORD=замените_на_свой_пароль
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.rp_filter=0
- net.ipv4.conf.default.rp_filter=0
- net.ipv4.conf.all.accept_redirects=0
- net.ipv4.conf.default.accept_redirects=0
- net.ipv4.conf.all.send_redirects=0
- net.ipv4.conf.default.send_redirects=0
volumes:
ipsec-vpn-data:
Обратите внимание на rp_filter=0 — без отключения строгой проверки обратного пути пакеты IPsec от клиентов могут отбрасываться ядром хоста. Это тот сысктл, который проще один раз прописать в compose-файле, чем ловить «клиент подключился, трафика нет» через полчаса дебага.
Порты 500 и 4500 — UDP, и оба обязаны быть проброшены именно как UDP: если случайно оставить TCP по умолчанию из шаблона, IKEv2-хендшейк просто не дойдёт. Базовая логика самого протокола и выбор между ним и L2TP разобраны в статье IKEv2 или L2TP: что выбрать.
Сеть, capabilities и sysctls: что обязательно указать
Три вещи, без которых VPN-контейнер не заработает ни для одного из протоколов:
| Параметр | Зачем | Где ломается без него |
|---|---|---|
cap_add: NET_ADMIN | Право создавать интерфейсы, менять маршруты | Демон падает при старте или не создаёт tun/wg-интерфейс |
devices: /dev/net/tun (OpenVPN) | Доступ к устройству TUN | ovpn не может открыть /dev/net/tun: No such device |
sysctls: net.ipv4.ip_forward=1 | Разрешить пересылку пакетов между интерфейсами | Клиент подключается, но трафик дальше контейнера не идёт |
Отдельный вопрос — сетевой режим. По умолчанию Compose создаёт контейнеру bridge-сеть с NAT через docker-proxy для проброшенных портов. Для одиночного UDP-порта (WireGuard) это работает без сюрпризов. Но если протокол использует несколько портов и ожидает видеть реальный source-IP клиента (частый случай для IKEv2 с NAT-T), двойной NAT — NAT Docker поверх NAT провайдера — иногда даёт артефакты: часть клиентов подключается, часть — нет, в зависимости от их собственного NAT.
Обходной путь — network_mode: host:
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
network_mode: host
cap_add:
- NET_ADMIN
volumes:
- ./config:/config
restart: unless-stopped
В этом режиме контейнер использует сетевой стек хоста напрямую, ports: в compose-файле игнорируется — порты открываются так, как их открыл бы процесс на хосте. Это снимает проблему двойного NAT ценой изоляции: контейнер теперь видит все интерфейсы хоста, а не только свои. Для одиночного VPN-сервиса на выделенной под него машине это обычно приемлемый компромисс, для сервера с несколькими сетевыми сервисами — уже нет, там нужно разбираться точечно, какие именно порты дают проблему.
Ограничения контейнеризации VPN
Честно про то, где контейнер — не идеальное решение:
- Kernel-модуль WireGuard общий для хоста. Если на хосте запущено несколько WireGuard-контейнеров (или контейнер плюс нативная установка), они делят один и тот же модуль ядра — это не мешает работе, но означает, что версия WireGuard в ядре одна на всех, независимо от версии образа.
- Двойной NAT для некоторых протоколов. Разобрали выше — для IKEv2 и в меньшей степени для OpenVPN иногда нужен
network_mode: host, что частично убирает смысл изоляции контейнера. - PPTP и L2TP без IPsec почти не запускаются в непривилегированном контейнере — этим протоколам нужны ядерные модули и capabilities шире, чем
NET_ADMIN. Для L2TP/IPsec образhwdsl2работает, потому что явно собран под это; для голого PPTP готовых надёжных образов почти нет, и это скорее сигнал не использовать PPTP вообще — протокол устарел и с точки зрения безопасности. - Провайдеры с ограничением privileged-режима. Некоторые managed-платформы (не VPS с полным root-доступом, а PaaS-обёртки над Docker) запрещают
cap_add: NET_ADMINили/dev/net/tunиз соображений мультитенантной безопасности. На обычном выделенном VPS с root-доступом это ограничение не действует. - Резервное копирование — ваша забота. Ключи, сертификаты и PSK лежат в volume; если он не резервируется отдельно, пересборка контейнера с нуля означает переиздание сертификатов всем клиентам. Общие практики бэкапа volume применимы и здесь: бэкап Docker volume — частые ошибки и решения.
По производительности контейнеризация сама по себе не добавляет заметных накладных расходов на шифрование — это делает ядро или userspace-библиотека протокола, а не Docker. Но точных цифр по конкретной связке протокол/образ/железо без замера на своём сервере приводить не буду — слишком много переменных.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Нужен ли привилегированный режим (privileged: true) для VPN-контейнера?
Как правило нет — cap_add: NET_ADMIN (и SYS_MODULE, если нужна загрузка модуля ядра) покрывает требования WireGuard, OpenVPN и IKEv2. privileged: true даёт больше прав, чем реально нужно, и стоит избегать его без явной причины.
Можно ли запустить несколько VPN-протоколов на одном сервере в разных контейнерах?
Да, это одно из практических преимуществ подхода — WireGuard на 51820/udp, OpenVPN на 1194/udp и IKEv2 на 500+4500/udp одновременно, каждый в своём контейнере со своим volume, без конфликтов конфигурации на уровне ОС.
Что делать, если контейнер поднимается, но клиенты не могут подключиться?
Сначала проверить, что порт действительно проброшен как UDP (docker ps покажет маппинг), затем — что NET_ADMIN и нужные sysctls указаны в compose-файле, и только потом переходить к диагностике на уровне протокола — большинство типовых причин для конкретных протоколов разобраны в статьях про ошибки WireGuard и OpenVPN на сервере.
Обновлять VPN-сервер через docker compose pull && up -d безопасно?
Да, если конфиги и ключи лежат в volume, а не внутри контейнера — пересоздание контейнера с новым образом не тронет volume, сервис поднимется с той же конфигурацией. Перед обновлением на проде стоит убедиться, что volume действительно резервируется.
Что выбрать — WireGuard, OpenVPN или IKEv2 — для контейнерного разворачивания?
Выбор протокола не зависит от того, разворачиваете вы его в контейнере или нативно — критерии те же: скорость подключения, поддержка на клиентских устройствах, требования к обходу блокировок. Сравнение — в статье WireGuard или OpenVPN: что выбрать для сервера.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →