MAATRIX / Блог / Docker Compose: стек нескольких VPN-протоколов сразу

Docker Compose: стек нескольких VPN-протоколов сразу

MAATRIX

Один протокол — это всегда компромисс: WireGuard быстрый, но его UDP-трафик легко детектируется DPI; Shadowsocks хорошо маскируется, но медленнее на слабом канале; Xray с Reality почти неотличим от обычного HTTPS, но сложнее в настройке. Вместо того чтобы выбирать один и потом жалеть, можно поднять все три на одном сервере через единый docker-compose.yml — и переключаться между ними по обстоятельствам без переустановки чего-либо.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Зачем держать три протокола на одном сервере

Смысл не в том, чтобы «использовать всё сразу» — это держать под рукой рабочие альтернативы, когда одна из них перестаёт работать. На практике встречаются три сценария:

  • Провайдер режет один протокол, но пропускает другой. WireGuard иногда режется по сигнатуре UDP-пакетов, а TCP-трафик Xray на 443 порту проходит без вопросов.
  • Разным устройствам удобны разные клиенты. На роутере — WireGuard (нативная поддержка в OpenWrt/Keenetic), на телефоне в поездке — Shadowsocks через простое приложение, на рабочем ноутбуке — Xray с Reality для максимальной маскировки.
  • Нужен запасной канал без второго сервера. Если аренда одной машины уже оплачена, три протокола на ней обходятся не дороже одного — расход ресурсов на них минимален.

Важная оговорка: это не балансировка нагрузки и не отказоустойчивость в классическом смысле — если упадёт сам сервер, упадут все три. Задача архитектуры — пережить блокировку конкретного протокола, а не аппаратный отказ.

Перед тем как собирать стек, стоит определиться, какие протоколы вообще нужны — по сценариям использования это разобрано в статье какой протокол VPN выбрать.

Структура проекта и docker-compose.yml целиком

Заводим на сервере одну директорию под весь стек:

mkdir -p ~/vpn-stack/{wireguard,shadowsocks,xray}
cd ~/vpn-stack

Файл docker-compose.yml — три сервиса, три разных порта, у каждого свой том с конфигом:

services:
  wireguard:
    image: linuxserver/wireguard:latest
    container_name: wg
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Moscow
      - SERVERURL=auto
      - SERVERPORT=51820
      - PEERS=3
      - PEERDNS=1.1.1.1
      - INTERNAL_SUBNET=10.13.13.0
    volumes:
      - ./wireguard/config:/config
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped

  shadowsocks:
    image: shadowsocks/shadowsocks-libev:latest
    container_name: ss
    environment:
      - PASSWORD=замените-на-длинный-пароль
      - METHOD=chacha20-ietf-poly1305
      - SERVER_PORT=8388
    ports:
      - "8388:8388/tcp"
      - "8388:8388/udp"
    restart: unless-stopped

  xray:
    image: teddysun/xray:latest
    container_name: xray
    volumes:
      - ./xray/config.json:/etc/xray/config.json:ro
    ports:
      - "443:443/tcp"
    restart: unless-stopped

Три независимых сервиса без общей internal-сети — каждый публикует свой порт напрямую на хост. Это осознанное решение: протоколы не должны зависеть друг от друга, чтобы падение одного контейнера не задело остальные. Если позже понадобится общий мониторинг или единый лог-агрегатор, для него можно завести отдельную bridge-сеть — но сами VPN-сервисы в ней быть не обязаны.

Арендуйте сервер под свои задачи!

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

WireGuard: конфиг и особенности контейнера

Образ linuxserver/wireguard генерирует конфиги пиров сам при первом запуске — по числу PEERS=3 создастся три клиентских конфига с QR-кодами. SERVERURL=auto заставляет контейнер подставить внешний IP сервера автоматически, но надёжнее прописать его явно, если у сервера несколько сетевых интерфейсов.

Два нюанса, которые часто ломают запуск:

  • cap_add: SYS_MODULE и монтирование /lib/modules нужны, чтобы контейнер мог подгрузить модуль ядра wireguard, если он ещё не загружен на хосте. На современных ядрах (5.6+) модуль обычно уже встроен, и эта строка становится избыточной, но не мешает.
  • sysctls: net.ipv4.conf.all.src_valid_mark=1 обязателен — без него пакеты с исходящим маркером роутинга WireGuard будут отбрасываться ядром хоста внутри контейнера.

После первого старта конфиги клиентов лежат в ./wireguard/config/peer1/peer1.conf — их забирают на устройства обычным образом, как описано в статье как установить и настроить WireGuard на VPS.

Shadowsocks и Xray: конфиги на разных портах

Shadowsocks в этой сборке настраивается через переменные окружения образа — конфиг-файл не нужен, всё в docker-compose.yml. Единственное, что стоит поменять сразу, — пароль и, при желании, метод шифрования (chacha20-ietf-poly1305 — разумный баланс скорости и совместимости с клиентами).

Xray конфигурируется файлом ./xray/config.json. Минимальный рабочий вариант с VLESS и Reality на 443 порту:

{
  "log": { "loglevel": "warning" },
  "inbounds": [
    {
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [
          { "id": "вставьте-свой-uuid", "flow": "xtls-rprx-vision" }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "dest": "www.microsoft.com:443",
          "serverNames": ["www.microsoft.com"],
          "privateKey": "вставьте-свой-приватный-ключ",
          "shortIds": [""]
        }
      }
    }
  ],
  "outbounds": [{ "protocol": "freedom" }]
}

