ikerdemo.cat was hardcoded in six server blocks, so the stack could only be deployed under that one domain. The vhosts are now a template rendered by the nginx entrypoint from MUSIFY_ROOT_DOMAIN, with the envsubst filter set so it does not eat nginx's own variables. The repeated proxy headers move to a shared snippet, the web player vhost gets the upload body limit it was missing (audio is posted to its form actions), and the streaming vhost turns off proxy buffering so range requests stay responsive.
Development deploy
Everything the backend needs for development, in one place. A single
docker-compose.yml brings up PostgreSQL, Zitadel, RabbitMQ, SeaweedFS and
Jaeger; the .NET apps run from the IDE (or, optionally, in Docker too).
Quick start
# From the repo root (Musify/)
./deploy/up.ps1
up.ps1 does the full bootstrap:
- Creates
deploy/.envfrom.env.exampleif missing. - Starts the stack (
docker compose up -d) and waits for Postgres to be healthy. - Creates the
webapi-storagebucket in SeaweedFS (seaweedfs-initservice). - Generates the RS256 stream-ticket keys in
D:\weedif missing. - Creates the Worker temp directory (
D:\tempsFilesDev). - Applies the EF Core migrations to
musify_db. - Provisions Zitadel automatically (project + OIDC app with JWT token) and
wires the ClientId into
Musify.Apiuser-secrets and.env.
When it finishes it prints a summary with URLs and credentials. No manual steps.
Without the script (infra only)
docker compose -f deploy/docker-compose.yml up -d
The compose file still creates the bucket on its own; run migrations with
dotnet ef database update (see ../docs/development.md).
Services and ports
| Service | Port(s) | Used for |
|---|---|---|
| PostgreSQL | 59000 → 5432 |
API + Worker (EF Core) and the Zitadel database |
| Zitadel | 8080 |
OIDC/OAuth2 (console at /ui/console) |
| RabbitMQ | 5672 / 15672 |
MassTransit / management UI |
| SeaweedFS S3 | 8333 |
S3 (presigned PUT/GET) |
| SeaweedFS filer | 8888 |
serves files → StreamingGateway |
| SeaweedFS master | 9333 |
admin/UI |
| Jaeger | 16686 / 4317 / 4318 |
OTLP traces (UI / gRPC / HTTP) |
Ports are published to the host, so apps run locally connect without changing
AppSettings.Development.json (Postgres, RabbitMQ, S3, OTLP).
Authentication (Zitadel) — automatic
There are no manual steps. up.ps1 runs zitadel/provision.ps1, which
idempotently:
- Uses a service account with a PAT that Zitadel writes on init
(
zitadel/.output/admin-sa.pat). - Creates (or reuses) the project
Musifyand an OIDC app (PKCE,devMode, access token = JWT — required forJwtBearerto validate). - Captures the generated ClientId and writes it to:
Musify.Apiuser-secrets (for running the apps locally), anddeploy/.envasAUTH_CLIENT_ID(for the full-docker path).
The values stay stable while the Postgres volume persists. After up.ps1 -Destroy
the next up.ps1 re-provisions (new ClientId, re-wired).
- Console:
http://host.docker.internal:8080/ui/console - Admin login:
admin@zitadel.host.docker.internal/Password1!(see.env). - Test login: open Scalar (
http://localhost:5111/scalar/v1) and authorize.
Why host.docker.internal
ZITADEL_EXTERNAL_DOMAIN=host.docker.internal forms the token issuer and
resolves the same from the browser, local apps and containers, so token
validation works both ways (local and full-docker). Zitadel routes by the Host
header, so the domain must be identical everywhere. If you only run the apps
locally you can use localhost. Changing the domain requires a re-init:
up.ps1 -Destroy; up.ps1.
Running the apps
Recommended: from the IDE (best for debugging)
dotnet run --project backend/Musify.Api # :5111
dotnet run --project backend/Musify.StreamingGateway # :8081
dotnet run --project backend/Musify.Worker # background
Host requirements: .NET 10 SDK, ffmpeg on PATH (Worker audio transcode).
Optional: everything in Docker
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.apps.yml up -d --build
Builds and runs api, worker and gateway as containers (the Worker already
ships ffmpeg). The stream-ticket keys are mounted from D:\weed (configurable via
STREAM_KEYS_DIR in .env). Auth works in this mode too (the API downloads the
Zitadel JWKS via host.docker.internal).
Note: on Linux the apps look for
appsettings*.jsonin lowercase. TheApi/Workerprojects useAppSettings*.json(PascalCase), so their Dockerfiles create lowercase copies at publish time. On Windows it did not matter (case-insensitive).
Useful commands
./deploy/up.ps1 -SkipMigrations # bring up without touching the DB
./deploy/up.ps1 -Down # stop and remove containers (keep data)
./deploy/up.ps1 -Destroy # stop and remove containers + volumes (wipe data)
docker compose -f deploy/docker-compose.yml logs -f zitadel
docker compose -f deploy/docker-compose.yml ps
Project layout — what each file does
deploy/
├─ docker-compose.yml # infrastructure stack (single source of truth)
├─ docker-compose.apps.yml # optional overlay: the 3 .NET apps in Docker
├─ .env # local config (secrets/ports); not versioned
├─ .env.example # config template (versioned)
├─ up.ps1 # orchestrator: up / down / destroy
├─ README.md # this file
├─ scripts/
│ ├─ common.ps1 # shared helpers (Read-DotEnv, Write-Step)
│ ├─ stream-keys.ps1 # generates the RS256 stream-ticket key pair
│ └─ migrate.ps1 # applies EF Core migrations
├─ seaweedfs/
│ └─ s3.conf # SeaweedFS S3 identities/credentials
└─ zitadel/
├─ provision.ps1 # creates project + OIDC app, wires the ClientId
└─ .output/ # PAT + init secrets written by Zitadel (not versioned)
backend/
├─ Musify.Api.Dockerfile # API image
├─ Musify.Worker.Dockerfile # Worker image (includes ffmpeg)
├─ Musify.StreamingGateway.Dockerfile # Gateway image
└─ .dockerignore
Compose
docker-compose.yml— the infra stack (postgres,zitadel,rabbitmq,seaweedfs, the one-shotseaweedfs-initbucket creator, andjaeger). This is the single source of truth for services, ports and volumes. Values come from.env.docker-compose.apps.yml— an optional overlay that adds theapi,workerandgatewayservices. It merges with the infra file (samemusify-devproject and network) and shares the backend env via YAML anchors. Used to run the whole stack in Docker; for active development prefer running the apps from the IDE.
Config
.env— the actual values used by compose (Postgres/RabbitMQ/Zitadel/S3 credentials, ports, host paths,AUTH_CLIENT_ID). Ignored by git (*.env)..env.example— the versioned template. Copy it to.env(or letup.ps1do it) and adjust.
Scripts (PowerShell)
up.ps1— the orchestrator. Parses.env, starts the stack, waits for Postgres, then calls the sub-scripts in order (keys → migrations → provisioning) and prints the summary. Also handles-Down/-Destroy/-SkipMigrations.scripts/common.ps1— shared helpers dot-sourced by the others:Read-DotEnv(parse.envinto a hashtable) andWrite-Step(step logging).scripts/stream-keys.ps1— generates the RS256 key pair that the API uses to sign stream tickets and the Gateway uses to verify them. No-op if the keys exist.scripts/migrate.ps1— sets the connection string in user-secrets and runsdotnet ef database update(startup project =Musify.Infrastructure).zitadel/provision.ps1— reads the service-account PAT, creates/reuses the Zitadel project and OIDC app (JWT token), and wires the ClientId intoMusify.Apiuser-secrets and.env. Idempotent; can be run standalone.
Service configs
seaweedfs/s3.conf— the S3 identities (access/secret keys) SeaweedFS loads. Must matchInfrastructureStorage:*in the app settings andS3_*in.env.zitadel/.output/— created at runtime; holds the PAT Zitadel writes on init (admin-sa.pat). Not versioned; wiped byup.ps1 -Destroy.
Dockerfiles (in backend/)
Musify.Api.Dockerfile/Musify.Worker.Dockerfile/Musify.StreamingGateway.Dockerfile— multi-stage builds for each host. The Worker image also installs ffmpeg. Built by the apps overlay; build context isbackend/so restore can copy the central props and all csproj files..dockerignore— keepsbin/,obj/, IDE folders andDesignSettings.jsonout of the build context.