diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index c18ff1e..c3b0439 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -159,7 +159,7 @@ jobs: '{tag_name:$tag, name:$name, body:$notes, prerelease:$pre, draft:false}')" id="$(curl -fsS -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \ -d "$body" "$API/releases" | jq -r .id)" - for f in "$APK" "$APK.sha256" deploy/compose.yaml deploy/.env.example deploy/install.sh deploy/update.sh; do + for f in "$APK" "$APK.sha256" deploy/compose.yaml deploy/compose.tls.yaml deploy/Caddyfile deploy/.env.example deploy/install.sh deploy/update.sh; do curl -fsS -X POST -H "Authorization: token $TOKEN" \ -F "attachment=@$f" "$API/releases/$id/assets?name=$(basename "$f")" > /dev/null echo "uploaded $(basename "$f")" diff --git a/README.md b/README.md index 09ac108..a88e830 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Self-hosted музыкальный сервис: веб и Android-прилож - Идея продукта: [docs/IDEA.md](docs/IDEA.md) - Документация для разработки: [docs/README.md](docs/README.md) - План: [docs/roadmap.md](docs/roadmap.md) +- Установка сервера на свой домен: [docs/install-guide.md](docs/install-guide.md) ## Структура diff --git a/deploy/.env.example b/deploy/.env.example index cd3bc0a..8ab0df3 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,9 +1,17 @@ # Скопируйте в .env (install.sh делает это сам и генерирует секреты). Описание — docs/deployment.md. # --- Образы --- +# Реестр Gitea: <хост>/<владелец>, например gitea.hsrv.site/mrleo1nid REGISTRY=git.example.lan/owner PIPU_VERSION=0.1.0 PIPU_DATA=/srv/pipu + +# --- Домен и HTTPS (docs/install-guide.md) --- +# Вариант А: встроенный Caddy с Let's Encrypt — оставьте строку COMPOSE_FILE и заполните DOMAIN, ACME_EMAIL. +# Вариант Б: свой reverse proxy — закомментируйте COMPOSE_FILE и направьте прокси на HTTP_PORT. +COMPOSE_FILE=compose.yaml:compose.tls.yaml +DOMAIN=music.example.com +ACME_EMAIL=admin@example.com HTTP_PORT=80 # --- Секреты (install.sh заполнит случайными значениями) --- @@ -11,6 +19,10 @@ POSTGRES_PASSWORD= MEILI_MASTER_KEY= INTERNAL_TOKEN= +# --- Первый администратор (необязательно; иначе пароль генерируется и выводится в лог api один раз) --- +#BOOTSTRAP_ADMIN_USERNAME=admin +#BOOTSTRAP_ADMIN_PASSWORD= + # --- api / recommender --- # DATABASE_URL собирается в compose.yaml из POSTGRES_PASSWORD REDIS_URL=redis://redis:6379/0 diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..130e728 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,18 @@ +# Caddy: автоматический HTTPS для Pipu Music. Переменные берутся из .env через compose.tls.yaml. +{ + email {$ACME_EMAIL} +} + +{$DOMAIN} { + encode zstd gzip + + # Загрузки в очередь на добавление идут чанками по 8 МБ (tus) + request_body { + max_size 16MB + } + + reverse_proxy nginx:80 { + # не буферизовать стриминг аудио + flush_interval -1 + } +} diff --git a/deploy/compose.tls.yaml b/deploy/compose.tls.yaml new file mode 100644 index 0000000..723199c --- /dev/null +++ b/deploy/compose.tls.yaml @@ -0,0 +1,24 @@ +# HTTPS на своём домене: Caddy получает и продлевает сертификат Let's Encrypt и проксирует на nginx. +# Подключается через COMPOSE_FILE=compose.yaml:compose.tls.yaml в .env (docs/install-guide.md). +# Если перед сервером уже стоит свой reverse proxy (Traefik, NPM, nginx) — этот файл не нужен. + +services: + nginx: + # наружу смотрит только Caddy + ports: !reset [] + + caddy: + image: caddy:2.10-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + environment: + DOMAIN: ${DOMAIN:?укажите DOMAIN в .env} + ACME_EMAIL: ${ACME_EMAIL:?укажите ACME_EMAIL в .env — на него Let's Encrypt пишет о проблемах с сертификатом} + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ${PIPU_DATA:-/srv/pipu}/caddy/data:/data + - ${PIPU_DATA:-/srv/pipu}/caddy/config:/config + depends_on: [nginx] diff --git a/deploy/install.sh b/deploy/install.sh index e9f98bd..c36f4f8 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" DATA="${PIPU_DATA:-/srv/pipu}" echo "→ каталоги в $DATA" -mkdir -p "$DATA"/{library,library-host,cache/transcode,cache/covers,postgres,meili,redis,models,updates,uploads,backups} +mkdir -p "$DATA"/{library,library-host,cache/transcode,cache/covers,postgres,meili,redis,models,updates,uploads,backups,caddy/data,caddy/config} # storage-agent монтирует NFS внутрь $DATA/library; чтобы точки монтирования видели другие контейнеры, # каталог должен быть отдельной точкой монтирования с shared propagation. @@ -44,7 +44,7 @@ if [ ! -f .env ]; then cp .env.example .env sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|; s|^MEILI_MASTER_KEY=.*|MEILI_MASTER_KEY=$(secret)|; s|^INTERNAL_TOKEN=.*|INTERNAL_TOKEN=$(secret)|; s|^PIPU_DATA=.*|PIPU_DATA=$DATA|" .env chmod 600 .env - echo " Проверьте REGISTRY и PIPU_VERSION в .env" + echo " Заполните в .env: REGISTRY, PIPU_VERSION, DOMAIN, ACME_EMAIL" fi echo "Готово. Запуск: ./update.sh \$(grep ^PIPU_VERSION= .env | cut -d= -f2)" diff --git a/deploy/nginx/default.conf.template b/deploy/nginx/default.conf.template index 11dfeb9..8e04d8f 100644 --- a/deploy/nginx/default.conf.template +++ b/deploy/nginx/default.conf.template @@ -1,5 +1,11 @@ # Шаблон обрабатывается envsubst при старте контейнера nginx (переменные: API_UPSTREAM). -# TLS терминируется здесь или на внешнем прокси — см. docs/deployment.md. +# HTTPS терминирует внешний прокси: Caddy из compose.tls.yaml или ваш собственный — см. docs/install-guide.md. + +# За внешним прокси (Caddy, Traefik, ...) сохраняем исходную схему и IP клиента. +map $http_x_forwarded_proto $pipu_scheme { + default $http_x_forwarded_proto; + '' $scheme; +} upstream pipu_api { server ${API_UPSTREAM}; @@ -10,6 +16,13 @@ server { listen 80; server_name _; + # Доверяем X-Forwarded-For только от частных сетей (docker, LAN), где стоит прокси. + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + root /usr/share/nginx/html; client_max_body_size 16m; @@ -23,7 +36,7 @@ server { 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-Proto $scheme; + proxy_set_header X-Forwarded-Proto $pipu_scheme; } location = /healthz { proxy_pass http://pipu_api; } diff --git a/docs/README.md b/docs/README.md index 1608099..235249d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,8 @@ Pipu Music — self-hosted музыкальный сервис (веб + Android | [i18n.md](i18n.md) | Локализация (ru / en, расширяемость) | | [testing.md](testing.md) | Стратегия тестирования | | [ci-cd.md](ci-cd.md) | Gitea Actions: проверки и релиз по тегу | -| [deployment.md](deployment.md) | Docker Compose, конфигурация, бэкапы | +| [install-guide.md](install-guide.md) | **Пошагово:** установка сервера на домен с HTTPS, подключение веба и Android | +| [deployment.md](deployment.md) | Справочник по развёртыванию: сервисы, каталоги, переменные, бэкапы | | [conventions.md](conventions.md) | Соглашения по коду, git, версиям | | [roadmap.md](roadmap.md) | Этапы разработки и критерии готовности | diff --git a/docs/deployment.md b/docs/deployment.md index f1465fd..f789213 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,5 +1,8 @@ # Развёртывание +> Пошаговая инструкция «с нуля до работающего сервера на домене» — [install-guide.md](install-guide.md). +> Здесь — справочник: требования, каталоги, сервисы, переменные. + ## Требования к серверу - Linux x86_64 (нужен mount propagation для storage-agent), Docker 24+, Docker Compose v2. @@ -22,7 +25,8 @@ ├── models/ # модели Essentia (скачиваются при первом старте analyzer) ├── updates/ # кэш APK релизов ├── uploads/ # staging очереди на добавление (размер ≈ лимит очереди, по умолчанию 10 ГБ) -└── backups/ +├── backups/ +└── caddy/ # сертификаты Let's Encrypt (если используется compose.tls.yaml) ``` Перед первым запуском: `mkdir -p /srv/pipu/library && mount --bind /srv/pipu/library /srv/pipu/library && mount --make-rshared /srv/pipu/library` @@ -125,8 +129,16 @@ services: ## TLS -- Свой домен — Let's Encrypt (через отдельный certbot / acme.sh или Caddy перед nginx). -- Локальная сеть — свой CA; сертификат CA нужно установить на устройства или включить пиннинг в мобильном (см. mobile.md). +- **Свой домен (рекомендуется)** — `deploy/compose.tls.yaml` добавляет Caddy, который получает и продлевает сертификат + Let's Encrypt для `DOMAIN` и проксирует на nginx; nginx тогда наружу не открыт. Включается строкой + `COMPOSE_FILE=compose.yaml:compose.tls.yaml` в `.env`. +- **Свой reverse proxy** (Traefik, NPM, nginx) — без `compose.tls.yaml`, прокси направляется на `HTTP_PORT` + (например `127.0.0.1:8081`); нужны `X-Forwarded-*`, тело до 16 МБ, без буферизации ответов. +- nginx внутри образа `web` берёт схему и IP клиента из `X-Forwarded-Proto` / `X-Forwarded-For`, если запрос пришёл + из частной сети (docker, LAN). +- Локальная сеть без домена — свой CA; сертификат CA нужно установить на устройства или включить пиннинг в мобильном (см. mobile.md). + +Пошаговая установка на домен — [install-guide.md](install-guide.md). ## Бэкапы diff --git a/docs/install-guide.md b/docs/install-guide.md new file mode 100644 index 0000000..55a1728 --- /dev/null +++ b/docs/install-guide.md @@ -0,0 +1,281 @@ +# Установка сервера и подключение приложений + +Пошаговая инструкция: от чистого Linux-сервера до работающего Pipu Music на своём домене +с HTTPS, плюс подключение веб- и Android-клиента. Справочник по устройству развёртывания — +[deployment.md](deployment.md). + +В примерах: +- домен — `music.example.com`, замените на свой; +- репозиторий — `gitea.hsrv.site/mrleo1nid/pipu-music`; +- реестр образов — `gitea.hsrv.site/mrleo1nid`. + +> **Что умеет текущая версия.** В `v0.1.0` (этап M0) работают развёртывание, HTTPS и проверка связи +> клиентов с сервером. Вход, библиотека и плеер появятся в следующих версиях +> (см. [roadmap.md](roadmap.md)). Шаги, которые зависят от версии, помечены. + +--- + +## 1. Что нужно заранее + +| Что | Требование | +|---|---| +| Сервер | Linux x86_64 (Debian 12 / Ubuntu 22.04+), 4+ ядра, 8+ ГБ RAM, 20+ ГБ на диске под систему и кэши | +| Доступ | root или пользователь с `sudo` | +| Домен | Поддомен, которым вы управляете, например `music.example.com` | +| Сеть | Входящие **80/tcp** и **443/tcp** (+ **443/udp** для HTTP/3) из интернета на сервер | +| Музыка | NFS-сервер с библиотекой (подключается позже из админки) | +| Gitea | Аккаунт с доступом к репозиторию, если пакеты и релизы приватные | + +--- + +## 2. DNS + +Создайте у регистратора или в DNS-панели запись: + +| Тип | Имя | Значение | +|---|---|---| +| `A` | `music` | публичный IPv4 сервера | +| `AAAA` | `music` | публичный IPv6 (если есть; если нет — запись не создавайте) | + +Если сервер стоит дома за роутером, пробросьте на роутере порты 80 и 443 на внутренний IP сервера. + +Проверка — команда должна вернуть IP вашего сервера: + +```bash +dig +short music.example.com +``` + +> Сертификат Let's Encrypt выдаётся, только когда домен уже указывает на сервер и порт 80 доступен из интернета. +> Не запускайте шаг 6, пока эта проверка не проходит. + +--- + +## 3. Docker + +```bash +curl -fsSL https://get.docker.com | sudo sh +``` + +Проверьте, что есть Docker Compose v2.24 или новее (нужен для `compose.tls.yaml`): + +```bash +docker compose version +``` + +Откройте порты в файрволе, если он включён (пример для `ufw`): + +```bash +sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw allow 443/udp +``` + +--- + +## 4. Файлы развёртывания + +Каталог для конфигурации: + +```bash +sudo mkdir -p /opt/pipu && sudo chown "$USER" /opt/pipu && cd /opt/pipu +``` + +Скачайте файлы из каталога `deploy/` репозитория. Пока нужная версия не вышла (в релизе `v0.1.0` +ещё нет `compose.tls.yaml` и `Caddyfile`), берите их из ветки `main`; начиная с `v0.1.1` те же файлы +лежат в ассетах релиза (`…/releases/download/vX.Y.Z/<файл>`). + +```bash +for f in compose.yaml compose.tls.yaml Caddyfile .env.example install.sh update.sh; do curl -fsSLO "https://gitea.hsrv.site/mrleo1nid/pipu-music/raw/branch/main/deploy/$f"; done +``` + +```bash +chmod +x install.sh update.sh +``` + +> Если репозиторий приватный, добавьте к `curl` заголовок с токеном Gitea (право `read:repository`): +> `-H "Authorization: token <ТОКЕН>"`. + +--- + +## 5. Подготовка сервера + +```bash +sudo ./install.sh +``` + +Скрипт: +- создаёт каталоги данных в `/srv/pipu` (БД, кэши, загрузки, сертификаты); +- делает `/srv/pipu/library` точкой монтирования с shared propagation, чтобы NFS-хранилища, которые потом + подключит админ, были видны всем контейнерам, и ставит systemd-юнит, повторяющий это после перезагрузки; +- создаёт `.env` со случайными паролями БД, ключом поиска и межсервисным токеном. + +Заполните в `/opt/pipu/.env`: + +```ini +REGISTRY=gitea.hsrv.site/mrleo1nid +PIPU_VERSION=0.1.0 + +COMPOSE_FILE=compose.yaml:compose.tls.yaml +DOMAIN=music.example.com +ACME_EMAIL=you@example.com +``` + +`ACME_EMAIL` — адрес, на который Let's Encrypt пишет, если с сертификатом что-то не так. +Остальные значения менять не нужно. + +Если образы в Gitea приватные, войдите в реестр токеном с правом `read:package`: + +```bash +docker login gitea.hsrv.site +``` + +--- + +## 6. Запуск + +```bash +./update.sh 0.1.0 +``` + +Скрипт скачает образы, запустит все сервисы и дождётся готовности API (до 2 минут). +При первом запуске Caddy сам получит сертификат для `DOMAIN`, обычно за 10–30 секунд. + +Проверка с любого компьютера: + +```bash +curl -s https://music.example.com/api/v1/server-info +``` + +Ожидаемый ответ: `{"name":"pipu-music","version":"0.1.0",...}`. Проверка всех зависимостей: + +```bash +curl -s https://music.example.com/readyz +``` + +Должно вернуться `"status":"ok"` и `ok` для `postgres` и `redis`. + +--- + +## 7. Веб-клиент + +Откройте `https://music.example.com` в браузере. + +- **v0.1.0:** карточка «Сервер 0.1.0 на связи» и переключатель языка RU / EN. +- **С версии 0.2 (M1):** экран входа. Первый вход — логин `admin` и пароль, который сервер один раз + выводит в лог при первом запуске: + + ```bash + docker compose logs api | grep -i bootstrap + ``` + + Сразу после входа приложение попросит сменить логин и пароль администратора + ([auth-and-roles.md](auth-and-roles.md#первый-запуск-bootstrap)). Чтобы задать пароль заранее, впишите + `BOOTSTRAP_ADMIN_PASSWORD` в `.env` **до** первого запуска. +- **С версии 0.3 (M2):** Админка → Хранилища → «Добавить хранилище» → адрес NFS-сервера и путь экспорта + → «Проверить» → шаблоны путей → «Сохранить и пересканировать» ([library-and-storage.md](library-and-storage.md)). + +--- + +## 8. Android-приложение + +1. На телефоне откройте страницу релизов + `https://gitea.hsrv.site/mrleo1nid/pipu-music/releases` и скачайте `pipu-music-<версия>.apk`. +2. Откройте файл. Android попросит разрешить установку из этого источника (браузера или файлового менеджера) — + разрешите и вернитесь к установке. +3. Запустите **Pipu Music**, на экране подключения введите адрес сервера: `music.example.com` + (`https://` подставится сам) → «Проверить». +4. Ожидаемый результат — «Сервер 0.1.0 на связи». Если версия приложения слишком старая для сервера, + приложение попросит обновиться. + +Дальше приложение обновляется само: с версии M5 оно проверяет новые релизы через ваш сервер +и предлагает обновиться ([mobile.md](mobile.md#самообновление)). Для этого в `.env` сервера нужно заполнить +`GITEA_URL`, `GITEA_REPO` и `GITEA_TOKEN` (токен только на чтение). + +> Приложению нужен настоящий сертификат. С самоподписанным сертификатом и голым IP-адресом подключиться не получится. +> Поэтому нужен домен с Let's Encrypt, как в этой инструкции. + +--- + +## 9. Вариант: свой reverse proxy вместо Caddy + +Если на сервере уже есть Traefik, Nginx Proxy Manager или свой nginx с сертификатами: + +1. В `.env` закомментируйте `COMPOSE_FILE` и задайте порт, на котором Pipu будет слушать только локально: + + ```ini + #COMPOSE_FILE=compose.yaml:compose.tls.yaml + HTTP_PORT=127.0.0.1:8081 + ``` + +2. Направьте прокси для `music.example.com` на `http://127.0.0.1:8081` с такими настройками: + - передавать заголовки `Host`, `X-Forwarded-For`, `X-Forwarded-Proto`; + - лимит тела запроса не меньше **16 МБ** (загрузка треков идёт чанками по 8 МБ); + - **без буферизации ответов** (стриминг аудио), длинные таймауты чтения (≥ 1 ч). + + Пример для nginx: + + ```nginx + server { + listen 443 ssl http2; + server_name music.example.com; + # ssl_certificate / ssl_certificate_key — как у остальных ваших сайтов + + client_max_body_size 16m; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_read_timeout 1h; + } + } + ``` + +3. Запуск и проверки — как в шагах 6–8. + +Если прокси работает на другой машине, укажите в `HTTP_PORT` внутренний адрес сервера (`192.168.1.10:8081`) и +закройте этот порт от интернета файрволом. + +--- + +## 10. Обновление, бэкапы, удаление + +Обновление (и откат) на любую версию: + +```bash +cd /opt/pipu && ./update.sh 0.2.0 +``` + +Миграции БД применяются автоматически при старте API. + +Бэкап базы (всё остальное восстанавливается само — см. [deployment.md](deployment.md#бэкапы)): + +```bash +docker compose exec -T postgres pg_dump -U pipu -Fc pipu > /srv/pipu/backups/pipu-$(date +%F).dump +``` + +Остановить сервис, сохранив данные: + +```bash +cd /opt/pipu && docker compose down +``` + +--- + +## 11. Если что-то не работает + +| Симптом | Что проверить | +|---|---| +| Сертификат не выдаётся, браузер ругается на HTTPS | `dig +short music.example.com` указывает на сервер; порт 80 открыт снаружи; `docker compose logs caddy` | +| `502 Bad Gateway` | API ещё запускается или упал: `docker compose ps`, `docker compose logs api` | +| `/readyz` отвечает `degraded` | Какая зависимость не `ok` — её логи: `docker compose logs postgres` / `redis` | +| `update.sh`: «pull access denied» | Неверный `REGISTRY` в `.env` или нужен `docker login gitea.hsrv.site` | +| `storage-agent` не стартует, ошибка про propagation / shared mount | Ещё раз `sudo ./install.sh`; на хосте `findmnt -o TARGET,PROPAGATION /srv/pipu/library` должно показать `shared` | +| Android: «Сервер не отвечает» | Тот же адрес открывается в браузере телефона; в адресе нет опечатки; телефон в сети с доступом к серверу | +| Android: «Эта версия приложения слишком старая» | Установите APK из последнего релиза | + +Логи всех сервисов сразу: + +```bash +cd /opt/pipu && docker compose logs -f --tail=100 +```