Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
11 changed files with 516 additions and 585 deletions
Showing only changes of commit 1e77394a4d - Show all commits
+10 -4
View File
@@ -15,10 +15,16 @@ artifacts/
Thumbs.db Thumbs.db
.DS_Store .DS_Store
# Secrets / local # Environment files (deploy/.env, deploy/.env.prod, web-player/.env, ...)
.env
.env.*
*.env *.env
!.env.example
!.env.prod.example
# Production env and mounted secrets # Deploy secrets and runtime state
.env.prod deploy/keys/
*.env.*
deploy/secrets/ deploy/secrets/
deploy/nginx/certs/
deploy/zitadel/.output/
deploy/.tmp/
+44 -23
View File
@@ -1,40 +1,61 @@
# Dev stack config (deploy/docker-compose.yml). # Development configuration for deploy/compose.yml + compose.dev.yml.
# Copy to deploy/.env and adjust. .env is not versioned (.gitignore ignores *.env). # Copy to deploy/.env (up.ps1 does it for you) and adjust. Not versioned.
# PostgreSQL (shared by Musify and Zitadel). Local apps use localhost:59000. DOTNET_ENVIRONMENT=Development
# --- Public URLs -------------------------------------------------------------
# How each service is reached from *outside* Docker. `host.docker.internal`
# resolves to the same address from the browser, from apps running on the host
# and from inside the containers, which is what makes the OIDC issuer and the
# presigned S3 URLs valid in both run modes.
PUBLIC_WEB_URL=http://localhost:3000
PUBLIC_API_URL=http://localhost:5111
PUBLIC_STREAM_URL=http://localhost:8081
PUBLIC_AUTH_URL=http://host.docker.internal:8080
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_DB=musify_db POSTGRES_DB=musify_db
POSTGRES_HOST_PORT=59000 POSTGRES_HOST_PORT=59000
# pgAdmin (Postgres UI). http://localhost:5050 # --- Zitadel (OIDC) ----------------------------------------------------------
PGADMIN_DEFAULT_EMAIL=admin@musify.dev # The masterkey MUST be exactly 32 characters. The external domain/port form
PGADMIN_DEFAULT_PASSWORD=admin # the token issuer, so they have to match PUBLIC_AUTH_URL. Changing them needs
PGADMIN_HOST_PORT=5050 # a re-init: ./up.ps1 -Destroy; ./up.ps1
# RabbitMQ (MassTransit)
RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
# Zitadel (OIDC). The masterkey MUST be exactly 32 characters.
ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters
# The external domain/port forms the token issuer and must match everywhere.
# host.docker.internal resolves the same from browser, local apps and containers,
# so auth works both locally and full-docker. Changing it needs a re-init
# (up.ps1 -Destroy; up.ps1). Admin login: <user>@zitadel.<domain>.
ZITADEL_EXTERNAL_DOMAIN=host.docker.internal ZITADEL_EXTERNAL_DOMAIN=host.docker.internal
ZITADEL_EXTERNAL_PORT=8080 ZITADEL_EXTERNAL_PORT=8080
ZITADEL_EXTERNAL_SECURE=false
ZITADEL_DATABASE_PASSWORD=zitadel
# Console login: <username>@zitadel.<external domain>
ZITADEL_ADMIN_USERNAME=admin ZITADEL_ADMIN_USERNAME=admin
ZITADEL_ADMIN_PASSWORD=Password1! ZITADEL_ADMIN_PASSWORD=Password1!
# SeaweedFS (S3). Must match deploy/seaweedfs/s3.conf. # OIDC clients, filled in by deploy/zitadel/provision.ps1.
AUTH_CLIENT_ID=
WEB_CLIENT_ID=
WEB_CLIENT_SECRET=
# --- RabbitMQ (MassTransit) --------------------------------------------------
RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
# --- SeaweedFS (S3) ----------------------------------------------------------
S3_ACCESS_KEY=admin_access_key S3_ACCESS_KEY=admin_access_key
S3_SECRET_KEY=admin_secret_key S3_SECRET_KEY=admin_secret_key
S3_BUCKET=webapi-storage S3_BUCKET=webapi-storage
S3_PUBLIC_URL=http://host.docker.internal:8333
S3_USE_HTTP=true
# Host paths used by up.ps1 and the apps overlay. # --- Web player --------------------------------------------------------------
STREAM_KEYS_DIR=D:/weed # Key material for the encrypted session cookie: any long random string.
WORKER_TEMP_DIR=D:/tempsFilesDev SESSION_SECRET=dev-session-secret-change-me-0123456789abcdef
# Allows the plain-HTTP Zitadel issuer in a production Node build (dev only).
AUTH_ALLOW_INSECURE_HTTP=true
# OIDC ClientId, filled in by zitadel/provision.ps1 (used by the docker path). # --- Misc --------------------------------------------------------------------
AUTH_CLIENT_ID= AUTH_REQUIRE_HTTPS=false
PGADMIN_DEFAULT_EMAIL=admin@musify.dev
PGADMIN_DEFAULT_PASSWORD=admin
PGADMIN_HOST_PORT=5050
+65
View File
@@ -0,0 +1,65 @@
# Production configuration for deploy/compose.yml + compose.prod.yml.
# Copy to deploy/.env.prod (up.sh does it for you), then:
#
# 1. replace every `example.com` with your domain,
# 2. replace every CHANGE_ME with a strong random value
# (`openssl rand -hex 24`; the Zitadel masterkey needs exactly 32 chars),
# 3. point these DNS records at the server, all proxied/TLS-terminated the
# same way: example.com, www, api, auth, stream, s3.
#
# Not versioned. Keep it readable only by the deploy user (chmod 600).
DOTNET_ENVIRONMENT=Production
# --- Domain ------------------------------------------------------------------
# ROOT_DOMAIN drives the nginx vhosts; the URLs below must agree with it.
ROOT_DOMAIN=example.com
PUBLIC_WEB_URL=https://example.com
PUBLIC_API_URL=https://api.example.com
PUBLIC_STREAM_URL=https://stream.example.com
PUBLIC_AUTH_URL=https://auth.example.com
S3_PUBLIC_URL=https://s3.example.com
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
POSTGRES_USER=postgres
POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_DB=musify_db
# --- Zitadel (OIDC) ----------------------------------------------------------
# The masterkey MUST be exactly 32 characters and can never be rotated in place.
ZITADEL_MASTERKEY=CHANGE_ME_32_CHARACTERS_EXACTLY_
ZITADEL_EXTERNAL_DOMAIN=auth.example.com
ZITADEL_EXTERNAL_PORT=443
ZITADEL_EXTERNAL_SECURE=true
ZITADEL_DATABASE_PASSWORD=CHANGE_ME
# Console login: <username>@zitadel.<external domain>
ZITADEL_ADMIN_USERNAME=admin
ZITADEL_ADMIN_PASSWORD=CHANGE_ME
# OIDC clients, filled in by deploy/zitadel/provision.sh.
AUTH_CLIENT_ID=
WEB_CLIENT_ID=
WEB_CLIENT_SECRET=
# --- RabbitMQ (MassTransit) --------------------------------------------------
RABBITMQ_DEFAULT_USER=musify
RABBITMQ_DEFAULT_PASS=CHANGE_ME
# --- SeaweedFS (S3) ----------------------------------------------------------
S3_ACCESS_KEY=CHANGE_ME
S3_SECRET_KEY=CHANGE_ME
S3_BUCKET=webapi-storage
S3_USE_HTTP=false
# --- Web player --------------------------------------------------------------
# Key material for the encrypted session cookie. Rotating it logs everyone out.
SESSION_SECRET=CHANGE_ME
AUTH_ALLOW_INSECURE_HTTP=false
# --- Misc --------------------------------------------------------------------
AUTH_REQUIRE_HTTPS=true
# Largest request body nginx accepts (audio uploads go through the web player).
MAX_UPLOAD_SIZE=1024m
# Extra yt-dlp flags for the Worker. The cookie jar avoids YouTube's bot checks;
# drop it here if you do not have one: deploy/secrets/ is mounted at /secrets.
YTDLP_ADDITIONAL_ARGUMENTS=--cookies /secrets/youtube_cookies.txt --extractor-retries 1 --socket-timeout 15 -N 4
+80
View File
@@ -0,0 +1,80 @@
name: musify-dev
# Development overlay: publishes the infrastructure ports to the host so the
# .NET apps and the web player can run from the IDE, and adds pgAdmin.
#
# docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
# docker compose -f deploy/compose.yml -f deploy/compose.dev.yml --profile apps up -d --build
#
# `host.docker.internal` (see ZITADEL_EXTERNAL_DOMAIN / *_PUBLIC_URL in .env)
# resolves to the same address from the browser, from apps on the host and from
# inside the containers, so the OIDC issuer and the presigned S3 URLs are valid
# in both modes. `extra_hosts` gives Linux containers that same name.
x-host-alias: &host-alias
- "${ZITADEL_EXTERNAL_DOMAIN}:host-gateway"
services:
postgres:
ports:
- "${POSTGRES_HOST_PORT}:5432"
zitadel:
ports:
- "${ZITADEL_EXTERNAL_PORT}:8080"
rabbitmq:
ports:
- "5672:5672" # AMQP -> MassTransit
- "15672:15672" # management UI
seaweedfs:
ports:
- "9333:9333" # master / admin UI
- "8888:8888" # filer -> StreamingGateway
- "8333:8333" # S3 API -> API, Worker and presigned uploads
jaeger:
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
# Optional Postgres UI: docker compose ... --profile tools up -d
pgadmin:
profiles: [tools]
image: dpage/pgadmin4:latest
container_name: musify-pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
PGADMIN_CONFIG_SERVER_MODE: "False"
depends_on:
postgres:
condition: service_healthy
ports:
- "${PGADMIN_HOST_PORT}:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
restart: unless-stopped
api:
ports:
- "5111:5111"
extra_hosts: *host-alias
worker:
extra_hosts: *host-alias
gateway:
ports:
- "8081:8081"
extra_hosts: *host-alias
web-player:
ports:
- "3000:3000"
extra_hosts: *host-alias
volumes:
pgadmin_data:
+40
View File
@@ -0,0 +1,40 @@
name: musify-prod
# Production overlay: adds the nginx edge and the host-mounted secrets.
# Nothing but nginx publishes a port; every other service is reachable only on
# the internal `musify-prod_default` network.
#
# docker compose --env-file deploy/.env.prod \
# -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
#
# Use ./up.sh, which also generates the stream keys, provisions Zitadel and
# applies the database migrations.
services:
worker:
# yt-dlp reads the YouTube cookie jar from here (see YTDLP_ADDITIONAL_ARGUMENTS).
volumes:
- ./secrets:/secrets:ro
nginx:
image: nginx:1.27-alpine
container_name: musify-nginx
depends_on:
- api
- gateway
- zitadel
- web-player
environment:
# The entrypoint renders /etc/nginx/templates/*.template with envsubst.
# The filter keeps it from eating nginx's own $host / $remote_addr.
MUSIFY_ROOT_DOMAIN: ${ROOT_DOMAIN}
MUSIFY_MAX_UPLOAD_SIZE: ${MAX_UPLOAD_SIZE:-1024m}
NGINX_ENVSUBST_FILTER: ^MUSIFY_
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/templates:/etc/nginx/templates:ro
- ./nginx/snippets:/etc/nginx/snippets:ro
- ./nginx/certs:/etc/nginx/certs:ro
restart: unless-stopped
+277
View File
@@ -0,0 +1,277 @@
# Musify — base stack, shared by development and production.
#
# This file publishes no host ports and hardcodes no domain: everything that
# differs between environments comes from the .env file (see .env.example) or
# from the overlay that is merged on top of it.
#
# dev docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
# prod docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d
#
# The four application services sit behind the `apps` profile, so a plain `up`
# starts only the infrastructure (the usual dev setup: apps from the IDE).
# Add `--profile apps` to run them in Docker. Use ./up.ps1 (dev) or ./up.sh
# (prod) instead of calling compose by hand.
x-app-depends: &app-depends
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
seaweedfs:
condition: service_healthy
# Backend settings that are identical for the API and the Worker. ASP.NET Core
# maps `__` to `:`, so these override the matching AppSettings.json sections.
x-backend-env: &backend-env
Database__ConnectionString: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
MassTransit__Address: "rabbitmq://rabbitmq:5672"
MassTransit__Username: ${RABBITMQ_DEFAULT_USER}
MassTransit__Password: ${RABBITMQ_DEFAULT_PASS}
# The public S3 URL, not the internal one: the same address is used to sign
# the presigned upload/download URLs the browser has to be able to reach.
InfrastructureStorage__Address: ${S3_PUBLIC_URL}
InfrastructureStorage__AccessKey: ${S3_ACCESS_KEY}
InfrastructureStorage__SecretAccessKey: ${S3_SECRET_KEY}
InfrastructureStorage__ForcePathStyle: "true"
InfrastructureStorage__UseHttp: ${S3_USE_HTTP}
ApplicationStorage__Bucket: ${S3_BUCKET}
OpenTelemetry__OtlpEndpoint: "http://jaeger:4317"
services:
# --- Infrastructure -------------------------------------------------------
postgres:
image: postgres:17-alpine
container_name: musify-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
# OIDC provider. `start-from-init` runs init + setup (both idempotent) and
# then starts, so the same command works on a cold and on a warm volume.
# It reuses the Postgres above: the admin user creates the `zitadel` database
# and role on first start.
zitadel:
image: ghcr.io/zitadel/zitadel:latest
container_name: musify-zitadel
command: start-from-init --masterkeyFromEnv --tlsMode disabled
environment:
ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY}
# The external domain/port forms the token issuer and must be identical
# for the browser, the apps and the containers (Zitadel routes by Host).
ZITADEL_EXTERNALSECURE: ${ZITADEL_EXTERNAL_SECURE}
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_EXTERNAL_DOMAIN}
ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNAL_PORT}
ZITADEL_TLS_ENABLED: "false"
ZITADEL_DATABASE_POSTGRES_HOST: postgres
ZITADEL_DATABASE_POSTGRES_PORT: 5432
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: ${ZITADEL_DATABASE_PASSWORD}
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: ${POSTGRES_USER}
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${POSTGRES_PASSWORD}
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
# Initial console admin. Login: <username>@zitadel.<external domain>
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_ADMIN_USERNAME}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_ADMIN_PASSWORD}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false"
# Service account + PAT written on init; the provisioning script uses it
# to create the Musify project and its OIDC apps.
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin Service Account
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
ZITADEL_FIRSTINSTANCE_PATPATH: /output/admin-sa.pat
depends_on:
postgres:
condition: service_healthy
volumes:
- ./zitadel/.output:/output
restart: unless-stopped
rabbitmq:
image: rabbitmq:4-management-alpine
container_name: musify-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
# All-in-one object store: master + volume + filer + S3 gateway.
# The S3 identity file is rendered from the environment on start so the
# credentials can never drift from the ones handed to the apps.
seaweedfs:
image: chrislusf/seaweedfs:latest
container_name: musify-seaweedfs
entrypoint: ["/bin/sh", "-c"]
command:
- |
cat > /tmp/s3.conf <<'JSON'
{
"identities": [
{
"name": "musify",
"credentials": [{ "accessKey": "${S3_ACCESS_KEY}", "secretKey": "${S3_SECRET_KEY}" }],
"actions": ["Admin", "Read", "Write"]
}
]
}
JSON
exec weed server \
-ip.bind=0.0.0.0 \
-dir=/data \
-master.volumeSizeLimitMB=1024 \
-volume.max=0 \
-filer \
-s3 \
-s3.config=/tmp/s3.conf
volumes:
- seaweedfs_data:/data
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
# One-shot: SeaweedFS does not auto-create buckets.
seaweedfs-init:
image: amazon/aws-cli:latest
container_name: musify-seaweedfs-init
depends_on:
seaweedfs:
condition: service_healthy
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
AWS_DEFAULT_REGION: us-east-1
entrypoint: ["/bin/sh", "-c"]
command:
- |
aws --endpoint-url http://seaweedfs:8333 s3 mb s3://${S3_BUCKET} 2>/dev/null || true
aws --endpoint-url http://seaweedfs:8333 s3 ls
restart: "no"
# OTLP collector + trace UI (MassTransit / EF Core / HTTP spans).
jaeger:
image: jaegertracing/all-in-one:latest
container_name: musify-jaeger
environment:
COLLECTOR_OTLP_ENABLED: "true"
restart: unless-stopped
# --- Applications (profile: apps) -----------------------------------------
api:
profiles: [apps]
build:
context: ../backend
dockerfile: Musify.Api.Dockerfile
container_name: musify-api
depends_on: *app-depends
environment:
<<: *backend-env
ASPNETCORE_ENVIRONMENT: ${DOTNET_ENVIRONMENT}
ASPNETCORE_URLS: http://+:5111
StreamGateway__PublicBaseUrl: ${PUBLIC_STREAM_URL}
StreamTicket__PrivateKeyPath: /keys/stream_private.pem
Workers__Routes__TemporaryFilesDirectory: /tmp/musify
# Client id and secret are written to the .env file by the Zitadel
# provisioning script (deploy/zitadel/provision.*).
Authentication__MetadataAddress: "${PUBLIC_AUTH_URL}/.well-known/openid-configuration"
Authentication__IssuerAddress: ${PUBLIC_AUTH_URL}
Authentication__ClientId: ${AUTH_CLIENT_ID}
Authentication__AudienceAddress: ${AUTH_CLIENT_ID}
Authentication__AuthorizationEndpoint: "${PUBLIC_AUTH_URL}/oauth/v2/authorize?prompt=login"
Authentication__TokenEndpoint: "${PUBLIC_AUTH_URL}/oauth/v2/token"
Authentication__ScalarRedirectUri: "${PUBLIC_API_URL}/scalar/"
Authentication__Scopes__0: openid
Authentication__Scopes__1: profile
Authentication__Scopes__2: email
Authentication__RequireHttpsMetadata: ${AUTH_REQUIRE_HTTPS}
volumes:
- ./keys:/keys:ro
restart: unless-stopped
worker:
profiles: [apps]
build:
context: ../backend
dockerfile: Musify.Worker.Dockerfile
container_name: musify-worker
depends_on: *app-depends
environment:
<<: *backend-env
DOTNET_ENVIRONMENT: ${DOTNET_ENVIRONMENT}
AudioTranscoder__Routes__WorkingDirectory: /tmp/musify
Workers__Routes__TemporaryFilesDirectory: /tmp/musify
YtDlp__AdditionalArguments: ${YTDLP_ADDITIONAL_ARGUMENTS:-}
restart: unless-stopped
# YARP in front of the SeaweedFS filer: validates the RS256 stream ticket and
# streams the bytes, so audio never goes through the API.
gateway:
profiles: [apps]
build:
context: ../backend
dockerfile: Musify.StreamingGateway.Dockerfile
container_name: musify-gateway
depends_on:
seaweedfs:
condition: service_healthy
environment:
ASPNETCORE_ENVIRONMENT: ${DOTNET_ENVIRONMENT}
ASPNETCORE_URLS: http://+:8081
StreamTicket__PublicKeyPath: /keys/stream_public.pem
ReverseProxy__Clusters__seaweed-filer__Destinations__filer__Address: "http://seaweedfs:8888/"
ReverseProxy__Routes__media__Transforms__0__PathPattern: "/buckets/${S3_BUCKET}/{**rest}"
Cors__AllowedOrigins__0: ${PUBLIC_WEB_URL}
volumes:
- ./keys:/keys:ro
restart: unless-stopped
web-player:
profiles: [apps]
build:
context: ../web-player
dockerfile: Dockerfile
container_name: musify-web-player
depends_on:
- api
environment:
NODE_ENV: production
PORT: 3000
ORIGIN: ${PUBLIC_WEB_URL}
ZITADEL_ISSUER: ${PUBLIC_AUTH_URL}
ZITADEL_CLIENT_ID: ${WEB_CLIENT_ID}
ZITADEL_CLIENT_SECRET: ${WEB_CLIENT_SECRET}
AUTH_REDIRECT_URI: "${PUBLIC_WEB_URL}/auth/callback"
AUTH_POST_LOGOUT_URI: "${PUBLIC_WEB_URL}/"
AUTH_ALLOW_INSECURE_HTTP: ${AUTH_ALLOW_INSECURE_HTTP}
SESSION_SECRET: ${SESSION_SECRET}
API_BASE_URL: http://api:5111
# Presigned PUTs are signed for the public S3 host; the container sends
# them straight to SeaweedFS instead of going back out through the edge.
S3_UPLOAD_ENDPOINT: http://seaweedfs:8333
restart: unless-stopped
volumes:
postgres_data:
rabbitmq_data:
seaweedfs_data:
-96
View File
@@ -1,96 +0,0 @@
name: musify-dev
# Optional overlay: runs the .NET apps in Docker. Merges with docker-compose.yml.
#
# docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.apps.yml up -d --build
#
# For active development prefer infra in Docker + apps from the IDE (easier to
# debug). Auth works in both modes: ZITADEL_EXTERNAL_DOMAIN=host.docker.internal
# resolves the same from the browser, local apps and containers, so the token
# issuer matches everywhere (containers reach the host via extra_hosts).
x-backend-depends: &backend-depends
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
seaweedfs:
condition: service_healthy
x-backend-env: &backend-env
Database__ConnectionString: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
MassTransit__Address: "rabbitmq://rabbitmq:5672"
MassTransit__Username: ${RABBITMQ_DEFAULT_USER}
MassTransit__Password: ${RABBITMQ_DEFAULT_PASS}
InfrastructureStorage__Address: "http://seaweedfs:8333"
InfrastructureStorage__AccessKey: ${S3_ACCESS_KEY}
InfrastructureStorage__SecretAccessKey: ${S3_SECRET_KEY}
InfrastructureStorage__ForcePathStyle: "true"
InfrastructureStorage__UseHttp: "true"
ApplicationStorage__Bucket: ${S3_BUCKET}
OpenTelemetry__OtlpEndpoint: "http://jaeger:4317"
services:
api:
build:
context: ../backend
dockerfile: Musify.Api.Dockerfile
container_name: musify-api
depends_on: *backend-depends
environment:
<<: *backend-env
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:5111
StreamGateway__PublicBaseUrl: "http://localhost:8081"
StreamTicket__PrivateKeyPath: "/keys/stream_private.pem"
# Auth values provisioned by zitadel/provision.ps1 (AUTH_CLIENT_ID in .env).
Authentication__MetadataAddress: "http://${ZITADEL_EXTERNAL_DOMAIN}:${ZITADEL_EXTERNAL_PORT}/.well-known/openid-configuration"
Authentication__IssuerAddress: "http://${ZITADEL_EXTERNAL_DOMAIN}:${ZITADEL_EXTERNAL_PORT}"
Authentication__ClientId: "${AUTH_CLIENT_ID}"
Authentication__AudienceAddress: "${AUTH_CLIENT_ID}"
Authentication__AuthorizationEndpoint: "http://${ZITADEL_EXTERNAL_DOMAIN}:${ZITADEL_EXTERNAL_PORT}/oauth/v2/authorize?prompt=login"
Authentication__TokenEndpoint: "http://${ZITADEL_EXTERNAL_DOMAIN}:${ZITADEL_EXTERNAL_PORT}/oauth/v2/token"
Authentication__ScalarRedirectUri: "http://localhost:5111/scalar/"
Authentication__Scopes__0: "openid"
Authentication__Scopes__1: "profile"
Authentication__Scopes__2: "email"
Authentication__RequireHttpsMetadata: "false"
extra_hosts:
- "${ZITADEL_EXTERNAL_DOMAIN}:host-gateway"
volumes:
- "${STREAM_KEYS_DIR:-D:/weed}:/keys:ro"
ports:
- "5111:5111"
restart: unless-stopped
worker:
build:
context: ../backend
dockerfile: Musify.Worker.Dockerfile
container_name: musify-worker
depends_on: *backend-depends
environment:
<<: *backend-env
DOTNET_ENVIRONMENT: Development
AudioTranscoder__Routes__WorkingDirectory: "/tmp/musify"
Workers__Routes__TemporaryFilesDirectory: "/tmp/musify"
restart: unless-stopped
gateway:
build:
context: ../backend
dockerfile: Musify.StreamingGateway.Dockerfile
container_name: musify-gateway
depends_on:
seaweedfs:
condition: service_healthy
environment:
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:8081
StreamTicket__PublicKeyPath: "/keys/stream_public.pem"
ReverseProxy__Clusters__seaweed-filer__Destinations__filer__Address: "http://seaweedfs:8888/"
volumes:
- "${STREAM_KEYS_DIR:-D:/weed}:/keys:ro"
ports:
- "8081:8081"
restart: unless-stopped
-273
View File
@@ -1,273 +0,0 @@
name: musify-prod
# Production stack for the VPS (ikerdemo.cat). Everything runs on the internal
# `musify-prod_default` network; only nginx publishes ports to the host (80/443).
# Config in deploy/.env.prod.
#
# docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml up -d --build
x-backend-depends: &backend-depends
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
seaweedfs:
condition: service_healthy
x-backend-env: &backend-env
Database__ConnectionString: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
MassTransit__Address: "rabbitmq://rabbitmq:5672"
MassTransit__Username: ${RABBITMQ_DEFAULT_USER}
MassTransit__Password: ${RABBITMQ_DEFAULT_PASS}
InfrastructureStorage__Address: "https://s3.ikerdemo.cat"
InfrastructureStorage__AccessKey: ${S3_ACCESS_KEY}
InfrastructureStorage__SecretAccessKey: ${S3_SECRET_KEY}
InfrastructureStorage__ForcePathStyle: "true"
InfrastructureStorage__UseHttp: "false"
ApplicationStorage__Bucket: ${S3_BUCKET}
OpenTelemetry__OtlpEndpoint: "http://jaeger:4317"
x-zitadel-db-env: &zitadel-db-env
ZITADEL_DATABASE_POSTGRES_HOST: postgres
ZITADEL_DATABASE_POSTGRES_PORT: 5432
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: ${POSTGRES_USER}
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${POSTGRES_PASSWORD}
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
services:
postgres:
image: postgres:17-alpine
container_name: musify-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
zitadel-init:
image: ghcr.io/zitadel/zitadel:latest
container_name: musify-zitadel-init
command: 'init'
environment:
<<: *zitadel-db-env
depends_on:
postgres:
condition: service_healthy
restart: "no"
zitadel-setup:
image: ghcr.io/zitadel/zitadel:latest
container_name: musify-zitadel-setup
command: 'setup --masterkeyFromEnv --tlsMode disabled'
environment:
<<: *zitadel-db-env
ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY}
ZITADEL_EXTERNALSECURE: "true"
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_EXTERNAL_DOMAIN}
ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNAL_PORT}
ZITADEL_TLS_ENABLED: "false"
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_ADMIN_USERNAME}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_ADMIN_PASSWORD}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false"
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin Service Account
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
ZITADEL_FIRSTINSTANCE_PATPATH: /output/admin-sa.pat
depends_on:
zitadel-init:
condition: service_completed_successfully
volumes:
- ./zitadel/.output:/output
restart: "no"
zitadel:
image: ghcr.io/zitadel/zitadel:latest
container_name: musify-zitadel
command: 'start --masterkeyFromEnv --tlsMode disabled'
environment:
<<: *zitadel-db-env
ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY}
ZITADEL_EXTERNALSECURE: "true"
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_EXTERNAL_DOMAIN}
ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNAL_PORT}
ZITADEL_TLS_ENABLED: "false"
depends_on:
zitadel-setup:
condition: service_completed_successfully
volumes:
- ./zitadel/.output:/output
restart: unless-stopped
rabbitmq:
image: rabbitmq:4-management-alpine
container_name: musify-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
seaweedfs:
image: chrislusf/seaweedfs:latest
container_name: musify-seaweedfs
command: >
server
-ip.bind=0.0.0.0
-dir=/data
-master.volumeSizeLimitMB=1024
-volume.max=0
-filer
-s3
-s3.config=/etc/seaweedfs/s3.conf
volumes:
- seaweedfs_data:/data
- ./seaweedfs/s3.conf:/etc/seaweedfs/s3.conf:ro
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
seaweedfs-init:
image: amazon/aws-cli:latest
container_name: musify-seaweedfs-init
depends_on:
seaweedfs:
condition: service_healthy
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
AWS_DEFAULT_REGION: us-east-1
entrypoint: ["/bin/sh", "-c"]
command:
- |
aws --endpoint-url http://seaweedfs:8333 s3 mb s3://${S3_BUCKET} 2>/dev/null || true
aws --endpoint-url http://seaweedfs:8333 s3 ls
restart: "no"
jaeger:
image: jaegertracing/all-in-one:latest
container_name: musify-jaeger
environment:
COLLECTOR_OTLP_ENABLED: "true"
restart: unless-stopped
api:
build:
context: ../backend
dockerfile: Musify.Api.Dockerfile
container_name: musify-api
depends_on: *backend-depends
environment:
<<: *backend-env
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:5111
StreamGateway__PublicBaseUrl: "https://${STREAM_DOMAIN}"
StreamTicket__PrivateKeyPath: "/keys/stream_private.pem"
Authentication__MetadataAddress: "https://${AUTH_DOMAIN}/.well-known/openid-configuration"
Authentication__IssuerAddress: "https://${AUTH_DOMAIN}"
Authentication__ClientId: "${AUTH_CLIENT_ID}"
Authentication__AudienceAddress: "${AUTH_CLIENT_ID}"
Authentication__AuthorizationEndpoint: "https://${AUTH_DOMAIN}/oauth/v2/authorize?prompt=login"
Authentication__TokenEndpoint: "https://${AUTH_DOMAIN}/oauth/v2/token"
Authentication__ScalarRedirectUri: "https://${API_DOMAIN}/scalar/"
Authentication__Scopes__0: "openid"
Authentication__Scopes__1: "profile"
Authentication__Scopes__2: "email"
Authentication__RequireHttpsMetadata: "true"
YtDlp__AdditionalArguments: "--cookies /secrets/youtube_cookies.txt --extractor-retries 1 --socket-timeout 15"
volumes:
- ./keys:/keys:ro
- ./secrets/youtube_cookies.txt:/secrets/youtube_cookies.txt
restart: unless-stopped
worker:
build:
context: ../backend
dockerfile: Musify.Worker.Dockerfile
container_name: musify-worker
depends_on: *backend-depends
environment:
<<: *backend-env
DOTNET_ENVIRONMENT: Production
AudioTranscoder__Routes__WorkingDirectory: "/tmp/musify"
Workers__Routes__TemporaryFilesDirectory: "/tmp/musify"
YtDlp__AdditionalArguments: "--cookies /secrets/youtube_cookies.txt --extractor-retries 1 --socket-timeout 15 -N 4"
volumes:
- ./secrets/youtube_cookies.txt:/secrets/youtube_cookies.txt
restart: unless-stopped
gateway:
build:
context: ../backend
dockerfile: Musify.StreamingGateway.Dockerfile
container_name: musify-gateway
depends_on:
seaweedfs:
condition: service_healthy
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8081
StreamTicket__PublicKeyPath: "/keys/stream_public.pem"
ReverseProxy__Clusters__seaweed-filer__Destinations__filer__Address: "http://seaweedfs:8888/"
volumes:
- ./keys:/keys:ro
restart: unless-stopped
web-player:
build:
context: ../web-player
dockerfile: Dockerfile
container_name: musify-web-player
depends_on:
- api
environment:
NODE_ENV: production
ORIGIN: "https://${WEB_DOMAIN}"
ZITADEL_ISSUER: "https://${AUTH_DOMAIN}"
ZITADEL_CLIENT_ID: "${WEB_CLIENT_ID}"
ZITADEL_CLIENT_SECRET: "${WEB_CLIENT_SECRET}"
AUTH_REDIRECT_URI: "https://${WEB_DOMAIN}/auth/callback"
AUTH_POST_LOGOUT_URI: "https://${WEB_DOMAIN}/"
SESSION_SECRET: "${SESSION_SECRET}"
API_BASE_URL: "http://api:5111"
restart: unless-stopped
nginx:
image: nginx:1.27-alpine
container_name: musify-nginx
depends_on:
- api
- gateway
- zitadel
- web-player
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro
restart: unless-stopped
volumes:
postgres_data:
rabbitmq_data:
seaweedfs_data:
-163
View File
@@ -1,163 +0,0 @@
name: musify-dev
# Infrastructure stack for Musify development: everything the backend needs
# (Postgres, Zitadel, RabbitMQ, SeaweedFS + bucket, Jaeger/OTLP). The .NET apps
# run from the IDE and connect over the published ports. Config in deploy/.env
#
# docker compose -f deploy/docker-compose.yml up -d
services:
postgres:
image: postgres:17-alpine
container_name: musify-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "${POSTGRES_HOST_PORT}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
pgadmin:
image: dpage/pgadmin4:latest
container_name: musify-pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
PGADMIN_CONFIG_SERVER_MODE: "False"
depends_on:
postgres:
condition: service_healthy
ports:
- "${PGADMIN_HOST_PORT}:80"
volumes:
- pgadmin_data:/var/lib/pgadmin
restart: unless-stopped
zitadel:
image: ghcr.io/zitadel/zitadel:latest
container_name: musify-zitadel
command: 'start-from-init --masterkeyFromEnv --tlsMode disabled'
environment:
ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY}
ZITADEL_EXTERNALSECURE: "false"
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_EXTERNAL_DOMAIN}
ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNAL_PORT}
ZITADEL_TLS_ENABLED: "false"
# Reuses the same Postgres: the admin user creates the 'zitadel' database
# and the 'zitadel' user on first start.
ZITADEL_DATABASE_POSTGRES_HOST: postgres
ZITADEL_DATABASE_POSTGRES_PORT: 5432
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: ${POSTGRES_USER}
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: ${POSTGRES_PASSWORD}
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
# Initial console admin. Login: <user>@zitadel.<domain>
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_ADMIN_USERNAME}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_ADMIN_PASSWORD}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false"
# Service account + PAT for automatic provisioning. Zitadel writes the PAT
# to PATPATH on init; provision.ps1 uses it to create the project and app.
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin Service Account
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
ZITADEL_FIRSTINSTANCE_PATPATH: /output/admin-sa.pat
depends_on:
postgres:
condition: service_healthy
ports:
- "${ZITADEL_EXTERNAL_PORT}:8080"
volumes:
- ./zitadel/.output:/output
restart: unless-stopped
rabbitmq:
image: rabbitmq:4-management-alpine
container_name: musify-rabbitmq
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
ports:
- "5672:5672" # AMQP -> MassTransit (API + Worker)
- "15672:15672" # management UI
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
seaweedfs:
image: chrislusf/seaweedfs:latest
container_name: musify-seaweedfs
# All-in-one dev server: master + volume + filer + S3 gateway.
command: >
server
-ip.bind=0.0.0.0
-dir=/data
-master.volumeSizeLimitMB=1024
-volume.max=0
-filer
-s3
-s3.config=/etc/seaweedfs/s3.conf
ports:
- "9333:9333" # master (admin/UI)
- "8888:8888" # filer HTTP -> StreamingGateway
- "8333:8333" # S3 API -> WebApi/Worker (InfrastructureStorage:Address)
volumes:
- seaweedfs_data:/data
- ./seaweedfs/s3.conf:/etc/seaweedfs/s3.conf:ro
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"]
interval: 10s
timeout: 5s
retries: 10
restart: unless-stopped
# One-shot: creates the app bucket in SeaweedFS (it is not auto-created).
seaweedfs-init:
image: amazon/aws-cli:latest
container_name: musify-seaweedfs-init
depends_on:
seaweedfs:
condition: service_healthy
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
AWS_DEFAULT_REGION: us-east-1
entrypoint: ["/bin/sh", "-c"]
command:
- |
aws --endpoint-url http://seaweedfs:8333 s3 mb s3://${S3_BUCKET} 2>/dev/null || true
aws --endpoint-url http://seaweedfs:8333 s3 ls
restart: "no"
# OTLP collector + trace UI (MassTransit/EF/HTTP spans). OtlpEndpoint = :4317
jaeger:
image: jaegertracing/all-in-one:latest
container_name: musify-jaeger
environment:
COLLECTOR_OTLP_ENABLED: "true"
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
restart: unless-stopped
volumes:
postgres_data:
pgadmin_data:
rabbitmq_data:
seaweedfs_data:
-24
View File
@@ -1,24 +0,0 @@
{
"identities": [
{
"name": "admin_user",
"credentials": [
{
"accessKey": "admin_access_key",
"secretKey": "admin_secret_key"
}
],
"actions": ["Admin", "Read", "Write"]
},
{
"name": "read_only_user",
"credentials": [
{
"accessKey": "readonly_access_key",
"secretKey": "readonly_secret_key"
}
],
"actions": ["Read"]
}
]
}
-2
View File
@@ -1,2 +0,0 @@
# Secrets Zitadel writes on init (do not version)
.output/