From a1213ae2562b1848a9876928f5160d12b91c9f94 Mon Sep 17 00:00:00 2001 From: ollyhearn Date: Sat, 6 Jun 2026 13:08:05 +0300 Subject: [PATCH] initial --- .env.example | 44 +++++++++++++++++ .gitignore | 4 ++ .gitmodules | 6 +++ Makefile | 105 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 120 +++++++++++++++++++++++++++++++++++++++++++++ mcma-backend | 1 + mcma-webui | 1 + nginx.conf | 60 +++++++++++++++++++++++ 8 files changed, 341 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 160000 mcma-backend create mode 160000 mcma-webui create mode 100644 nginx.conf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d29c2cd --- /dev/null +++ b/.env.example @@ -0,0 +1,44 @@ +# ====================================================================== +# mycoolmusicapp — combined env for docker compose (dev). +# Copy to .env: cp .env.example .env (or: make env) +# Compose injects this into api, worker and webui. Never commit real .env. +# Per-repo .env.example files still exist for running a service standalone. +# ====================================================================== + +# ---- Postgres (db service) ------------------------------------------- +POSTGRES_USER=mcma +POSTGRES_PASSWORD=mcma +POSTGRES_DB=mcma +POSTGRES_PORT=5432 +REDIS_PORT=6379 + +# ---- Backend (api + worker) ------------------------------------------ +ENVIRONMENT=dev # dev | test | prod +LOG_LEVEL=INFO +LOG_JSON=false # true in prod + +# DATABASE_URL / REDIS_URL are overridden in compose to point at the db/redis +# services. These localhost values are the fallback for host-run processes. +DATABASE_URL=postgresql+asyncpg://mcma:mcma@localhost:5432/mcma +DB_ECHO=false +REDIS_URL=redis://localhost:6379/0 + +# auth — GENERATE a strong secret for prod: `openssl rand -hex 32` +JWT_SECRET=change-me-in-prod +ACCESS_TOKEN_TTL_SECONDS=900 +REFRESH_TOKEN_TTL_SECONDS=2592000 + +# media / storage (paths inside the container) +MEDIA_PATH=/data/media +TRANSCODE_CACHE_PATH=/data/transcode-cache +MAX_PARALLEL_DOWNLOADS=2 + +# external services (all optional — backend degrades gracefully if unset) +# ML_SERVICE_URL=http://ml:9000 +# ACOUSTID_API_KEY= +MUSICBRAINZ_USER_AGENT=mcma-backend/0.1.0 ( https://github.com/your/repo ) +# YOUTUBE_COOKIES_PATH=/data/cookies.txt + +# ---- Frontend (webui) ------------------------------------------------ +# Served same-origin behind nginx, so the default '/api/v1' just works. +PUBLIC_API_BASE_URL=/api/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b15c3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +*.zip +plans/ +modern-sk/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0e05a96 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "mcma-backend"] + path = mcma-backend + url = ssh://git@git.ollyhearn.ru:49239/olly/mcma-backend.git +[submodule "mcma-webui"] + path = mcma-webui + url = ssh://git@git.ollyhearn.ru:49239/olly/mcma-webui.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f563ad6 --- /dev/null +++ b/Makefile @@ -0,0 +1,105 @@ +# mycoolmusicapp — dev workflow shortcuts. +# `make` or `make help` lists targets. + +COMPOSE := docker compose + +.DEFAULT_GOAL := help +.PHONY: help env up build rebuild down stop restart ps logs logs-api logs-webui \ + sh-api sh-webui db-shell redis-cli migrate makemigration downgrade \ + test test-api test-webui lint fmt clean prune \ + prod-build prod-build-api prod-build-webui + +help: ## Show this help + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' + +env: ## Create .env from .env.example (no overwrite) + @test -f .env || cp .env.example .env && echo ".env ready" + +## ---- lifecycle ------------------------------------------------------- +up: env ## Build (if needed) + start the full dev stack + $(COMPOSE) up --build + +build: ## Build all images + $(COMPOSE) build + +rebuild: ## Rebuild images with no cache + $(COMPOSE) build --no-cache + +down: ## Stop and remove containers + $(COMPOSE) down + +stop: ## Stop containers (keep them) + $(COMPOSE) stop + +restart: ## Restart all services + $(COMPOSE) restart + +ps: ## Show container status + $(COMPOSE) ps + +## ---- logs ------------------------------------------------------------ +logs: ## Tail logs (all services) + $(COMPOSE) logs -f --tail=100 + +logs-api: ## Tail api logs + $(COMPOSE) logs -f --tail=100 api + +logs-webui: ## Tail webui logs + $(COMPOSE) logs -f --tail=100 webui + +## ---- shells ---------------------------------------------------------- +sh-api: ## Shell into the api container + $(COMPOSE) exec api bash + +sh-webui: ## Shell into the webui container + $(COMPOSE) exec webui sh + +db-shell: ## psql into the database + $(COMPOSE) exec db psql -U $${POSTGRES_USER:-mcma} -d $${POSTGRES_DB:-mcma} + +redis-cli: ## redis-cli into redis + $(COMPOSE) exec redis redis-cli + +## ---- database migrations (alembic) ----------------------------------- +migrate: ## Apply latest migrations + $(COMPOSE) exec api alembic upgrade head + +makemigration: ## Autogenerate a migration: make makemigration m="msg" + $(COMPOSE) exec api alembic revision --autogenerate -m "$(m)" + +downgrade: ## Roll back one migration + $(COMPOSE) exec api alembic downgrade -1 + +## ---- quality --------------------------------------------------------- +test: test-api test-webui ## Run all tests + +test-api: ## Run backend tests + $(COMPOSE) exec api pytest + +test-webui: ## Run frontend tests + $(COMPOSE) exec webui npm test + +lint: ## Lint backend + frontend + $(COMPOSE) exec api ruff check . + $(COMPOSE) exec webui npm run lint + +fmt: ## Format backend + frontend + $(COMPOSE) exec api ruff format . + $(COMPOSE) exec webui npm run format + +## ---- cleanup --------------------------------------------------------- +clean: ## Stop + remove containers AND volumes (DESTROYS db data) + $(COMPOSE) down -v + +prune: ## Docker system prune (dangling images/build cache) + docker system prune -f + +## ---- production image builds (no compose yet) ------------------------ +prod-build: prod-build-api prod-build-webui ## Build both prod images + +prod-build-api: ## Build the backend prod image + docker build -f mcma-backend/dockerfiles/Dockerfile.prod -t mcma-backend:prod ./mcma-backend + +prod-build-webui: ## Build the webui prod image + docker build -f mcma-webui/dockerfiles/Dockerfile.prod -t mcma-webui:prod ./mcma-webui diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..118e928 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,120 @@ +# DEV stack for mycoolmusicapp — one entrypoint at http://localhost:80. +# +# make up # build + start everything (db, redis, api, worker, webui, nginx) +# make logs # tail logs +# make down # stop +# +# Source is bind-mounted into api/worker/webui, so edits hot-reload without a +# rebuild. PROD has NO compose yet — services there are built straight from +# their dockerfiles/Dockerfile.prod (a separate prod compose lands later). +# +# Env: copy .env.example -> .env at the repo root (single combined file). + +services: + # -- backing services ----------------------------------------------------- + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: ${POSTGRES_USER:-mcma} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mcma} + POSTGRES_DB: ${POSTGRES_DB:-mcma} + ports: + - "${POSTGRES_PORT:-5432}:5432" # exposed so host-run tests can reach it + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mcma}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + redis: + image: redis:7-alpine + command: redis-server --save 60 1 --loglevel warning + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - redisdata:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # -- application services ------------------------------------------------- + api: + build: + context: ./mcma-backend + dockerfile: dockerfiles/Dockerfile.dev + env_file: .env + environment: + # Service-name URLs override the localhost values from .env. + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-mcma}:${POSTGRES_PASSWORD:-mcma}@db:5432/${POSTGRES_DB:-mcma} + REDIS_URL: redis://redis:6379/0 + volumes: + - ./mcma-backend:/app # live source (hot reload) + - /app/.venv # keep the image's venv, don't shadow it + - media:/data/media + - transcode_cache:/data/transcode-cache + ports: + - "8000:8000" # direct access for debugging / docs + depends_on: + db: { condition: service_healthy } + redis: { condition: service_healthy } + restart: unless-stopped + + worker: + build: + context: ./mcma-backend + dockerfile: dockerfiles/Dockerfile.dev + command: arq app.workers.arq_worker.WorkerSettings + env_file: .env + environment: + DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-mcma}:${POSTGRES_PASSWORD:-mcma}@db:5432/${POSTGRES_DB:-mcma} + REDIS_URL: redis://redis:6379/0 + volumes: + - ./mcma-backend:/app + - /app/.venv + - media:/data/media + - transcode_cache:/data/transcode-cache + depends_on: + db: { condition: service_healthy } + redis: { condition: service_healthy } + restart: unless-stopped + + webui: + build: + context: ./mcma-webui + dockerfile: dockerfiles/Dockerfile.dev + env_file: .env + environment: + RSBUILD_HOST: 0.0.0.0 + RSBUILD_PORT: "3000" + # Browser reaches HMR through nginx on :80, not the container's :3000. + RSBUILD_HMR_CLIENT_PORT: "80" + volumes: + - ./mcma-webui:/app + - /app/node_modules # keep the image's install, don't shadow it + expose: + - "3000" + restart: unless-stopped + + # -- reverse proxy: single entrypoint at localhost:80 --------------------- + nginx: + image: nginx:1.27-alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - api + - webui + restart: unless-stopped + +volumes: + pgdata: + redisdata: + media: + transcode_cache: diff --git a/mcma-backend b/mcma-backend new file mode 160000 index 0000000..87b48e9 --- /dev/null +++ b/mcma-backend @@ -0,0 +1 @@ +Subproject commit 87b48e941eb5c95f2356ca5649a9abd4adf21bd4 diff --git a/mcma-webui b/mcma-webui new file mode 160000 index 0000000..37c1a59 --- /dev/null +++ b/mcma-webui @@ -0,0 +1 @@ +Subproject commit 37c1a5944aec26264222b392c3e0e5674de831c8 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..7b01e56 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,60 @@ +# DEV reverse proxy — single entrypoint at http://localhost:80. +# / -> webui rsbuild dev server (with HMR websocket) +# /rsbuild-hmr -> webui HMR websocket channel +# /api/... -> backend API +# /health... -> backend health/readiness probes +# +# This is the DEV topology only. Production gets its own proxy in the future +# prod compose (the webui PROD image already self-serves static assets). + +# WebSocket upgrade plumbing. +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream webui_dev { server webui:3000; } +upstream api_dev { server api:8000; } + +server { + listen 80; + server_name localhost; + + # Large media uploads — don't choke on big files. + client_max_body_size 0; + + # ---- backend ------------------------------------------------------- + location /api/ { + proxy_pass http://api_dev; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_buffering off; + } + + location /health { + proxy_pass http://api_dev; + proxy_set_header Host $host; + } + + # ---- rsbuild HMR websocket ---------------------------------------- + location /rsbuild-hmr { + proxy_pass http://webui_dev; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } + + # ---- webui dev server (catch-all) --------------------------------- + location / { + proxy_pass http://webui_dev; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +}