Эта статья является прямым продолжением вот этой статьи про мой проект ServeHub-2, которое охватывает релиз 1.4.0 и отчасти 2.0.0, 2.1.0 (а также определенные подробности, которые не упоминались в прошлой статье). Здесь будут подробно объясняться моменты в том порядке, в котором они появлялись в проекте.
Также изначально планировалось в этой же статье рассказать про Go Wails установщик, но тогда она получается слишком большой, поэтому про это будет в следующей части.
Сборка amnezia контейнера для клиента
Так как amnezia не предоставляет готовые cli решения для клиента, для сервера пришлось собирать все самостоятельно. Ниже будут файлы с пояснениями
docker-compose.local.yaml:
amnezia-client: build: context: . dockerfile: Dockerfile.amnezia # dockerfile в котором билдится образ container_name: amnezia-client restart: unless-stopped cap_add: # выдаем контейнеру конкретные привилегии Linux ядра, вместо того чтобы выдавать полные root права - NET_ADMIN # позволяет контейнеру управлять сетевыми настройками хоста (создание интерфейсов, настройка таблиц маршрутизации, настраивать правила фильтрации трафика) - SYS_MODULE # дает возможность загружать и выгружать модули ядра Linux на хостовой системе (для работы Ansible заранее скачивает нужные модули ядра amnezia: https://github.com/amnezia-vpn/amneziawg-linux-kernel-module) privileged: true # дает возможность получать root права devices: - /dev/net/tun:/dev/net/tun # специальный виртуальный файл в Linux. Используется программами для создания соединений. Вместо отправки данных через физический кабель, программы отправляют их в этот файл, где они шифруются volumes: - ./apps-data/amnezia:/config # прокидываем файл awg0.conf - /lib/modules:/lib/modules:ro # прокидываем модули ядра logging: *default-logging network_mode: "host" # контейнер использует сетевой стек компьютера напрямую healthcheck: test: ["CMD-SHELL", "awg show | grep interface || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 10s
Dockerfile.amnezia:
FROM golang:1.24.5 AS builder # первый этап сборки, используем go для установки amnezia-go (нужен на всякий случай, если что-то случится с модулем ядра, так как он запускается полностью самостоятельно)ARG TARGETARCH # переменные сборки, docker автоматически подставляет туда архитектуру процессораARG TARGETVARIANTWORKDIR /amneziawg-go # рабочая директорияRUN git clone https://github.com/amnezia-vpn/amneziawg-go.git . --depth=1 \ && go mod download \ # после клонирования качаем зависимости && go mod verify \ && CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH GOARM=${TARGETVARIANT#v} \ # Отключает использование библиотек Си. Также указывает компилятору, под какую целевую ОС и архитектуру процессора нужно собрать файл go build -ldflags '-s -w' -v -o amneziawg-go # компилируем бинарник. Флаг -ldflags '-s -w' удаляет из готового файла отладочную информацию.FROM alpine:3.22.1 # собираем финальный образARG TARGETARCHARG AWGTOOLS_RELEASE=1.0.20250706 #версияRUN apk --no-cache add \ iproute2 \ iptables \ bash \ openresolv \ dumb-init \ && apk --no-cache add --virtual .build-deps \ dpkg \ wget \ unzip \ git \ build-base \ linux-headers # устанавливаем зависимостиRUN git clone https://github.com/amnezia-vpn/amneziawg-tools.git /amneziawg-tools --depth=1 \ # клонируем консольные утилиты && cd /amneziawg-tools/src \ && make \ # компилируем утилиты из исходного кода && make install \ && ln -s /usr/local/bin/awg /usr/bin/wg \ # создаем ярлыки (если будет вызываться wg, перенаправим на awg) && ln -s /usr/local/bin/awg-quick /usr/bin/wg-quick \ && cd / \ && rm -rf /amneziawg-tools \ # удаляем исходники && apk del .build-deps # удаляем компиляторы, которые ставили временноCOPY --from=builder /amneziawg-go/amneziawg-go /usr/bin/ # копируем бинарник go версии с предыдущего этапа сборкиCOPY scripts/amnezia-client_init.sh /init.shRUN chmod +x /init.shENTRYPOINT ["/usr/bin/dumb-init", "/init.sh"] # запускаем скрипт. dumb-init встает на место главного процесса PID 1 и запускает скрипт, слушает команды Docker и транслирует внутрь клиента. Dumb-init нужен, так как bash скрипты сами по себе не умеют управлять дочерними процессами и обрабатывать системные сигналы.
amnezia-client_init.sh
#!/bin/bashfind /etc/amnezia/amneziawg -mindepth 1 -delete # удаляем все файлы в директории, гарантирует чистый стартCOUNTER=0 # для проверки удалось ли что-нибудь найтиfor s in $(find /config -name "*.conf") # ищем конфигиdo if test -f ${s} # убеждаемся что это файл then COUNTER=$(( COUNTER + 1 )) basename=$(basename ${s}) # извлекаем имя файла name=${basename%.conf} echo awg interface "${name}" will be created from config file "${basename}" cp ${s} /etc/amnezia/amneziawg/${name}.conf # копируем себе chmod 600 /etc/amnezia/amneziawg/${name}.conf # выдаем права: чтение и запись только для root awg-quick up ${name} # главная команда, которая создаем виртуальный сетевой интерфейс с именем файла fidoneif [[ $COUNTER -lt 1 ]]then echo "There are no config files in the /config folder"fisleep infinity # контейнер живет до тех пор, пока работает главный процесс PID 1
Появление Gitea + настройки в Nginx
docker-compose.local.yaml:
gitea: image: gitea/gitea:1.26 container_name: gitea logging: *default-logging environment: - USER_UID=1000 - USER_GID=1000 - GITEA__server__ROOT_URL=https://${SERVER_NAME}/gitea/ # эти переменные служат для генерации файла конфига .ini (формат GITEA__SECTION__KEY) - GITEA__server__DISABLE_SSH=true # отключам ssh чтобы не пробрасывать через nginx, так как работает в vpn сети, можно спокойно пользоваться https - GITEA__database__DB_TYPE=sqlite3 - GITEA__database__PATH=/data/gitea/gitea.db - GITEA__security__INSTALL_LOCK=true # отключаем страницу первоначальной установки, сразу запуская gitea в рабочем режиме volumes: - ./apps-data/gitea:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro expose: - 3000 # даем доступ только в docker сети для nginx restart: unless-stopped
nginx.remote.conf.template: (location который перенаправляет трафик на локальные сервисы)
location ~* ^/(navidrome|audiobookshelf|nextcloud|gitea|vaultwarden|grafana|portainer|borg-ui|v2) { # перенаправляем запрос на Nginx локального сервера внутри vpn# оригинальный путь (URI) передается автоматически, что позволит локальному Nginx распределить запросы по сервисамproxy_pass http://10.8.0.2:80; proxy_http_version 1.1; # пробрасываем заголовки для поддержки WebSockets (необходимо для Vaultwarden, Portainer и др.)proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; # передаем оригинальные данные клиента, чтобы конечные сервисы знали реальный IP и доменproxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; # сообщаем сервисам, что внешнее соединение защищено https (предотвращает проблемы с безопасностью и редиректами)proxy_set_header X-Forwarded-Proto https; # отключаем буферизацию, чтобы данные передавались мгновенно в реальном времени # это критично для стриминга медиа и загрузки больших файлов через Nextcloud.proxy_buffering off; proxy_request_buffering off; # увеличиваем таймауты до 1 часа, чтобы не рвались веб-сокеты и долгие загрузки файловproxy_read_timeout 3600; keepalive_timeout 3600; }
Также вы могли заметить v2 путь. Он нужен для того, чтобы использовать gitea registry, так как docker push по спецификации всегда добавляет в путь /v2/, а так как gitea в моем случае находится не в корне домена, без этого все ломается.
К примеру: docker делает запрос и получается путь https://server.name/v2/, при этом gitea находится на https://server.name/gitea/. Соответственно нужно просто “обмануть” docker чтобы /v2/ шел на локальный сервер, и там уже шел в gitea, как если бы docker обратился по /gitea/.
nginx.local.conf.template: (location для gitea на локальном сервере)
# ведем оба пути в ту же самую gitea, только тут есть небольшой нюанс, который позволяет всему этому работать location /gitea/ { # слэш на конце "http://gitea:3000/" отрезает префикс "/gitea/" из запроса. # например, запрос к "/gitea/explore" уйдет в контейнер как "/explore". proxy_pass http://gitea:3000/; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_buffering off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } location /v2 { # слэша на конце нет, поэтому Nginx проксирует путь "/v2" без изменений. # запрос "/v2/token" уйдет в контейнер именно как "/v2/token", где его обработает реестр Gitea. proxy_pass http://gitea:3000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; }
Redis для Nextcloud
Здесь кратко пройдусь по финальному виду docker-compose файла для Nextcloud с описанияеми
docker-compose.local.yaml:
nextcloud-db: image: postgres:18 container_name: nextcloud-db logging: *default-logging restart: unless-stopped volumes: - ./apps-data/postgres/nextcloud:/var/lib/postgresql environment: - POSTGRES_DB=${POSTGRES_DB_NEXTCLOUD} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} expose: - 5432 healthcheck: # проверка здоровья БД: pg_isready проверяет, готов ли сервер принимать подключения test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 5s timeout: 5s retries: 5 deploy: resources: limits: memory: 1G nextcloud-redis: image: redis:8.8.0 container_name: nextcloud-redis restart: unless-stopped # устанавливаем пароль прямо в команде запуска сервера для безопасности command: redis-server --requirepass ${NEXTCLOUD_REDIS_PASS} logging: *default-logging deploy: resources: limits: memory: 1G nextcloud: image: nextcloud:34.0.0-apache container_name: nextcloud logging: *default-logging restart: unless-stopped expose: - 80 depends_on: # запуск Nextcloud начнется только тогда, когда база пройдет проверку здоровья (healthcheck) nextcloud-db: condition: service_healthy nextcloud-redis: condition: service_started environment: - POSTGRES_HOST=nextcloud-db # OVERWRITEWEBROOT важен, так как Nextcloud запущен не в корне домена, а в /nextcloud - OVERWRITEWEBROOT=/nextcloud - OVERWRITEPROTOCOL=https # список доверенных прокси (включая ваш VPN 10.8.0.0/24), чтобы Nextcloud # корректно определял IP клиента через заголовки X-Forwarded-For - TRUSTED_PROXIES=172.16.0.0/12 10.8.0.0/24 # настройка кэширования: APCu для локального кэша, Redis для блокировки файлов - 'NC_memcache.local=\OC\Memcache\APCu' - 'NC_memcache.locking=\OC\Memcache\Redis' volumes: - ./apps-data/nextcloud/html:/var/www/html - /home/${LOCAL_USER}/PersonalData/NextcloudData:/var/www/html/data deploy: resources: limits: memory: 3G healthcheck: # проверка: существует ли конфиг и работает ли PHP-утилита OCC (Nextcloud CLI) test: ["CMD-SHELL", "test -f /var/www/html/config/config.php && php occ status"] interval: 1m30s timeout: 30s retries: 5 start_period: 30s nextcloud-cron: image: nextcloud:34.0.0-apache container_name: nextcloud-cron restart: unless-stopped # этот контейнер только для запуска фоновых задач (очистка кэша, превью, индексация) # это разгружает основной процесс веб-сервера entrypoint: /cron.sh logging: *default-logging depends_on: nextcloud-db: condition: service_healthy nextcloud: condition: service_healthy deploy: resources: limits: memory: 512M
Исправления ошибок
Cборка модулей для 7.1.3-arch1-2
Проблема заключалась в том, что при обновлении ядра Linux одна из внутренних функций изменила свое название, из-за чего модуль amneziawg перестал компилироваться (он все еще пытался использовать старый вызов). Стоит отметить, что из-за быстрого обновления ядра в Arch Linux подобные ситуации с внешними модулями случаются, и проект не всегда может корректно собраться автоматически без вмешательства.
Чтобы это исправить я добавил в Ansible такой шаг:
- name: "[AmneziaWG] Arch: исправление совместимости с ядром 7.x+ (ipv6_stub)" block: - name: "[AmneziaWG] Arch: проверяем наличие исходников в /usr/src" ansible.builtin.stat: path: /usr/src/amneziawg-1.0.0/socket.c register: amneziawg_source # записываем результат есть ли исходники - name: "[AmneziaWG] Arch: замена ipv6_stub на ip6_dst_lookup_flow" ansible.builtin.replace: path: /usr/src/amneziawg-1.0.0/socket.c regexp: 'ipv6_stub->ipv6_dst_lookup_flow' # находим функцию и меняем название replace: 'ip6_dst_lookup_flow' register: patch_result # пишем удалось ли перезаписать when: amneziawg_source.stat.exists # выполняется только если найдены исходники - name: "[AmneziaWG] Arch: принудительная пересборка модуля после патча" ansible.builtin.shell: | dkms remove amneziawg/1.0.0 --all || true # удаляем загруженный в оперативку модуль dkms add -m amneziawg -v 1.0.0 || true # добавляем обратно dkms build -m amneziawg -v 1.0.0 # билдим dkms install -m amneziawg -v 1.0.0 # устанавливаем become: true # используем root when: patch_result.changed when: ansible_facts['os_family'] == "Archlinux"
Если что dkms — это фреймворк, который позволяет автоматически пересобирать модули ядра при каждом обновлении самого ядра в системе. Ansible-скрипт в данном случае просто патчит код и инициирует пересборку.
Nginx и Loki не запускаются после перезагрузки сервера
Проблема заключалась в том, что в docker-compose файле Nginx и Loki были привязаны к IP-адресу VPN-туннеля (10.8.0.2). При старте системы Docker пытался запустить контейнеры раньше, чем сетевой интерфейс VPN успевал подняться. Из-за отсутствия нужного IP на хосте контейнеры аварийно завершались.
Чтобы это решить я добавил в Ansible такой шаг:
- name: "[Net] Разрешить привязку к несуществующим IP (ip_nonlocal_bind)" ansible.posix.sysctl: # используем sysctl name: net.ipv4.ip_nonlocal_bind value: '1' # ставим значение 1 (раньше было 0) state: present # Убеждаемся, что параметр установлен reload: yes # Применяем изменения параметров ядра немедленно
Заключение
Подробно рассказал про все изменения, которые были сделаны и про исправления ошибок, а также про то, что упустил в предыдущей части. Следующая статья будет полностью посвящена Go Wails установщику, который автоматизирует установку и разворачивание сервисов на серверах.
Вся кодовая база проекта, подробная документация, инструкции по развертыванию открыты и доступны для сообщества:
🔗 GitHub-репозиторий: https://github.com/canntstand/ServeHub-2
Буду рад вашему фидбеку в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/1059486/