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: