Как я одной командой прокси поднимал
TL;DR: docker-compose файл, поднимающий 3X-UI с настройкой пары входящих протоколов(VLESS, Hysteria2), маскировочным web-сервером с обновляемыми сертификатами, а также dnsproxy для предоставления DoH/DoT. Обкатывался аж на целой одной VPS, так что это скорее концепт, чем инструмент.
При использовании не забудьте создать рядом .env файл с переменными окружения (они описаны в начале compose-файла), а также каталог www, и положить туда маскировочный сайт (ну хотя бы непустой index.html). После запуска:
панель будет доступна по адресу
https://ваш.домен/admin/(если вы не задали иной путь);не забудьте изменить логин/пароль для панели с дефолтного
admin/adminпосле первого входа (Настройки панели - Аутентификация - Учётная запись администратора);стоит также на всякий пожарный отозвать API-ключ, использованный при настройке (Настройки панели - Аутентификация - API токен).

Началось всё с того, что я искал на Хабре, как сейчас поднимают базовый веб-сервер с TLS. Найденный комментарий @AlexGluck очень пригодился, а главное - натолкнул на мысль, до которой я сам, каюсь, не допёр: можно использовать "одноразовые" контейнеры для выполнения первоначальной настройки, используя общие тома и корректируя порядок выполнения зависимостями. Как говорится, "А что, так можно было?!"
Ну а зачем мне понадобился веб-сервер? Помимо прочего, чтобы было что использовать для маскировки VLESS+Reality. По моему опыту, VLESS без self-steal, т.е. маскирующийся под чужой популярный сайт, живёт недолго - а вот маскирующийся под сайт на том же сервере у меня живёт годами. Для создания туннеля я использую популярную панель 3xui, каковую было несложно добавить как ещё один контейнер. Вот только требовалась первоначальная настройка панели: сменить порт по умолчанию, после чего приходится править compose-файл; полученные сертификаты надо прописывать в настройках панели и в самом VLESS; создание Inbound по образцу с прописыванием SNI сервера... Дело усугубилось, когда из-за банов DoH/DoT провайдеров пришлось прикручивать dnsproxy, которому требовалось всё примерно то же самое.
"Как бы это автоматизировать? Ведь рано или поздно придётся переезжать на другой сервер..." подумал я.
Очень многое решается комбинацией двух механизмов docker-compose - подстановка переменных и встроенные конфиги. Это, конечно, изрядно раздувает compose файл - но зато позволяет использовать его как единое декларативное описание системы. Это позволяет легко указать, скажем, настройки nginx, которые стыкуются с настройками портов и томов. Да и с конфигурацией dnsproxy проблем не возникло.
К сожалению, 3xui хранит всю свою конфигурацию в базе SQLite. Через переменные окружения можно настроить только порт. А хотелось автоматизировать это более полно, потому что если оставить "сделаю сам" - точно забудешь. От идеи ставить клиент sqlite3 отказался сразу - структура базы непростая, и лезть в неё руками боязно. К счастью, у панели есть API. К несчастью, ключ от API надо было как-то получить.
Здесь пригодилась утилита x-ui для начального конфигурирования панели: она позволяет не только задать порт/логин/пароль/сертификаты, но и выпустить API-ключ для работы с панелью. Проблема была в том, как выполнить эту утилиту: чтобы выполнить команду в контейнере, нужно либо работать с хоста, либо прокидывать доступ к сокету docker внутрь контейнера. Оба варианта меня не устраивали. К счастью, удалось выкрутиться: используя для одноразового контейнера тот же образ, что и для самого 3xui, я получил доступ к утилите, а смонтировав тот же том, получил доступ к БД. База SQLite поддерживает параллельное открытие (полагаю, для того, чтобы утилита x-ui вообще работала), так что задать первоначальные настройки оказалось несложно. Вопрос был - как это всё автоматизировать.
Сначала я по инерции попытался использовать bash, благо образ 3xui содержит и curl, и wget. Но быстро упёрся в порог: подключения (Inbound) для клиентов я могу создать, а вот для создания клиента нужно знать ID свежесозданных подключений, которыми он будет пользоваться. Т.е. просто сделать запрос к API мало, нужно прочитать и распарсить JSON-ответ. Делать это башем не хотелось, а ИИшница (каюсь, с bash я знаком плохо) настойчиво предлагала использовать Питон. "Ну какой питон, дура железная? Вот, смотри, docker compose exec 3xui python3 --version , ну нет там..."
Python 3.14.7
...блин. Как обычно, час отладки спасает от пяти минут чтения доковисследования окружения. Ну зато получилось выкинуть Bash, и начать писать скрипт сразу на Питоне. По ходу написания было найдено несколько прибабахов, связанных с особенностью работы веб-сервера панели 3xui. В частности, он отвечает или строго по HTTP, или строго по HTTPS, но не делает редирект. Как следствие, первоначальная настройка потребовала пробовать оба протокола. Это до сих пор не реализовано как следует - если по уму, то, обнаружив ответ по HTTPS, нужно вообще не проводить первичную настройку сертификатов и прочего. До кучи, настройки панели применяются только все разом - так что сначала приходится получить все текущие настройки в виде одного JSON объекта, патчить их, а потом уже отправлять обратно исправленную версию.
Тем не менее, после того как получилось сделать перезапуск панели "изнутри" (через API) после задания сертификатов, портов, путей и прочего, стало возможным работать дальше. А дальше было создание входящих подключений. Среди них особую нишу занимает VLESS, так как через него в итоге шёл бы весь трафик к панели и к маскировочному сайту - он прозрачно прокидывает все "чужие" соединения на маскировочный сервер, в моем случае nginx. Поэтому выключать этот inbound не стоит.
Возникла ещё одна мелкая проблема - нужно было генерировать секреты (ключи, пароли, ID и т.п.), и подставлять их на нужные места. В итоге пришлось остановиться на наколенном шаблонизаторе, для которого Питон готовит данные. Грубо, но работает, и позволяет править конфиг VLESS или Hysteria, не слишком закапываясь в Python-код. Получив ответ с ID подключения, можно сформировать клиента по умолчанию. Почти готово...
...но тут меня ткнули носом в то, что отдельные (и хорошо известные) порты для панели и подписок выдают наличие 3xui на сервере. Пришлось пытаться убрать их, спрятав соответствующие эндпойнты за маскировочным nginx (он же для этого и стоит, если на то пошло). Без веселья не обошлось: красивая человеко-читаемая страница подписки ломается, если путь к подписке, заданный в настройках /3xui/, не совпадает с путём, заданным в nginx. Самое смешное, что сама HTML страница (ну и текстовая версия для клиентов) отдаются нормально, ломаются ассеты - скрипты и стили. В принципе, пофиг, раз уж текстовая версия работает (а ради неё всё и затевалось!), но перфекционизм заставил найти обходной путь.
Ещё одни забавные грабли заключались в том, что когда веб-интерфейс 3xui перестаёт смотреть во внешний мир напрямую, он начинает отдавать в подписках адрес сервера как 172.*.*.*, т.е. адрес из внутренней docker-сети. К счастью, это лечится передачей пары заголовков из nginx. Альтернатива - использовать механизм хостов (hosts) в 3xui, позволяющий прямо сказать "этот сервер доступен по такому-то, такому-то, и такому-то адресам". Но хосты нужно явно привязывать ко всем входящим подключениям, что усложнило бы дальнейшую донастройку... так что обход через заголовки от nginx мне кажется предпочтительнее.
В итоге получился файлик на 500+ строк, позволяющий поднять настроенный сервер с приличным уровнем маскировки под обычный сайт. Разумеется, остаются вопросы выбора протоколов (у меня на мобилке вообще работает только VLESS), объёма трафика (сразу много трафика на один мелкий сайт - подозрительно, на йоте улетаешь в бан через сутки) и утечек адреса туннеля через неприкрытый сетевой интерфейс (привет мобильным клиентам в первую очередь), но это уже конфигами не лечится...
Наверняка, есть что поправить, и в первую очередь - распилить это чудище на несколько файлов поменьше. Но пока работает, своё дело делает - и ладно.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.