Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1324cad2af | ||
|
|
01319c58d7 | ||
|
|
1633b28c1a |
No files matched your search
@@ -73,6 +73,27 @@ jobs:
|
|||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
- run: pnpm --filter @pipu/web build
|
- run: pnpm --filter @pipu/web build
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: docker (образы собираются — их сервер собирает сам при обновлении)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { name: api, dockerfile: services/api/Dockerfile, target: api }
|
||||||
|
- { name: storage-agent, dockerfile: services/api/Dockerfile, target: storage-agent }
|
||||||
|
- { name: recommender, dockerfile: services/recommender/Dockerfile, target: "" }
|
||||||
|
- { name: web, dockerfile: apps/web/Dockerfile, target: "" }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.dockerfile }}
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
push: false
|
||||||
|
build-args: VERSION=ci
|
||||||
|
|
||||||
mobile:
|
mobile:
|
||||||
name: mobile (Metro-бандл Android)
|
name: mobile (Metro-бандл Android)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
# Релиз по тегу vMAJOR.MINOR.PATCH[-rc.N] (docs/ci-cd.md):
|
# Релиз по тегу vMAJOR.MINOR.PATCH[-rc.N] (docs/ci-cd.md): проверки → подписанный APK → Gitea Release.
|
||||||
# проверки → Docker-образы в Gitea Container Registry → подписанный APK → Gitea Release с ассетами.
|
# Docker-образы не публикуются: сервер собирает их сам из клонированного репозитория (deploy/update.sh).
|
||||||
#
|
#
|
||||||
# Переменные репозитория (Settings → Actions → Variables):
|
|
||||||
# REGISTRY — хост реестра, например git.home.lan (по умолчанию — хост сервера Gitea)
|
|
||||||
# Секреты (Settings → Actions → Secrets):
|
# Секреты (Settings → Actions → Secrets):
|
||||||
# REGISTRY_USER, REGISTRY_TOKEN — push образов (scope: write:package)
|
|
||||||
# RELEASE_TOKEN — создание релиза и загрузка ассетов (scope: write:repository)
|
# RELEASE_TOKEN — создание релиза и загрузка ассетов (scope: write:repository)
|
||||||
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD — подпись APK
|
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD — подпись APK
|
||||||
name: release
|
name: release
|
||||||
@@ -45,47 +42,9 @@ jobs:
|
|||||||
- name: TypeScript
|
- name: TypeScript
|
||||||
run: pnpm i18n:check && pnpm test:scripts && pnpm typecheck && pnpm test
|
run: pnpm i18n:check && pnpm test:scripts && pnpm typecheck && pnpm test
|
||||||
|
|
||||||
images:
|
|
||||||
needs: verify
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: true
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- { name: api, dockerfile: services/api/Dockerfile, target: api }
|
|
||||||
- { name: storage-agent, dockerfile: services/api/Dockerfile, target: storage-agent }
|
|
||||||
- { name: recommender, dockerfile: services/recommender/Dockerfile, target: "" }
|
|
||||||
- { name: web, dockerfile: apps/web/Dockerfile, target: "" }
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Имя образа
|
|
||||||
id: img
|
|
||||||
run: |
|
|
||||||
registry="${{ vars.REGISTRY }}"
|
|
||||||
[ -n "$registry" ] || registry="$(echo '${{ github.server_url }}' | sed -E 's#^https?://##')"
|
|
||||||
owner="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
|
|
||||||
echo "registry=$registry" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "image=$registry/$owner/pipu-music-${{ matrix.name }}" >> "$GITHUB_OUTPUT"
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ steps.img.outputs.registry }}
|
|
||||||
username: ${{ secrets.REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
||||||
- uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ${{ matrix.dockerfile }}
|
|
||||||
target: ${{ matrix.target }}
|
|
||||||
push: true
|
|
||||||
build-args: VERSION=${{ needs.verify.outputs.version }}
|
|
||||||
tags: |
|
|
||||||
${{ steps.img.outputs.image }}:${{ needs.verify.outputs.version }}
|
|
||||||
${{ needs.verify.outputs.prerelease == 'false' && format('{0}:latest', steps.img.outputs.image) || '' }}
|
|
||||||
|
|
||||||
android-release:
|
android-release:
|
||||||
name: android + release
|
name: android + release
|
||||||
needs: [verify, images]
|
needs: verify
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PIPU_VERSION: ${{ needs.verify.outputs.version }}
|
PIPU_VERSION: ${{ needs.verify.outputs.version }}
|
||||||
@@ -96,6 +55,10 @@ jobs:
|
|||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with: { distribution: temurin, java-version: "17" }
|
with: { distribution: temurin, java-version: "17" }
|
||||||
- uses: android-actions/setup-android@v3
|
- uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
# пакета `tools` (значение по умолчанию) в репозитории SDK больше нет — sdkmanager падает;
|
||||||
|
# build-tools нужны шагу подписи (apksigner), версии — как у React Native 0.79
|
||||||
|
packages: platform-tools platforms;android-35 build-tools;35.0.0
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
||||||
@@ -159,7 +122,7 @@ jobs:
|
|||||||
'{tag_name:$tag, name:$name, body:$notes, prerelease:$pre, draft:false}')"
|
'{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' \
|
id="$(curl -fsS -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||||
-d "$body" "$API/releases" | jq -r .id)"
|
-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"; do
|
||||||
curl -fsS -X POST -H "Authorization: token $TOKEN" \
|
curl -fsS -X POST -H "Authorization: token $TOKEN" \
|
||||||
-F "attachment=@$f" "$API/releases/$id/assets?name=$(basename "$f")" > /dev/null
|
-F "attachment=@$f" "$API/releases/$id/assets?name=$(basename "$f")" > /dev/null
|
||||||
echo "uploaded $(basename "$f")"
|
echo "uploaded $(basename "$f")"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Self-hosted музыкальный сервис: веб и Android-прилож
|
|||||||
- Идея продукта: [docs/IDEA.md](docs/IDEA.md)
|
- Идея продукта: [docs/IDEA.md](docs/IDEA.md)
|
||||||
- Документация для разработки: [docs/README.md](docs/README.md)
|
- Документация для разработки: [docs/README.md](docs/README.md)
|
||||||
- План: [docs/roadmap.md](docs/roadmap.md)
|
- План: [docs/roadmap.md](docs/roadmap.md)
|
||||||
|
- Установка сервера на свой домен: [docs/install-guide.md](docs/install-guide.md)
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
@@ -64,5 +65,6 @@ pnpm generate && (cd services/api && go generate ./...)
|
|||||||
|
|
||||||
## Релиз
|
## Релиз
|
||||||
|
|
||||||
Тег `vX.Y.Z` (или `vX.Y.Z-rc.N`) запускает `.gitea/workflows/release.yml`: проверки → образы в Gitea Container Registry →
|
Тег `vX.Y.Z` (или `vX.Y.Z-rc.N`) запускает `.gitea/workflows/release.yml`: проверки → подписанный APK → Gitea Release.
|
||||||
подписанный APK → Gitea Release. Нужные переменные и секреты перечислены в шапке workflow и в [docs/ci-cd.md](docs/ci-cd.md).
|
Docker-образы не публикуются: сервер клонирует репозиторий и собирает их сам (`deploy/update.sh`,
|
||||||
|
[docs/install-guide.md](docs/install-guide.md)). Нужные секреты — в шапке workflow и в [docs/ci-cd.md](docs/ci-cd.md).
|
||||||
+14
-3
@@ -1,9 +1,16 @@
|
|||||||
# Скопируйте в .env (install.sh делает это сам и генерирует секреты). Описание — docs/deployment.md.
|
# Скопируйте в .env (install.sh делает это сам и генерирует секреты). Описание — docs/deployment.md.
|
||||||
|
|
||||||
# --- Образы ---
|
# --- Версия и данные ---
|
||||||
REGISTRY=git.example.lan/owner
|
# PIPU_VERSION проставляет update.sh по git-тегу — вручную не менять
|
||||||
PIPU_VERSION=0.1.0
|
PIPU_VERSION=dev
|
||||||
PIPU_DATA=/srv/pipu
|
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
|
HTTP_PORT=80
|
||||||
|
|
||||||
# --- Секреты (install.sh заполнит случайными значениями) ---
|
# --- Секреты (install.sh заполнит случайными значениями) ---
|
||||||
@@ -11,6 +18,10 @@ POSTGRES_PASSWORD=
|
|||||||
MEILI_MASTER_KEY=
|
MEILI_MASTER_KEY=
|
||||||
INTERNAL_TOKEN=
|
INTERNAL_TOKEN=
|
||||||
|
|
||||||
|
# --- Первый администратор (необязательно; иначе пароль генерируется и выводится в лог api один раз) ---
|
||||||
|
#BOOTSTRAP_ADMIN_USERNAME=admin
|
||||||
|
#BOOTSTRAP_ADMIN_PASSWORD=
|
||||||
|
|
||||||
# --- api / recommender ---
|
# --- api / recommender ---
|
||||||
# DATABASE_URL собирается в compose.yaml из POSTGRES_PASSWORD
|
# DATABASE_URL собирается в compose.yaml из POSTGRES_PASSWORD
|
||||||
REDIS_URL=redis://redis:6379/0
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
+31
-7
@@ -1,7 +1,15 @@
|
|||||||
# Продакшен-развёртывание (docs/deployment.md). Перед первым запуском: sudo ./install.sh
|
# Продакшен-развёртывание (docs/install-guide.md). Образы собираются на сервере из клонированного репозитория.
|
||||||
# Обновление: ./update.sh <version>
|
# Перед первым запуском: sudo ./install.sh. Установка / обновление: ./update.sh [vX.Y.Z]
|
||||||
name: pipu
|
name: pipu
|
||||||
|
|
||||||
|
x-version: &version
|
||||||
|
VERSION: ${PIPU_VERSION:-0.0.0-dev}
|
||||||
|
|
||||||
|
x-build-rec: &build-rec
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/recommender/Dockerfile
|
||||||
|
args: *version
|
||||||
|
|
||||||
x-library-ro: &library-ro
|
x-library-ro: &library-ro
|
||||||
type: bind
|
type: bind
|
||||||
source: ${PIPU_DATA:-/srv/pipu}/library
|
source: ${PIPU_DATA:-/srv/pipu}/library
|
||||||
@@ -15,7 +23,11 @@ x-restart: &restart
|
|||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
<<: *restart
|
<<: *restart
|
||||||
image: ${REGISTRY}/pipu-music-web:${PIPU_VERSION}
|
image: pipu-music-web:${PIPU_VERSION:-dev}
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: apps/web/Dockerfile
|
||||||
|
args: *version
|
||||||
ports: ["${HTTP_PORT:-80}:80"]
|
ports: ["${HTTP_PORT:-80}:80"]
|
||||||
volumes:
|
volumes:
|
||||||
- *library-ro
|
- *library-ro
|
||||||
@@ -25,7 +37,12 @@ services:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
<<: *restart
|
<<: *restart
|
||||||
image: ${REGISTRY}/pipu-music-api:${PIPU_VERSION}
|
image: pipu-music-api:${PIPU_VERSION:-dev}
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/api/Dockerfile
|
||||||
|
target: api
|
||||||
|
args: *version
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment: &db-env
|
environment: &db-env
|
||||||
DATABASE_URL: postgres://pipu:${POSTGRES_PASSWORD}@postgres:5432/pipu?sslmode=disable
|
DATABASE_URL: postgres://pipu:${POSTGRES_PASSWORD}@postgres:5432/pipu?sslmode=disable
|
||||||
@@ -41,7 +58,12 @@ services:
|
|||||||
|
|
||||||
storage-agent:
|
storage-agent:
|
||||||
<<: *restart
|
<<: *restart
|
||||||
image: ${REGISTRY}/pipu-music-storage-agent:${PIPU_VERSION}
|
image: pipu-music-storage-agent:${PIPU_VERSION:-dev}
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: services/api/Dockerfile
|
||||||
|
target: storage-agent
|
||||||
|
args: *version
|
||||||
cap_add: [SYS_ADMIN]
|
cap_add: [SYS_ADMIN]
|
||||||
security_opt: ["apparmor:unconfined"]
|
security_opt: ["apparmor:unconfined"]
|
||||||
environment:
|
environment:
|
||||||
@@ -56,7 +78,8 @@ services:
|
|||||||
|
|
||||||
recommender:
|
recommender:
|
||||||
<<: *restart
|
<<: *restart
|
||||||
image: ${REGISTRY}/pipu-music-recommender:${PIPU_VERSION}
|
image: pipu-music-recommender:${PIPU_VERSION:-dev}
|
||||||
|
build: *build-rec
|
||||||
command: ["serve"]
|
command: ["serve"]
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment: *db-env
|
environment: *db-env
|
||||||
@@ -65,7 +88,8 @@ services:
|
|||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
<<: *restart
|
<<: *restart
|
||||||
image: ${REGISTRY}/pipu-music-recommender:${PIPU_VERSION}
|
image: pipu-music-recommender:${PIPU_VERSION:-dev}
|
||||||
|
build: *build-rec
|
||||||
command: ["analyze", "--workers", "4"]
|
command: ["analyze", "--workers", "4"]
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment: *db-env
|
environment: *db-env
|
||||||
|
|||||||
+7
-3
@@ -4,10 +4,13 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# .env и каталоги создаются от root, но сам репозиторий должен остаться у пользователя, который делает git pull.
|
||||||
|
OWNER="${SUDO_USER:-root}"
|
||||||
|
|
||||||
DATA="${PIPU_DATA:-/srv/pipu}"
|
DATA="${PIPU_DATA:-/srv/pipu}"
|
||||||
|
|
||||||
echo "→ каталоги в $DATA"
|
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; чтобы точки монтирования видели другие контейнеры,
|
# storage-agent монтирует NFS внутрь $DATA/library; чтобы точки монтирования видели другие контейнеры,
|
||||||
# каталог должен быть отдельной точкой монтирования с shared propagation.
|
# каталог должен быть отдельной точкой монтирования с shared propagation.
|
||||||
@@ -44,7 +47,8 @@ if [ ! -f .env ]; then
|
|||||||
cp .env.example .env
|
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
|
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
|
chmod 600 .env
|
||||||
echo " Проверьте REGISTRY и PIPU_VERSION в .env"
|
chown "$OWNER" .env
|
||||||
|
echo " Заполните в .env: DOMAIN и ACME_EMAIL (или настройте свой прокси — docs/install-guide.md)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Готово. Запуск: ./update.sh \$(grep ^PIPU_VERSION= .env | cut -d= -f2)"
|
echo "Готово. Сборка и запуск последнего релиза: ./update.sh"
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# Шаблон обрабатывается envsubst при старте контейнера nginx (переменные: API_UPSTREAM).
|
# Шаблон обрабатывается 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 {
|
upstream pipu_api {
|
||||||
server ${API_UPSTREAM};
|
server ${API_UPSTREAM};
|
||||||
@@ -10,6 +16,13 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
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;
|
root /usr/share/nginx/html;
|
||||||
client_max_body_size 16m;
|
client_max_body_size 16m;
|
||||||
|
|
||||||
@@ -23,7 +36,7 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
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; }
|
location = /healthz { proxy_pass http://pipu_api; }
|
||||||
|
|||||||
+38
-4
@@ -1,13 +1,47 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Обновление / откат на указанную версию: ./update.sh 1.3.0
|
# Установка / обновление / откат из клонированного репозитория: образы собираются локально.
|
||||||
|
# ./update.sh — последний релизный тег (без -rc)
|
||||||
|
# ./update.sh v0.2.0 — конкретная версия (можно без «v»)
|
||||||
|
# ./update.sh main — текущая ветка main (для тестов, не для продакшена)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
VERSION="${1:?usage: $0 <version>}"
|
# git checkout может заменить этот файл прямо во время работы — выполняемся из временной копии.
|
||||||
|
if [ -z "${PIPU_UPDATE_REEXEC:-}" ]; then
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
cp "$0" "$tmp"
|
||||||
|
PIPU_UPDATE_REEXEC=1 PIPU_DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" exec bash "$tmp" "$@"
|
||||||
|
fi
|
||||||
|
trap 'rm -f "$0"' EXIT
|
||||||
|
cd "$PIPU_DEPLOY_DIR"
|
||||||
|
|
||||||
|
[ -f .env ] || { echo "нет .env — сначала sudo ./install.sh" >&2; exit 1; }
|
||||||
|
|
||||||
|
REF="${1:-}"
|
||||||
|
git fetch --tags --prune --quiet origin
|
||||||
|
|
||||||
|
if [ -z "$REF" ]; then
|
||||||
|
REF="$(git tag -l 'v*' --sort=-v:refname | grep -v -- '-rc' | head -1)"
|
||||||
|
[ -n "$REF" ] || { echo "в репозитории нет релизных тегов; укажите версию или main" >&2; exit 1; }
|
||||||
|
elif [[ "$REF" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
|
REF="v$REF"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ переключаемся на $REF"
|
||||||
|
if git show-ref --verify --quiet "refs/remotes/origin/$REF"; then
|
||||||
|
git checkout --quiet "$REF"
|
||||||
|
git merge --ff-only --quiet "origin/$REF"
|
||||||
|
else
|
||||||
|
git checkout --quiet --detach "$REF"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$(git describe --tags --always --dirty)"
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$VERSION|" .env
|
sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$VERSION|" .env
|
||||||
|
echo "→ версия $VERSION, сборка образов (первая сборка — 5–15 минут)"
|
||||||
|
|
||||||
docker compose pull
|
docker compose build --pull
|
||||||
docker compose up -d --remove-orphans
|
docker compose up -d --remove-orphans
|
||||||
|
docker image prune -f >/dev/null
|
||||||
|
|
||||||
echo "→ ждём готовности api"
|
echo "→ ждём готовности api"
|
||||||
for _ in $(seq 1 60); do
|
for _ in $(seq 1 60); do
|
||||||
|
|||||||
+2
-1
@@ -22,7 +22,8 @@ Pipu Music — self-hosted музыкальный сервис (веб + Android
|
|||||||
| [i18n.md](i18n.md) | Локализация (ru / en, расширяемость) |
|
| [i18n.md](i18n.md) | Локализация (ru / en, расширяемость) |
|
||||||
| [testing.md](testing.md) | Стратегия тестирования |
|
| [testing.md](testing.md) | Стратегия тестирования |
|
||||||
| [ci-cd.md](ci-cd.md) | Gitea Actions: проверки и релиз по тегу |
|
| [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, версиям |
|
| [conventions.md](conventions.md) | Соглашения по коду, git, версиям |
|
||||||
| [roadmap.md](roadmap.md) | Этапы разработки и критерии готовности |
|
| [roadmap.md](roadmap.md) | Этапы разработки и критерии готовности |
|
||||||
|
|
||||||
|
|||||||
+9
-13
@@ -27,6 +27,7 @@ jobs:
|
|||||||
recommender: # uv sync; ruff; mypy --strict; pytest (вкл. оффлайн-оценку качества)
|
recommender: # uv sync; ruff; mypy --strict; pytest (вкл. оффлайн-оценку качества)
|
||||||
ts: # pnpm install --frozen-lockfile; turbo lint typecheck test; i18n:check
|
ts: # pnpm install --frozen-lockfile; turbo lint typecheck test; i18n:check
|
||||||
web-build: # vite build, проверка размера бандла
|
web-build: # vite build, проверка размера бандла
|
||||||
|
docker: # сборка всех Docker-образов без push (сервер собирает их сам — ADR-013)
|
||||||
mobile-check: # tsc, jest, expo prebuild + ./gradlew assembleDebug (только если менялся mobile/)
|
mobile-check: # tsc, jest, expo prebuild + ./gradlew assembleDebug (только если менялся mobile/)
|
||||||
e2e: # needs: api, web-build; docker compose -f deploy/compose.test.yaml up; playwright test
|
e2e: # needs: api, web-build; docker compose -f deploy/compose.test.yaml up; playwright test
|
||||||
```
|
```
|
||||||
@@ -45,35 +46,30 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
verify: # все проверки ci.yml (reusable workflow)
|
verify: # все проверки ci.yml (reusable workflow)
|
||||||
|
|
||||||
images: # needs: verify
|
android-release: # needs: verify
|
||||||
strategy: { matrix: { service: [api, storage-agent, recommender, web] } }
|
|
||||||
# docker buildx build --push
|
|
||||||
# ${REGISTRY}/${OWNER}/pipu-music-${service}:${VERSION} и :latest (кроме pre-release)
|
|
||||||
# web-образ = nginx + собранная SPA + конфиг
|
|
||||||
|
|
||||||
android: # needs: verify
|
|
||||||
# setup-java 17, Android SDK, pnpm install
|
# setup-java 17, Android SDK, pnpm install
|
||||||
# VERSION / VERSION_CODE из тега → app.config.ts
|
# VERSION / VERSION_CODE из тега → app.config.ts
|
||||||
# expo prebuild --platform android --clean
|
# expo prebuild --platform android --clean
|
||||||
# ./gradlew assembleRelease (signingConfig из секретов)
|
# ./gradlew assembleRelease (signingConfig из секретов)
|
||||||
# apksigner verify; sha256sum > pipu-music-${VERSION}.apk.sha256
|
# apksigner verify; sha256sum > pipu-music-${VERSION}.apk.sha256
|
||||||
|
|
||||||
release: # needs: [images, android]
|
# changelog из Conventional Commits между тегами
|
||||||
# git-cliff --latest > CHANGELOG_RELEASE.md
|
|
||||||
# создать Gitea Release через API (POST /repos/{owner}/{repo}/releases), prerelease по суффиксу
|
# создать Gitea Release через API (POST /repos/{owner}/{repo}/releases), prerelease по суффиксу
|
||||||
# загрузить ассеты: APK, .sha256, deploy/compose.yaml, deploy/.env.example
|
# загрузить ассеты: APK и .sha256
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Docker-образы **не публикуются**: сервер собирает их сам из клонированного репозитория (ADR-013).
|
||||||
|
Чтобы сборка на сервере не ломалась, job `docker` в `ci.yml` собирает все образы без push на каждый PR.
|
||||||
|
|
||||||
## Секреты
|
## Секреты
|
||||||
|
|
||||||
| Секрет | Назначение |
|
| Секрет | Назначение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `REGISTRY_USER`, `REGISTRY_TOKEN` | Push в Gitea Container Registry |
|
|
||||||
| `RELEASE_TOKEN` | Создание релиза и загрузка ассетов |
|
| `RELEASE_TOKEN` | Создание релиза и загрузка ассетов |
|
||||||
| `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. |
|
| `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. |
|
||||||
|
|
||||||
## Деплой
|
## Деплой
|
||||||
|
|
||||||
Автодеплой не обязателен. Рекомендуемый вариант: на сервере `deploy/update.sh <version>` —
|
Автодеплой не обязателен. На сервере в клоне репозитория: `deploy/update.sh [vX.Y.Z]` — checkout тега,
|
||||||
`docker compose pull && docker compose up -d` с `PIPU_VERSION=<version>`; миграции применяются при старте `api`.
|
`docker compose build` и `up -d`; миграции применяются при старте `api` ([install-guide.md](install-guide.md)).
|
||||||
Опционально — job `deploy` в `release.yml` по SSH на сервер для не-pre-release тегов (с ручным approval через environment).
|
Опционально — job `deploy` в `release.yml` по SSH на сервер для не-pre-release тегов (с ручным approval через environment).
|
||||||
@@ -155,3 +155,18 @@ Postgres остаётся источником истины, индексы пе
|
|||||||
|
|
||||||
**Последствия.** Публичный `api` по-прежнему не может изменить библиотеку. Нужны права на запись в NFS-экспорт
|
**Последствия.** Публичный `api` по-прежнему не может изменить библиотеку. Нужны права на запись в NFS-экспорт
|
||||||
для UID агента. Подробно — [uploads.md](uploads.md).
|
для UID агента. Подробно — [uploads.md](uploads.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADR-013. Образы собираются на сервере, реестр не используется
|
||||||
|
|
||||||
|
**Контекст.** Публикация образов в Gitea Container Registry требует настройки реестра, токенов на push и pull,
|
||||||
|
`docker login` на сервере. Сервер один, и он сам может собрать образы.
|
||||||
|
|
||||||
|
**Решение.** На сервере клонируется репозиторий; `deploy/update.sh [vX.Y.Z]` переключается на тег и выполняет
|
||||||
|
`docker compose build` + `up -d` (`build:` у сервисов в `deploy/compose.yaml`). Релиз в Gitea содержит только APK.
|
||||||
|
CI на каждый PR собирает все образы без push, чтобы сборка на сервере не ломалась.
|
||||||
|
|
||||||
|
**Последствия.** Серверу нужен доступ к репозиторию и интернет на время сборки; первая сборка — 5–15 минут,
|
||||||
|
нужно ~5–10 ГБ под кэш сборки. Откат — `update.sh` на предыдущий тег (пересборка из кэша). Отпадают секреты
|
||||||
|
`REGISTRY_USER` / `REGISTRY_TOKEN`. Заменяет часть ADR-001 / ci-cd о публикации образов.
|
||||||
+37
-75
@@ -1,5 +1,8 @@
|
|||||||
# Развёртывание
|
# Развёртывание
|
||||||
|
|
||||||
|
> Пошаговая инструкция «с нуля до работающего сервера на домене» — [install-guide.md](install-guide.md).
|
||||||
|
> Здесь — справочник: требования, каталоги, сервисы, переменные.
|
||||||
|
|
||||||
## Требования к серверу
|
## Требования к серверу
|
||||||
|
|
||||||
- Linux x86_64 (нужен mount propagation для storage-agent), Docker 24+, Docker Compose v2.
|
- Linux x86_64 (нужен mount propagation для storage-agent), Docker 24+, Docker Compose v2.
|
||||||
@@ -22,91 +25,40 @@
|
|||||||
├── models/ # модели Essentia (скачиваются при первом старте analyzer)
|
├── models/ # модели Essentia (скачиваются при первом старте analyzer)
|
||||||
├── updates/ # кэш APK релизов
|
├── updates/ # кэш APK релизов
|
||||||
├── uploads/ # staging очереди на добавление (размер ≈ лимит очереди, по умолчанию 10 ГБ)
|
├── 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`
|
Перед первым запуском: `mkdir -p /srv/pipu/library && mount --bind /srv/pipu/library /srv/pipu/library && mount --make-rshared /srv/pipu/library`
|
||||||
(делает `deploy/install.sh`, плюс systemd-unit для повторения после перезагрузки).
|
(делает `deploy/install.sh`, плюс systemd-unit для повторения после перезагрузки).
|
||||||
|
|
||||||
## `deploy/compose.yaml` (сокращённо)
|
## `deploy/compose.yaml`
|
||||||
|
|
||||||
```yaml
|
Актуальная конфигурация — сам файл [`deploy/compose.yaml`](../deploy/compose.yaml); здесь — как он устроен.
|
||||||
x-library-ro: &library-ro
|
|
||||||
type: bind
|
|
||||||
source: /srv/pipu/library
|
|
||||||
target: /library
|
|
||||||
read_only: true
|
|
||||||
bind: { propagation: rslave }
|
|
||||||
|
|
||||||
services:
|
- **Образы собираются на сервере** из клонированного репозитория (`build:` у каждого сервиса, контекст — корень репо),
|
||||||
nginx:
|
в реестр ничего не публикуется. Теги локальные: `pipu-music-<сервис>:<версия>`, версия — `git describe` текущего тега
|
||||||
image: ${REGISTRY}/pipu-music-web:${PIPU_VERSION}
|
(её проставляет `update.sh` в `PIPU_VERSION`).
|
||||||
ports: ["443:443", "80:80"]
|
|
||||||
volumes:
|
|
||||||
- *library-ro
|
|
||||||
- /srv/pipu/cache:/cache:ro
|
|
||||||
- /srv/pipu/uploads:/uploads:ro # прослушивание очереди админом (internal location)
|
|
||||||
- ./certs:/etc/nginx/certs:ro
|
|
||||||
depends_on: [api]
|
|
||||||
|
|
||||||
api:
|
| Сервис | Собирается из | Особенности |
|
||||||
image: ${REGISTRY}/pipu-music-api:${PIPU_VERSION}
|
|---|---|---|
|
||||||
env_file: .env
|
| `nginx` | `apps/web/Dockerfile` | SPA + конфиг nginx; единственный открытый порт (`HTTP_PORT`) или за Caddy |
|
||||||
volumes:
|
| `api` | `services/api/Dockerfile`, target `api` | Миграции при старте |
|
||||||
- *library-ro
|
| `storage-agent` | `services/api/Dockerfile`, target `storage-agent` | `CAP_SYS_ADMIN`, `apparmor:unconfined`, библиотека с `propagation: rshared` |
|
||||||
- /srv/pipu/cache:/cache
|
| `recommender`, `analyzer` | `services/recommender/Dockerfile` (один образ) | Команды `serve` и `analyze --workers 4` |
|
||||||
- /srv/pipu/updates:/updates
|
| `postgres` | `pgvector/pgvector:pg17` | Healthcheck `pg_isready` |
|
||||||
- /srv/pipu/uploads:/uploads
|
| `redis` | `redis:7-alpine` | AOF |
|
||||||
depends_on: [postgres, redis, meilisearch, storage-agent]
|
| `meilisearch` | `getmeili/meilisearch:v1.11` | |
|
||||||
|
| `caddy` (из `compose.tls.yaml`) | `caddy:2.10-alpine` | HTTPS на `DOMAIN`, nginx тогда без открытых портов |
|
||||||
|
|
||||||
storage-agent:
|
- Библиотека монтируется в `nginx`, `api`, `analyzer` только на чтение с `propagation: rslave`.
|
||||||
image: ${REGISTRY}/pipu-music-storage-agent:${PIPU_VERSION}
|
- Наружу открыт только `nginx` (или `caddy`). Все остальные — во внутренней сети compose.
|
||||||
cap_add: [SYS_ADMIN]
|
|
||||||
security_opt: ["apparmor:unconfined"]
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /srv/pipu/library
|
|
||||||
target: /library
|
|
||||||
bind: { propagation: rshared }
|
|
||||||
- /srv/pipu/uploads:/uploads:ro # читает staging при импорте
|
|
||||||
# /library-rw/<id> монтируется агентом внутри своего контейнера, наружу не пробрасывается
|
|
||||||
environment: [INTERNAL_TOKEN, DATABASE_URL, PIPU_FILE_UID, PIPU_FILE_GID]
|
|
||||||
|
|
||||||
recommender:
|
|
||||||
image: ${REGISTRY}/pipu-music-recommender:${PIPU_VERSION}
|
|
||||||
command: ["serve"]
|
|
||||||
env_file: .env
|
|
||||||
depends_on: [postgres, redis]
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
image: ${REGISTRY}/pipu-music-recommender:${PIPU_VERSION}
|
|
||||||
command: ["analyze", "--workers", "4"]
|
|
||||||
env_file: .env
|
|
||||||
volumes: [*library-ro, /srv/pipu/models:/models]
|
|
||||||
deploy: { resources: { limits: { cpus: "4" } } }
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: pgvector/pgvector:pg17
|
|
||||||
volumes: [/srv/pipu/postgres:/var/lib/postgresql/data]
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
|
||||||
volumes: [/srv/pipu/redis:/data]
|
|
||||||
|
|
||||||
meilisearch:
|
|
||||||
image: getmeili/meilisearch:v1.11
|
|
||||||
environment: [MEILI_MASTER_KEY, MEILI_ENV=production]
|
|
||||||
volumes: [/srv/pipu/meili:/meili_data]
|
|
||||||
```
|
|
||||||
|
|
||||||
Наружу открыт только `nginx`. Все остальные — во внутренней сети compose.
|
|
||||||
|
|
||||||
## Конфигурация (`.env`)
|
## Конфигурация (`.env`)
|
||||||
|
|
||||||
| Переменная | Пример / по умолчанию | Описание |
|
| Переменная | Пример / по умолчанию | Описание |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PIPU_VERSION` | `1.0.0` | Версия образов |
|
| `PIPU_VERSION` | проставляет `update.sh` | Версия собранных образов (`git describe`) |
|
||||||
| `PUBLIC_URL` | `https://music.home.lan` | Внешний адрес |
|
| `PUBLIC_URL` | `https://music.home.lan` | Внешний адрес |
|
||||||
| `DATABASE_URL` | `postgres://pipu:…@postgres:5432/pipu` | |
|
| `DATABASE_URL` | `postgres://pipu:…@postgres:5432/pipu` | |
|
||||||
| `REDIS_URL` | `redis://redis:6379/0` | |
|
| `REDIS_URL` | `redis://redis:6379/0` | |
|
||||||
@@ -125,8 +77,16 @@ services:
|
|||||||
|
|
||||||
## TLS
|
## TLS
|
||||||
|
|
||||||
- Свой домен — Let's Encrypt (через отдельный certbot / acme.sh или Caddy перед nginx).
|
- **Свой домен (рекомендуется)** — `deploy/compose.tls.yaml` добавляет Caddy, который получает и продлевает сертификат
|
||||||
- Локальная сеть — свой CA; сертификат CA нужно установить на устройства или включить пиннинг в мобильном (см. mobile.md).
|
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).
|
||||||
|
|
||||||
## Бэкапы
|
## Бэкапы
|
||||||
|
|
||||||
@@ -138,7 +98,9 @@ services:
|
|||||||
## Обновление
|
## Обновление
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/pipu && ./update.sh 1.3.0 # pull образов, up -d, миграции при старте api, проверка /readyz
|
cd /opt/pipu-music/deploy && ./update.sh # последний релизный тег
|
||||||
```
|
```
|
||||||
Откат: `./update.sh 1.2.4` (миграции пишутся обратно-совместимыми в пределах минорной версии: сначала добавить,
|
Скрипт: `git fetch` → checkout тега → `docker compose build --pull` → `up -d` → удаление старых образов →
|
||||||
|
ожидание `/readyz`; миграции применяются при старте api.
|
||||||
|
Конкретная версия или откат: `./update.sh v1.2.4` (миграции пишутся обратно-совместимыми в пределах минорной версии: сначала добавить,
|
||||||
потом — в следующем релизе — удалить).
|
потом — в следующем релизе — удалить).
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
# Установка сервера и подключение приложений
|
||||||
|
|
||||||
|
Пошаговая инструкция: от чистого Linux-сервера до работающего Pipu Music на своём домене
|
||||||
|
с HTTPS, плюс подключение веб- и Android-клиента. Справочник по устройству развёртывания —
|
||||||
|
[deployment.md](deployment.md).
|
||||||
|
|
||||||
|
В примерах:
|
||||||
|
- домен — `music.example.com`, замените на свой;
|
||||||
|
- репозиторий — `gitea.hsrv.site/mrleo1nid/pipu-music`;
|
||||||
|
- каталог установки — `/opt/pipu-music`.
|
||||||
|
|
||||||
|
Готовые Docker-образы не публикуются: сервер клонирует репозиторий и собирает образы сам
|
||||||
|
(`docker compose build`). Релизы в Gitea содержат только APK для Android.
|
||||||
|
|
||||||
|
> **Что умеет текущая версия.** В `v0.1.0` (этап M0) работают развёртывание, HTTPS и проверка связи
|
||||||
|
> клиентов с сервером. Вход, библиотека и плеер появятся в следующих версиях
|
||||||
|
> (см. [roadmap.md](roadmap.md)). Шаги, которые зависят от версии, помечены.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что нужно заранее
|
||||||
|
|
||||||
|
| Что | Требование |
|
||||||
|
|---|---|
|
||||||
|
| Сервер | Linux x86_64 (Debian 12 / Ubuntu 22.04+), 4+ ядра, 8+ ГБ RAM, 30+ ГБ на диске (система, образы, кэш сборки, данные) |
|
||||||
|
| Доступ | root или пользователь с `sudo` |
|
||||||
|
| Домен | Поддомен, которым вы управляете, например `music.example.com` |
|
||||||
|
| Сеть | Входящие **80/tcp** и **443/tcp** (+ **443/udp** для HTTP/3) из интернета на сервер |
|
||||||
|
| Музыка | NFS-сервер с библиотекой (подключается позже из админки) |
|
||||||
|
| Интернет на сервере | Нужен при сборке: скачиваются базовые образы и зависимости Go / npm / Python |
|
||||||
|
| Gitea | Доступ к репозиторию на чтение (если он приватный — токен или SSH-ключ) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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`, чтобы запускать сборку без `sudo` (после этого перелогиньтесь):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
```
|
||||||
|
|
||||||
|
И `git`, если его нет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install -y git
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте, что есть 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-music && sudo chown "$USER" /opt/pipu-music
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.hsrv.site/mrleo1nid/pipu-music.git /opt/pipu-music && cd /opt/pipu-music/deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
> Если репозиторий приватный: клонируйте по SSH (`git@gitea.hsrv.site:mrleo1nid/pipu-music.git`) с ключом,
|
||||||
|
> добавленным в Gitea, или по HTTPS с токеном (право `read:repository`) — Git спросит логин и токен вместо пароля.
|
||||||
|
> Сохраните доступ (`git config credential.helper store` или SSH-ключ): `update.sh` делает `git fetch` при каждом обновлении.
|
||||||
|
|
||||||
|
Все дальнейшие команды выполняются из `/opt/pipu-music/deploy`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Подготовка сервера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
- создаёт каталоги данных в `/srv/pipu` (БД, кэши, загрузки, сертификаты);
|
||||||
|
- делает `/srv/pipu/library` точкой монтирования с shared propagation, чтобы NFS-хранилища, которые потом
|
||||||
|
подключит админ, были видны всем контейнерам, и ставит systemd-юнит, повторяющий это после перезагрузки;
|
||||||
|
- создаёт `.env` со случайными паролями БД, ключом поиска и межсервисным токеном.
|
||||||
|
|
||||||
|
Заполните в `/opt/pipu-music/deploy/.env`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
COMPOSE_FILE=compose.yaml:compose.tls.yaml
|
||||||
|
DOMAIN=music.example.com
|
||||||
|
ACME_EMAIL=you@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
`ACME_EMAIL` — адрес, на который Let's Encrypt пишет, если с сертификатом что-то не так.
|
||||||
|
Остальные значения менять не нужно; `PIPU_VERSION` проставляет `update.sh`.
|
||||||
|
|
||||||
|
`.env` не хранится в git (он в `.gitignore`), поэтому обновления репозитория его не трогают.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Сборка и запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт переключает репозиторий на последний релизный тег (например, `v0.1.1`), собирает образы, запускает
|
||||||
|
все сервисы и ждёт готовности API (до 2 минут). Первая сборка занимает 5–15 минут, следующие — быстрее
|
||||||
|
за счёт кэша Docker. Конкретная версия: `./update.sh v0.1.1`.
|
||||||
|
|
||||||
|
> Локальная сборка поддерживается начиная с `v0.1.1`: в теге `v0.1.0` `compose.yaml` ещё ссылается на реестр образов.
|
||||||
|
> Пока `v0.1.1` не вышел, запускайте `./update.sh main`.
|
||||||
|
При первом запуске 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-music/deploy && ./update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Конкретная версия или откат: `./update.sh v0.2.0`. Скрипт делает `git fetch`, переключается на тег,
|
||||||
|
пересобирает образы и перезапускает сервисы; старые образы удаляются. Миграции БД применяются
|
||||||
|
автоматически при старте 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-music/deploy && 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`: ошибка `git fetch` | Нет доступа к репозиторию: проверьте SSH-ключ или сохранённый токен (`git fetch` вручную) |
|
||||||
|
| Сборка падает на скачивании зависимостей | Нет интернета или DNS на сервере; повторите `./update.sh` — уже собранные слои берутся из кэша |
|
||||||
|
| Сборка падает: «no space left on device» | Очистите кэш сборки: `docker builder prune` |
|
||||||
|
| `permission denied … docker.sock` | Пользователь не в группе `docker` (шаг 3) или вы не перелогинились |
|
||||||
|
| `storage-agent` не стартует, ошибка про propagation / shared mount | Ещё раз `sudo ./install.sh`; на хосте `findmnt -o TARGET,PROPAGATION /srv/pipu/library` должно показать `shared` |
|
||||||
|
| Android: «Сервер не отвечает» | Тот же адрес открывается в браузере телефона; в адресе нет опечатки; телефон в сети с доступом к серверу |
|
||||||
|
| Android: «Эта версия приложения слишком старая» | Установите APK из последнего релиза |
|
||||||
|
|
||||||
|
Логи всех сервисов сразу:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/pipu-music/deploy && docker compose logs -f --tail=100
|
||||||
|
```
|
||||||
Reference in new issue
Block a user