UUID генерируется командой docker run --rm teddysun/xray xray uuid, пара ключей Reality — docker run --rm teddysun/xray xray x25519. Подробный разбор параметров Reality и типичных ошибок при выборе dest — в статье Xray и VLESS Reality на сервере; там же логика выбора домена-маски, которая здесь не дублируется.

Ключевая мысль этого раздела: Xray занимает 443, тот же порт, что обычно слушает веб-сервер. Если на сервере уже крутится сайт на nginx, придётся либо переносить сайт на другой порт, либо ставить Xray в режиме fallback перед nginx (Xray слушает 443 снаружи и сам проксирует не-VPN-трафик на локальный веб-сервер) — это отдельная настройка streamSettings.tcpSettings с секцией acceptProxyProtocol, в базовый compose-файл она не входит.

Сеть, порты и файрвол

Три протокола — три разных порта, и файрвол должен пропускать все три одновременно, не выдавая лишнего:

СервисПортПротоколЧто видно снаружи
WireGuard51820UDPСлучайный UDP-трафик — без TLS-подписи detectable по паттерну
Shadowsocks8388TCP+UDPЗашифрованный поток, похож на случайные данные
Xray (VLESS+Reality)443TCPНеотличим от обычного HTTPS-соединения к www.microsoft.com

Правила ufw:

ufw allow 51820/udp
ufw allow 8388/tcp
ufw allow 8388/udp
ufw allow 443/tcp
ufw enable

Если сервер в России или другой юрисдикции с активной DPI-фильтрацией, порт WireGuard стоит держать нестандартным (не 51820, а что-то вроде 43127) — сигнатура протокола всё равно детектируется по содержимому пакетов, но нестандартный порт хотя бы убирает его из автоматических списков блокировки по умолчанию.

Отдельно стоит учитывать, что Docker по умолчанию сам управляет правилами iptables и может открывать порты в обход настроек ufw, если не указать iptables: false в /etc/docker/daemon.json или не использовать ufw-docker. Это частая причина, когда порт «закрыт» в ufw status, а снаружи всё равно доступен, — стоит проверить нужным ли образом настроен именно docker-слой файрвола, а не только системный.

Запуск, автозапуск и логи

Поднимаем весь стек одной командой:

docker compose up -d
docker compose ps

Проверка, что порты слушаются и на хосте, и внутри контейнеров:

ss -tulnp | grep -E '51820|8388|443'
docker compose logs -f xray

restart: unless-stopped в каждом сервисе гарантирует автозапуск при перезагрузке хоста и рестарт при падении процесса — но не спасает от ошибок в конфиге: если Xray упадёт из-за битого JSON, он будет уходить в restart loop бесконечно. Здесь помогает healthcheck на уровне compose:

    healthcheck:
      test: ["CMD", "sh", "-c", "nc -z localhost 443 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3

Такую секцию стоит добавить в сервис xray (и по аналогии в остальные), чтобы docker compose ps сразу показывал unhealthy, а не просто «running» у контейнера, который на самом деле не принимает соединения.

Для долгосрочной эксплуатации не помешает внешний мониторинг доступности всех трёх портов — например, через Uptime Kuma, который умеет проверять и TCP-порт, и полноценный TLS-handshake на 443, что для Xray с Reality важнее, чем просто «порт открыт».

Если через какое-то время решите, что один из трёх протоколов не нужен и лучше сосредоточиться на одном — например, полностью перейти на Xray и вывести клиентов с WireGuard, — сам перенос конфигов и трафика можно сделать постепенно, не отключая рабочий протокол до тех пор, пока новый не проверен на всех устройствах.

Арендуйте сервер под свои задачи!

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли запускать все три сервиса на одном IP без конфликта портов?

Да, конфликт возникает только если два сервиса пытаются занять один и тот же порт. WireGuard на UDP/51820, Shadowsocks на TCP+UDP/8388, Xray на TCP/443 — пересечений нет, docker сам проверит это при up и откажется стартовать при коллизии.

Что если 443 порт уже занят веб-сервером?

Тогда Xray и веб-сервер конфликтуют за один порт напрямую — либо переносите сайт на 8443 и настраивайте редирект через сам Xray (fallback на локальный nginx), либо ставьте Xray на другой порт (например, 2053) и принимайте, что маскировка под обычный HTTPS-трафик станет менее убедительной для DPI, которая часто и так смотрит именно на 443.

Нужно ли всем трём сервисам одинаковое количество ресурсов?

Нет. WireGuard работает в ядре и почти не потребляет CPU контейнера, Shadowsocks лёгкий, Xray с Reality чуть тяжелее из-за TLS-обвязки, но на VPS с 1 vCPU и 1-2 ГБ RAM все три сервиса одновременно работают без проблем при обычной нагрузке в несколько активных клиентов.

Как обновлять образы без даунтайма всех трёх сразу?

Обновляйте по одному: docker compose pull wireguard && docker compose up -d wireguard, затем то же самое для остальных сервисов по очереди — так недоступным будет только обновляемый протокол, а не весь стек.

Стоит ли выносить конфиги трёх протоколов в docker secrets вместо переменных окружения?

Для пароля Shadowsocks и приватного ключа Xray это разумная практика, особенно если сервер используется не только вами — открытый docker-compose.yml с паролем в переменной окружения виден любому, у кого есть доступ на чтение файла. Подход описан в статье Docker secrets: управление паролями.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →