refactor(deploy): merge the three compose files into a base plus overlays

The dev and prod stacks each declared the same nine services, which is how the
S3 credentials and the gateway configuration ended up diverging. compose.yml
now holds every service with no published port and no domain in it, and the
overlays only add what is specific: host ports and pgAdmin for development,
the nginx edge and the mounted secrets for production. The applications sit
behind the `apps` profile, so a plain `up` still starts infrastructure only.

Everything environment-specific moves to the env files: the public URLs, the
credentials and the environment name. The SeaweedFS identity file is rendered
inside the container from S3_ACCESS_KEY and S3_SECRET_KEY, so the store and
the applications can no longer be given different keys.
This commit is contained in:
2026-07-20 15:13:39 +02:00
parent 6156101029
commit a779a8bf73
11 changed files with 516 additions and 585 deletions
+10 -4
View File
@@ -15,10 +15,16 @@ artifacts/
Thumbs.db
.DS_Store
# Secrets / local
# Environment files (deploy/.env, deploy/.env.prod, web-player/.env, ...)
.env
.env.*
*.env
!.env.example
!.env.prod.example
# Production env and mounted secrets
.env.prod
*.env.*
# Deploy secrets and runtime state
deploy/keys/
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).
# Copy to deploy/.env and adjust. .env is not versioned (.gitignore ignores *.env).
# Development configuration for deploy/compose.yml + compose.dev.yml.
# 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_PASSWORD=postgres
POSTGRES_DB=musify_db
POSTGRES_HOST_PORT=59000
# pgAdmin (Postgres UI). http://localhost:5050
PGADMIN_DEFAULT_EMAIL=admin@musify.dev
PGADMIN_DEFAULT_PASSWORD=admin
PGADMIN_HOST_PORT=5050
# RabbitMQ (MassTransit)
RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
# Zitadel (OIDC). The masterkey MUST be exactly 32 characters.
# --- Zitadel (OIDC) ----------------------------------------------------------
# The masterkey MUST be exactly 32 characters. The external domain/port form
# the token issuer, so they have to match PUBLIC_AUTH_URL. Changing them needs
# a re-init: ./up.ps1 -Destroy; ./up.ps1
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_PORT=8080
ZITADEL_EXTERNAL_SECURE=false
ZITADEL_DATABASE_PASSWORD=zitadel
# Console login: <username>@zitadel.<external domain>
ZITADEL_ADMIN_USERNAME=admin
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_SECRET_KEY=admin_secret_key
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.
STREAM_KEYS_DIR=D:/weed
WORKER_TEMP_DIR=D:/tempsFilesDev
# --- Web player --------------------------------------------------------------
# Key material for the encrypted session cookie: any long random string.
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).
AUTH_CLIENT_ID=
# --- Misc --------------------------------------------------------------------
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/