Move shared infrastructure to a separate Infrastructure repo

Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger no longer run as part
of this stack. They're shared services (used by Musify, Ping and
docker-manager) that now live in a separate Infrastructure repository
and must already be running.

deploy/compose.yml keeps only what's specific to Musify: its own
database connection, its own OIDC app and storage bucket (created
against the shared Zitadel/SeaweedFS via zitadel-init/seaweedfs-init),
and the application containers. A new wait-for-postgres one-shot
replaces the in-file Postgres health check, which no longer works now
that Postgres lives in a different compose project.

compose.prod.yml joins Infrastructure's infra-net network instead of
running its own copies. Docs and READMEs updated to match.
This commit is contained in:
2026-09-21 22:09:08 +02:00
parent c5748e9765
commit 63018a7c72
11 changed files with 261 additions and 369 deletions
+60 -168
View File
@@ -1,33 +1,32 @@
# Musify — base stack, shared by development and production.
# 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.
# Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are NOT part of this
# file. They're shared infrastructure that lives in ../../../Infrastructure
# and must already be running (see Infrastructure/README.md). This file only
# has what's specific to Musify: its own database, its own OIDC apps and
# storage bucket (created against the shared instances below), and its own
# apps.
#
# 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 the infrastructure plus the one-shot bootstrap jobs (stream-ticket
# keys, database migrations, Zitadel project/apps) and stops there — the usual
# dev setup, with the apps running from the IDE. Add `--profile apps up -d
# --build` as a SECOND command to also build and run them in Docker: by then
# zitadel-init has already written the OIDC client ids into the env file, so
# the apps start with real values. See deploy/README.md.
x-app-depends: &app-depends
postgres:
condition: service_healthy
rabbitmq:
condition: service_healthy
seaweedfs:
condition: service_healthy
# The four application services sit behind the `apps` profile, so a plain
# `up` runs only the one-shot bootstrap jobs (stream-ticket keys, database
# migrations, Zitadel project/apps, the storage bucket) and stops there. That's
# the usual dev setup, with the apps running from the IDE. Add `--profile apps
# up -d --build` as a SECOND command to also build and run them in Docker: by
# then zitadel-init has already written the OIDC client ids into the env
# file, so the apps start with real values. See deploy/README.md.
# Backend settings that are identical for the API and the Worker. ASP.NET Core
# maps `__` to `:`, so these override the matching AppSettings.json sections.
# POSTGRES_HOST / RABBITMQ_HOST / JAEGER_HOST differ between environments
# (host.docker.internal in dev, the container name over infra-net in prod,
# see .env.example / .env.prod.example), same as PUBLIC_AUTH_URL/S3_PUBLIC_URL
# already do.
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"
Database__ConnectionString: "Host=${POSTGRES_HOST};Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
MassTransit__Address: "rabbitmq://${RABBITMQ_HOST}: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
@@ -38,128 +37,35 @@ x-backend-env: &backend-env
InfrastructureStorage__ForcePathStyle: "true"
InfrastructureStorage__UseHttp: ${S3_USE_HTTP}
ApplicationStorage__Bucket: ${S3_BUCKET}
OpenTelemetry__OtlpEndpoint: "http://jaeger:4317"
OpenTelemetry__OtlpEndpoint: "http://${JAEGER_HOST}:4317"
services:
# --- Infrastructure -------------------------------------------------------
# --- Shared-infrastructure bootstrap ---------------------------------------
postgres:
# Waits for Infrastructure's Postgres to accept connections before `migrate`
# runs. This replaces a `depends_on: postgres: condition: service_healthy`:
# that only works within the same compose file, and Postgres now lives in a
# separate one (../../../Infrastructure).
wait-for-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
container_name: musify-wait-for-postgres
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
set -e
deadline=$$(( $$(date +%s) + 60 ))
until pg_isready -h ${POSTGRES_HOST} -p 5432 -U ${POSTGRES_USER} >/dev/null 2>&1; do
[ "$$(date +%s)" -lt "$$deadline" ] || { echo "postgres did not respond at ${POSTGRES_HOST}:5432. Is Infrastructure running?" >&2; exit 1; }
sleep 2
done
restart: "no"
# One-shot: SeaweedFS does not auto-create buckets.
# One-shot: SeaweedFS does not auto-create buckets. `aws s3 ls` doubles as
# the readiness probe, since seaweedfs-init no longer has an in-file
# `depends_on: seaweedfs: condition: service_healthy` to wait for.
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}
@@ -167,13 +73,19 @@ services:
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
set -e
deadline=$$(( $$(date +%s) + 60 ))
until aws --endpoint-url ${S3_PUBLIC_URL} s3 ls >/dev/null 2>&1; do
[ "$$(date +%s)" -lt "$$deadline" ] || { echo "seaweedfs did not respond at ${S3_PUBLIC_URL}. Is Infrastructure running?" >&2; exit 1; }
sleep 2
done
aws --endpoint-url ${S3_PUBLIC_URL} s3 mb s3://${S3_BUCKET} 2>/dev/null || true
aws --endpoint-url ${S3_PUBLIC_URL} s3 ls
restart: "no"
# One-shot: generates the RS256 key pair used to sign (API) and verify
# (Gateway) stream tickets, if it is not already in ./keys. No host tools
# required — openssl runs inside the container.
# required, since openssl runs inside the container.
keys-init:
image: alpine:3.20
container_name: musify-keys-init
@@ -203,30 +115,27 @@ services:
dockerfile: Musify.Migrator.Dockerfile
container_name: musify-migrate
depends_on:
postgres:
condition: service_healthy
wait-for-postgres:
condition: service_completed_successfully
environment:
Database__ConnectionString: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
Database__ConnectionString: "Host=${POSTGRES_HOST};Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
restart: "no"
# One-shot: creates the Musify project and its two OIDC apps in Zitadel (or
# reuses them if they already exist), then writes AUTH_CLIENT_ID,
# WEB_CLIENT_ID and WEB_CLIENT_SECRET back into the env file mounted at
# /workspace/.env (see compose.dev.yml / compose.prod.yml) — so a later
# `--profile apps up` picks up real values instead of empty ones.
# One-shot: creates the Musify project and its two OIDC apps in the shared
# Zitadel (or reuses them if they already exist), then writes
# AUTH_CLIENT_ID, WEB_CLIENT_ID and WEB_CLIENT_SECRET back into the env file
# mounted at /workspace/.env (see compose.dev.yml / compose.prod.yml), so a
# later `--profile apps up` picks up real values instead of empty ones.
zitadel-init:
image: alpine:3.20
container_name: musify-zitadel-init
depends_on:
zitadel:
condition: service_started
environment:
PUBLIC_AUTH_URL: ${PUBLIC_AUTH_URL}
PUBLIC_API_URL: ${PUBLIC_API_URL}
PUBLIC_WEB_URL: ${PUBLIC_WEB_URL}
ZITADEL_DEV_MODE: ${ZITADEL_DEV_MODE:-false}
volumes:
- ./zitadel/.output:/output:ro
- ${ZITADEL_ADMIN_PAT_DIR}:/output:ro
entrypoint: ["/bin/sh", "-c"]
command:
- |
@@ -240,7 +149,7 @@ services:
fi
# The env file may have CRLF line endings (edited on Windows), so
# strip \r on read and drop it on write — otherwise a "blank" value
# strip \r on read and drop it on write, otherwise a "blank" value
# is actually a lone \r, and `[ -z ... ]` lies.
env_get() { grep -E "^$$2=" "$$1" 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '\r'; }
env_set() {
@@ -257,7 +166,7 @@ services:
echo "==> waiting for the Zitadel service-account PAT"
deadline=$$(( $$(date +%s) + 180 ))
while [ ! -f /output/admin-sa.pat ]; do
[ "$$(date +%s)" -lt "$$deadline" ] || { echo "PAT not found, check 'docker logs musify-zitadel'" >&2; exit 1; }
[ "$$(date +%s)" -lt "$$deadline" ] || { echo "PAT not found under /output. Is Infrastructure's Zitadel running? Check 'docker logs infra-zitadel'." >&2; exit 1; }
sleep 3
done
PAT=$$(tr -d '\r\n' </output/admin-sa.pat)
@@ -387,14 +296,6 @@ services:
echo "==> wired into $$ENV_FILE"
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:
@@ -404,7 +305,6 @@ services:
dockerfile: Musify.Api.Dockerfile
container_name: musify-api
depends_on:
<<: *app-depends
migrate:
condition: service_completed_successfully
keys-init:
@@ -439,7 +339,6 @@ services:
dockerfile: Musify.Worker.Dockerfile
container_name: musify-worker
depends_on:
<<: *app-depends
migrate:
condition: service_completed_successfully
environment:
@@ -458,15 +357,13 @@ services:
dockerfile: Musify.StreamingGateway.Dockerfile
container_name: musify-gateway
depends_on:
seaweedfs:
condition: service_healthy
keys-init:
condition: service_completed_successfully
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__Clusters__seaweed-filer__Destinations__filer__Address: "http://${SEAWEEDFS_HOST}:8888/"
ReverseProxy__Routes__media__Transforms__0__PathPattern: "/buckets/${S3_BUCKET}/{**rest}"
Cors__AllowedOrigins__0: ${PUBLIC_WEB_URL}
volumes:
@@ -495,10 +392,5 @@ services:
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
S3_UPLOAD_ENDPOINT: ${S3_INTERNAL_URL}
restart: unless-stopped
volumes:
postgres_data:
rabbitmq_data:
seaweedfs_data: