feat: M0 monorepo skeleton with services, clients, deploy and CI
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m55s
ci / api (push) Successful in 51s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s
release / images (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
release / images (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / recommender (push) Successful in 1m11s
ci / ts (packages + web) (push) Canceled after 3s
release / verify (push) Successful in 1m16s
release / images (apps/web/Dockerfile, web, ) (push) Failing after 2m40s
release / android + release (push) Skipped
release / images (services/api/Dockerfile, api, api) (push) Canceled after 1s
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m55s
ci / api (push) Successful in 51s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s
release / images (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
release / images (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / recommender (push) Successful in 1m11s
ci / ts (packages + web) (push) Canceled after 3s
release / verify (push) Successful in 1m16s
release / images (apps/web/Dockerfile, web, ) (push) Failing after 2m40s
release / android + release (push) Skipped
release / images (services/api/Dockerfile, api, api) (push) Canceled after 1s
- api/openapi.yaml: contract with /healthz, /readyz, /api/v1/server-info - services/api (Go): api with oapi-codegen strict server, goose migrations, readiness checks for Postgres/Redis; storage-agent stub with internal token - services/recommender (Python, uv): FastAPI health/readiness/internal status, `pipu-rec serve` and `pipu-rec analyze` entrypoints - packages: generated api-client, core (server URL + server check), i18n (ru/en + dictionary check), design-tokens (CSS vars + TS from tokens.json) - apps/web: Vite + React shell showing server status, language switch - apps/mobile: Expo (SDK 53) Android app with server connect screen - deploy: Dockerfiles, compose (prod + dev), nginx template, install/update scripts - .gitea/workflows: ci.yml and tag-based release.yml (images, signed APK, release) - scripts/version.mjs: version and Android versionCode from git tag - tests/fixtures/music/generate.sh: synthetic test library via ffmpeg - docs: versionCode formula, OpenAPI 3.0.3, dev instructions in README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9eb1c25147
commit
ebf296542c
98 files changed
+12130
-4
No files matched your search
@@ -0,0 +1,25 @@
|
||||
# Скопируйте в .env (install.sh делает это сам и генерирует секреты). Описание — docs/deployment.md.
|
||||
|
||||
# --- Образы ---
|
||||
REGISTRY=git.example.lan/owner
|
||||
PIPU_VERSION=0.1.0
|
||||
PIPU_DATA=/srv/pipu
|
||||
HTTP_PORT=80
|
||||
|
||||
# --- Секреты (install.sh заполнит случайными значениями) ---
|
||||
POSTGRES_PASSWORD=
|
||||
MEILI_MASTER_KEY=
|
||||
INTERNAL_TOKEN=
|
||||
|
||||
# --- api / recommender ---
|
||||
# DATABASE_URL собирается в compose.yaml из POSTGRES_PASSWORD
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
MEILI_URL=http://meilisearch:7700
|
||||
LOG_LEVEL=info
|
||||
SUPPORTED_LOCALES=ru,en
|
||||
MIN_ANDROID_VERSION_CODE=0
|
||||
|
||||
# --- Самообновление Android (M5) ---
|
||||
GITEA_URL=https://git.example.lan
|
||||
GITEA_REPO=owner/pipu-music
|
||||
GITEA_TOKEN=
|
||||
@@ -0,0 +1,69 @@
|
||||
# Зависимости для локальной разработки.
|
||||
# docker compose -f deploy/compose.dev.yaml up -d # postgres, redis, meilisearch
|
||||
# docker compose -f deploy/compose.dev.yaml --profile app up # + api и recommender из исходников
|
||||
# Затем: веб — `pnpm --filter @pipu/web dev` (проксирует /api на :8080),
|
||||
# api без docker — см. README, раздел «Разработка».
|
||||
name: pipu-dev
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
environment:
|
||||
POSTGRES_USER: pipu
|
||||
POSTGRES_PASSWORD: pipu
|
||||
POSTGRES_DB: pipu
|
||||
ports: ["5432:5432"]
|
||||
volumes: [pg-data:/var/lib/postgresql/data]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U pipu -d pipu"]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports: ["6379:6379"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.11
|
||||
environment:
|
||||
MEILI_MASTER_KEY: dev-master-key-change-me
|
||||
MEILI_ENV: development
|
||||
ports: ["7700:7700"]
|
||||
volumes: [meili-data:/meili_data]
|
||||
|
||||
api:
|
||||
profiles: [app]
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: services/api/Dockerfile
|
||||
target: api
|
||||
environment:
|
||||
DATABASE_URL: postgres://pipu:pipu@postgres:5432/pipu?sslmode=disable
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
LOG_LEVEL: debug
|
||||
ports: ["8080:8080"]
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
redis: { condition: service_healthy }
|
||||
|
||||
recommender:
|
||||
profiles: [app]
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: services/recommender/Dockerfile
|
||||
environment:
|
||||
DATABASE_URL: postgresql://pipu:pipu@postgres:5432/pipu
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
INTERNAL_TOKEN: dev-internal-token
|
||||
ports: ["8000:8000"]
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
redis: { condition: service_healthy }
|
||||
|
||||
volumes:
|
||||
pg-data:
|
||||
meili-data:
|
||||
@@ -0,0 +1,104 @@
|
||||
# Продакшен-развёртывание (docs/deployment.md). Перед первым запуском: sudo ./install.sh
|
||||
# Обновление: ./update.sh <version>
|
||||
name: pipu
|
||||
|
||||
x-library-ro: &library-ro
|
||||
type: bind
|
||||
source: ${PIPU_DATA:-/srv/pipu}/library
|
||||
target: /library
|
||||
read_only: true
|
||||
bind: { propagation: rslave }
|
||||
|
||||
x-restart: &restart
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
nginx:
|
||||
<<: *restart
|
||||
image: ${REGISTRY}/pipu-music-web:${PIPU_VERSION}
|
||||
ports: ["${HTTP_PORT:-80}:80"]
|
||||
volumes:
|
||||
- *library-ro
|
||||
- ${PIPU_DATA:-/srv/pipu}/cache:/cache:ro
|
||||
- ${PIPU_DATA:-/srv/pipu}/uploads:/uploads:ro
|
||||
depends_on: [api]
|
||||
|
||||
api:
|
||||
<<: *restart
|
||||
image: ${REGISTRY}/pipu-music-api:${PIPU_VERSION}
|
||||
env_file: .env
|
||||
environment: &db-env
|
||||
DATABASE_URL: postgres://pipu:${POSTGRES_PASSWORD}@postgres:5432/pipu?sslmode=disable
|
||||
volumes:
|
||||
- *library-ro
|
||||
- ${PIPU_DATA:-/srv/pipu}/cache:/cache
|
||||
- ${PIPU_DATA:-/srv/pipu}/updates:/updates
|
||||
- ${PIPU_DATA:-/srv/pipu}/uploads:/uploads
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
redis: { condition: service_started }
|
||||
storage-agent: { condition: service_started }
|
||||
|
||||
storage-agent:
|
||||
<<: *restart
|
||||
image: ${REGISTRY}/pipu-music-storage-agent:${PIPU_VERSION}
|
||||
cap_add: [SYS_ADMIN]
|
||||
security_opt: ["apparmor:unconfined"]
|
||||
environment:
|
||||
INTERNAL_TOKEN: ${INTERNAL_TOKEN}
|
||||
LIBRARY_ROOT: /library
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${PIPU_DATA:-/srv/pipu}/library
|
||||
target: /library
|
||||
bind: { propagation: rshared }
|
||||
- ${PIPU_DATA:-/srv/pipu}/uploads:/uploads:ro
|
||||
|
||||
recommender:
|
||||
<<: *restart
|
||||
image: ${REGISTRY}/pipu-music-recommender:${PIPU_VERSION}
|
||||
command: ["serve"]
|
||||
env_file: .env
|
||||
environment: *db-env
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
|
||||
analyzer:
|
||||
<<: *restart
|
||||
image: ${REGISTRY}/pipu-music-recommender:${PIPU_VERSION}
|
||||
command: ["analyze", "--workers", "4"]
|
||||
env_file: .env
|
||||
environment: *db-env
|
||||
volumes:
|
||||
- *library-ro
|
||||
- ${PIPU_DATA:-/srv/pipu}/models:/models
|
||||
deploy: { resources: { limits: { cpus: "4" } } }
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
|
||||
postgres:
|
||||
<<: *restart
|
||||
image: pgvector/pgvector:pg17
|
||||
environment:
|
||||
POSTGRES_USER: pipu
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: pipu
|
||||
volumes: [ "${PIPU_DATA:-/srv/pipu}/postgres:/var/lib/postgresql/data" ]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U pipu -d pipu"]
|
||||
interval: 5s
|
||||
retries: 20
|
||||
|
||||
redis:
|
||||
<<: *restart
|
||||
image: redis:7-alpine
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes: [ "${PIPU_DATA:-/srv/pipu}/redis:/data" ]
|
||||
|
||||
meilisearch:
|
||||
<<: *restart
|
||||
image: getmeili/meilisearch:v1.11
|
||||
environment:
|
||||
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
|
||||
MEILI_ENV: production
|
||||
volumes: [ "${PIPU_DATA:-/srv/pipu}/meili:/meili_data" ]
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Первичная подготовка сервера: каталоги данных, общий корень библиотеки с mount propagation, .env с секретами.
|
||||
# Запуск: sudo ./install.sh (повторный запуск безопасен)
|
||||
set -euo pipefail
|
||||
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}
|
||||
|
||||
# storage-agent монтирует NFS внутрь $DATA/library; чтобы точки монтирования видели другие контейнеры,
|
||||
# каталог должен быть отдельной точкой монтирования с shared propagation.
|
||||
if ! findmnt -n "$DATA/library" >/dev/null 2>&1; then
|
||||
echo "→ bind-mount $DATA/library (rshared)"
|
||||
mount --bind "$DATA/library" "$DATA/library"
|
||||
fi
|
||||
mount --make-rshared "$DATA/library"
|
||||
|
||||
UNIT=/etc/systemd/system/pipu-library-mount.service
|
||||
if [ -d /etc/systemd/system ] && [ ! -f "$UNIT" ]; then
|
||||
echo "→ systemd-юнит $UNIT (повторяет bind-mount после перезагрузки)"
|
||||
cat > "$UNIT" <<EOF
|
||||
[Unit]
|
||||
Description=Pipu Music library mount propagation
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c 'findmnt -n $DATA/library || mount --bind $DATA/library $DATA/library; mount --make-rshared $DATA/library'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable pipu-library-mount.service
|
||||
fi
|
||||
|
||||
secret() { head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 40; }
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "→ .env из .env.example"
|
||||
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"
|
||||
fi
|
||||
|
||||
echo "Готово. Запуск: ./update.sh \$(grep ^PIPU_VERSION= .env | cut -d= -f2)"
|
||||
@@ -0,0 +1,48 @@
|
||||
# Шаблон обрабатывается envsubst при старте контейнера nginx (переменные: API_UPSTREAM).
|
||||
# TLS терминируется здесь или на внешнем прокси — см. docs/deployment.md.
|
||||
|
||||
upstream pipu_api {
|
||||
server ${API_UPSTREAM};
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
client_max_body_size 16m;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://pipu_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
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;
|
||||
}
|
||||
|
||||
location = /healthz { proxy_pass http://pipu_api; }
|
||||
location = /readyz { proxy_pass http://pipu_api; }
|
||||
|
||||
# Аудио и обложки отдаёт nginx по заголовку X-Accel-Redirect от api (docs/playback.md).
|
||||
location /_internal/library/ { internal; alias /library/; sendfile on; aio threads; }
|
||||
location /_internal/cache/ { internal; alias /cache/transcode/; sendfile on; }
|
||||
location /_internal/covers/ { internal; alias /cache/covers/; expires 30d; }
|
||||
location /_internal/uploads/ { internal; alias /uploads/; sendfile on; }
|
||||
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header Cache-Control "no-cache";
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Обновление / откат на указанную версию: ./update.sh 1.3.0
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
VERSION="${1:?usage: $0 <version>}"
|
||||
sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$VERSION|" .env
|
||||
|
||||
docker compose pull
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
echo "→ ждём готовности api"
|
||||
for _ in $(seq 1 60); do
|
||||
if docker compose exec -T nginx wget -qO- http://localhost/readyz >/dev/null 2>&1; then
|
||||
echo "pipu $VERSION запущен"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "api не стал готов за 2 минуты: docker compose logs api" >&2
|
||||
exit 1
|
||||
Reference in new issue
Block a user