build(deploy): build images on the server instead of publishing to a registry
ci / api (push) Successful in 50s
ci / recommender (push) Successful in 37s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (apps/web/Dockerfile, web, ) (push) Successful in 44s
release / android + release (push) Failing after 53s
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 45s
ci / ts (packages + web) (push) Successful in 37s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, api, api) (push) Successful in 1m30s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Successful in 52s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/recommender/Dockerfile, recommender, ) (push) Successful in 28s
ci / mobile (Metro-бандл Android) (push) Successful in 49s
release / verify (push) Successful in 1m14s

- deploy/compose.yaml: every app service has `build:` (repo root context),
  local tags pipu-music-<service>:<version>
- deploy/update.sh: git fetch → checkout tag (latest release by default,
  or vX.Y.Z / main) → docker compose build --pull → up -d → prune → /readyz;
  re-executes from a temp copy because checkout may replace the script
- install.sh / .env.example: no REGISTRY, PIPU_VERSION set by update.sh,
  .env owned by the invoking user
- release.yml: drop image publishing; release ships APK + sha256 only
- ci.yml: new `docker` job builds all images without push
- docs: install guide uses git clone, deployment/ci-cd updated, ADR-013

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 00:16:05 +03:00
1 parent 1633b28c1a
commit 01319c58d7
11 files changed
+201 -186

No files matched your search

+21
View File
@@ -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
+4 -45
View File
@@ -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 }}
@@ -159,7 +118,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/compose.tls.yaml deploy/Caddyfile 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")"
+3 -2
View File
@@ -65,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).
+3 -4
View File
@@ -1,9 +1,8 @@
# Скопируйте в .env (install.sh делает это сам и генерирует секреты). Описание — docs/deployment.md. # Скопируйте в .env (install.sh делает это сам и генерирует секреты). Описание — docs/deployment.md.
# --- Образы --- # --- Версия и данные ---
# Реестр Gitea: <хост>/<владелец>, например gitea.hsrv.site/mrleo1nid # PIPU_VERSION проставляет update.sh по git-тегу — вручную не менять
REGISTRY=git.example.lan/owner PIPU_VERSION=dev
PIPU_VERSION=0.1.0
PIPU_DATA=/srv/pipu PIPU_DATA=/srv/pipu
# --- Домен и HTTPS (docs/install-guide.md) --- # --- Домен и HTTPS (docs/install-guide.md) ---
+31 -7
View File
@@ -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
+6 -2
View File
@@ -4,6 +4,9 @@
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"
@@ -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 " Заполните в .env: REGISTRY, PIPU_VERSION, DOMAIN, ACME_EMAIL" 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"
+38 -4
View File
@@ -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
+9 -13
View File
@@ -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).
+15
View File
@@ -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 о публикации образов.
+22 -72
View File
@@ -32,85 +32,33 @@
Перед первым запуском: `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` | |
@@ -150,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` (миграции пишутся обратно-совместимыми в пределах минорной версии: сначала добавить,
потом — в следующем релизе — удалить). потом — в следующем релизе — удалить).
+49 -37
View File
@@ -7,7 +7,10 @@
В примерах: В примерах:
- домен — `music.example.com`, замените на свой; - домен — `music.example.com`, замените на свой;
- репозиторий — `gitea.hsrv.site/mrleo1nid/pipu-music`; - репозиторий — `gitea.hsrv.site/mrleo1nid/pipu-music`;
- реестр образов — `gitea.hsrv.site/mrleo1nid`. - каталог установки — `/opt/pipu-music`.
Готовые Docker-образы не публикуются: сервер клонирует репозиторий и собирает образы сам
(`docker compose build`). Релизы в Gitea содержат только APK для Android.
> **Что умеет текущая версия.** В `v0.1.0` (этап M0) работают развёртывание, HTTPS и проверка связи > **Что умеет текущая версия.** В `v0.1.0` (этап M0) работают развёртывание, HTTPS и проверка связи
> клиентов с сервером. Вход, библиотека и плеер появятся в следующих версиях > клиентов с сервером. Вход, библиотека и плеер появятся в следующих версиях
@@ -19,12 +22,13 @@
| Что | Требование | | Что | Требование |
|---|---| |---|---|
| Сервер | Linux x86_64 (Debian 12 / Ubuntu 22.04+), 4+ ядра, 8+ ГБ RAM, 20+ ГБ на диске под систему и кэши | | Сервер | Linux x86_64 (Debian 12 / Ubuntu 22.04+), 4+ ядра, 8+ ГБ RAM, 30+ ГБ на диске (система, образы, кэш сборки, данные) |
| Доступ | root или пользователь с `sudo` | | Доступ | root или пользователь с `sudo` |
| Домен | Поддомен, которым вы управляете, например `music.example.com` | | Домен | Поддомен, которым вы управляете, например `music.example.com` |
| Сеть | Входящие **80/tcp** и **443/tcp** (+ **443/udp** для HTTP/3) из интернета на сервер | | Сеть | Входящие **80/tcp** и **443/tcp** (+ **443/udp** для HTTP/3) из интернета на сервер |
| Музыка | NFS-сервер с библиотекой (подключается позже из админки) | | Музыка | NFS-сервер с библиотекой (подключается позже из админки) |
| Gitea | Аккаунт с доступом к репозиторию, если пакеты и релизы приватные | | Интернет на сервере | Нужен при сборке: скачиваются базовые образы и зависимости Go / npm / Python |
| Gitea | Доступ к репозиторию на чтение (если он приватный — токен или SSH-ключ) |
--- ---
@@ -56,6 +60,18 @@ dig +short music.example.com
curl -fsSL https://get.docker.com | sudo sh 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`): Проверьте, что есть Docker Compose v2.24 или новее (нужен для `compose.tls.yaml`):
```bash ```bash
@@ -70,28 +86,21 @@ sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw allow 443/udp
--- ---
## 4. Файлы развёртывания ## 4. Клонирование репозитория
Каталог для конфигурации:
```bash ```bash
sudo mkdir -p /opt/pipu && sudo chown "$USER" /opt/pipu && cd /opt/pipu sudo mkdir -p /opt/pipu-music && sudo chown "$USER" /opt/pipu-music
```
Скачайте файлы из каталога `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 ```bash
chmod +x install.sh update.sh git clone https://gitea.hsrv.site/mrleo1nid/pipu-music.git /opt/pipu-music && cd /opt/pipu-music/deploy
``` ```
> Если репозиторий приватный, добавьте к `curl` заголовок с токеном Gitea (право `read:repository`): > Если репозиторий приватный: клонируйте по SSH (`git@gitea.hsrv.site:mrleo1nid/pipu-music.git`) с ключом,
> `-H "Authorization: token <ТОКЕН>"`. > добавленным в Gitea, или по HTTPS с токеном (право `read:repository`) — Git спросит логин и токен вместо пароля.
> Сохраните доступ (`git config credential.helper store` или SSH-ключ): `update.sh` делает `git fetch` при каждом обновлении.
Все дальнейшие команды выполняются из `/opt/pipu-music/deploy`.
--- ---
@@ -107,35 +116,33 @@ sudo ./install.sh
подключит админ, были видны всем контейнерам, и ставит systemd-юнит, повторяющий это после перезагрузки; подключит админ, были видны всем контейнерам, и ставит systemd-юнит, повторяющий это после перезагрузки;
- создаёт `.env` со случайными паролями БД, ключом поиска и межсервисным токеном. - создаёт `.env` со случайными паролями БД, ключом поиска и межсервисным токеном.
Заполните в `/opt/pipu/.env`: Заполните в `/opt/pipu-music/deploy/.env`:
```ini ```ini
REGISTRY=gitea.hsrv.site/mrleo1nid
PIPU_VERSION=0.1.0
COMPOSE_FILE=compose.yaml:compose.tls.yaml COMPOSE_FILE=compose.yaml:compose.tls.yaml
DOMAIN=music.example.com DOMAIN=music.example.com
ACME_EMAIL=you@example.com ACME_EMAIL=you@example.com
``` ```
`ACME_EMAIL` — адрес, на который Let's Encrypt пишет, если с сертификатом что-то не так. `ACME_EMAIL` — адрес, на который Let's Encrypt пишет, если с сертификатом что-то не так.
Остальные значения менять не нужно. Остальные значения менять не нужно; `PIPU_VERSION` проставляет `update.sh`.
Если образы в Gitea приватные, войдите в реестр токеном с правом `read:package`: `.env` не хранится в git (он в `.gitignore`), поэтому обновления репозитория его не трогают.
```bash
docker login gitea.hsrv.site
```
--- ---
## 6. Запуск ## 6. Сборка и запуск
```bash ```bash
./update.sh 0.1.0 ./update.sh
``` ```
Скрипт скачает образы, запустит все сервисы и дождётся готовности API (до 2 минут). Скрипт переключает репозиторий на последний релизный тег (например, `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 секунд. При первом запуске Caddy сам получит сертификат для `DOMAIN`, обычно за 10–30 секунд.
Проверка с любого компьютера: Проверка с любого компьютера:
@@ -240,13 +247,15 @@ curl -s https://music.example.com/readyz
## 10. Обновление, бэкапы, удаление ## 10. Обновление, бэкапы, удаление
Обновление (и откат) на любую версию: Обновление до последнего релиза:
```bash ```bash
cd /opt/pipu && ./update.sh 0.2.0 cd /opt/pipu-music/deploy && ./update.sh
``` ```
Миграции БД применяются автоматически при старте API. Конкретная версия или откат: `./update.sh v0.2.0`. Скрипт делает `git fetch`, переключается на тег,
пересобирает образы и перезапускает сервисы; старые образы удаляются. Миграции БД применяются
автоматически при старте API.
Бэкап базы (всё остальное восстанавливается само — см. [deployment.md](deployment.md#бэкапы)): Бэкап базы (всё остальное восстанавливается само — см. [deployment.md](deployment.md#бэкапы)):
@@ -257,7 +266,7 @@ docker compose exec -T postgres pg_dump -U pipu -Fc pipu > /srv/pipu/backups/pip
Остановить сервис, сохранив данные: Остановить сервис, сохранив данные:
```bash ```bash
cd /opt/pipu && docker compose down cd /opt/pipu-music/deploy && docker compose down
``` ```
--- ---
@@ -269,7 +278,10 @@ cd /opt/pipu && docker compose down
| Сертификат не выдаётся, браузер ругается на HTTPS | `dig +short music.example.com` указывает на сервер; порт 80 открыт снаружи; `docker compose logs caddy` | | Сертификат не выдаётся, браузер ругается на HTTPS | `dig +short music.example.com` указывает на сервер; порт 80 открыт снаружи; `docker compose logs caddy` |
| `502 Bad Gateway` | API ещё запускается или упал: `docker compose ps`, `docker compose logs api` | | `502 Bad Gateway` | API ещё запускается или упал: `docker compose ps`, `docker compose logs api` |
| `/readyz` отвечает `degraded` | Какая зависимость не `ok` — её логи: `docker compose logs postgres` / `redis` | | `/readyz` отвечает `degraded` | Какая зависимость не `ok` — её логи: `docker compose logs postgres` / `redis` |
| `update.sh`: «pull access denied» | Неверный `REGISTRY` в `.env` или нужен `docker login gitea.hsrv.site` | | `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` | | `storage-agent` не стартует, ошибка про propagation / shared mount | Ещё раз `sudo ./install.sh`; на хосте `findmnt -o TARGET,PROPAGATION /srv/pipu/library` должно показать `shared` |
| Android: «Сервер не отвечает» | Тот же адрес открывается в браузере телефона; в адресе нет опечатки; телефон в сети с доступом к серверу | | Android: «Сервер не отвечает» | Тот же адрес открывается в браузере телефона; в адресе нет опечатки; телефон в сети с доступом к серверу |
| Android: «Эта версия приложения слишком старая» | Установите APK из последнего релиза | | Android: «Эта версия приложения слишком старая» | Установите APK из последнего релиза |
@@ -277,5 +289,5 @@ cd /opt/pipu && docker compose down
Логи всех сервисов сразу: Логи всех сервисов сразу:
```bash ```bash
cd /opt/pipu && docker compose logs -f --tail=100 cd /opt/pipu-music/deploy && docker compose logs -f --tail=100
``` ```