Point at the shared Infrastructure repo instead of Musify's folder

Ping already reused Musify's Zitadel and SeaweedFS, but did so through a
hardcoded relative path into Musify's own deploy folder
(../../../Musify/MusifyM/deploy/zitadel/.output), which only worked
because Musify happened to be the one running them. Those services now
live in a separate Infrastructure repository instead, so this points
there.

Also drops Ping's own Postgres container in favor of Infrastructure's
shared instance (its own ping_db database), with a wait-for-postgres
one-shot replacing the in-file health check.
This commit is contained in:
2026-09-21 22:09:22 +02:00
parent 6601ca80ca
commit f043ef3b85
5 changed files with 108 additions and 90 deletions
+18 -6
View File
@@ -21,8 +21,8 @@ A Discord style chat app. Servers with text and voice channels, direct messages,
- SignalR for chat, presence and call signaling (ringing, accepting, hanging up). - SignalR for chat, presence and call signaling (ringing, accepting, hanging up).
- LiveKit's server SDK issues the access tokens clients use to join a voice/video room. - LiveKit's server SDK issues the access tokens clients use to join a voice/video room.
- FluentValidation on the request DTOs, wired in through a minimal API filter. - FluentValidation on the request DTOs, wired in through a minimal API filter.
- Storage goes through an S3 compatible bucket, reusing the SeaweedFS instance from the Musify stack instead of running a second one. - Storage goes through an S3 compatible bucket, reusing the shared SeaweedFS instance from the Infrastructure stack instead of running a second one.
- Zitadel for OIDC/OAuth2 login, also shared with Musify so a single account works across both apps. - Zitadel for OIDC/OAuth2 login, also shared through Infrastructure so a single account works across every project.
**Frontend**: React 19 with TypeScript, built with Vite. **Frontend**: React 19 with TypeScript, built with Vite.
- Zustand for the small bits of client state: current view, active call, toasts, presence. - Zustand for the small bits of client state: current view, active call, toasts, presence.
@@ -31,7 +31,10 @@ A Discord style chat app. Servers with text and voice channels, direct messages,
- `@livekit/components-react` plus `livekit-client` for the actual call UI and media handling. - `@livekit/components-react` plus `livekit-client` for the actual call UI and media handling.
- Plain CSS, no component library. - Plain CSS, no component library.
**Infrastructure**: Docker Compose for local development. Postgres and LiveKit run in containers; Zitadel and the S3 storage are the ones already running for Musify, so this stack only adds what's specific to Ping. **Infrastructure**: Docker Compose for local development. LiveKit runs in a
container here; Postgres, Zitadel and the S3 storage are shared services
provided by the separate `Infrastructure` git repository, used by other
projects too, so this stack only adds what's specific to Ping.
## Project layout ## Project layout
@@ -43,13 +46,22 @@ backend/deploy/ Compose files and env templates for local development
## Running it locally ## Running it locally
The backend expects Musify's Zitadel and SeaweedFS containers to already be running, since it reuses them instead of standing up its own copy. With that in place: The backend expects the shared `Infrastructure` stack to already be running
(see its own README for the exact command), since it reuses its
Postgres/Zitadel/SeaweedFS instead of standing up its own copies. With that
in place:
```bash ```bash
cp backend/deploy/.env.example backend/deploy/.env cp backend/deploy/.env.example backend/deploy/.env
# edit backend/deploy/.env to point at your Musify checkout and fill in the real S3 keys # edit backend/deploy/.env if your Infrastructure checkout isn't where
# ZITADEL_ADMIN_PAT_DIR assumes, or its S3 keys differ from the defaults
docker compose -f backend/deploy/compose.yml -f backend/deploy/compose.dev.yml up -d docker compose -f backend/deploy/compose.yml -f backend/deploy/compose.dev.yml up -d
``` ```
That starts Postgres, LiveKit and the one-shot setup jobs (migrations, the Zitadel project for Ping, the storage bucket). From there, run the API and the frontend from your IDE or with `dotnet run` / `npm run dev` against them. See [backend/deploy/README.md](backend/deploy/README.md) for the full details, including how to also build and run the API and frontend inside Docker. That starts LiveKit and the one-shot setup jobs (waiting for Postgres,
migrations, the Zitadel project for Ping, the storage bucket). From there,
run the API and the frontend from your IDE or with `dotnet run` / `npm run
dev` against them. See [backend/deploy/README.md](backend/deploy/README.md)
for the full details, including how to also build and run the API and
frontend inside Docker.
+17 -17
View File
@@ -1,29 +1,32 @@
# Development configuration for deploy/compose.yml + compose.dev.yml. # Development configuration for deploy/compose.yml + compose.dev.yml.
# Copy to deploy/.env and adjust. Not versioned. # Copy to deploy/.env and adjust.
# #
# cp deploy/.env.example deploy/.env # cp deploy/.env.example deploy/.env
#
# Requires ../../../Infrastructure running first (Postgres, Zitadel,
# SeaweedFS). See ../../../Infrastructure/README.md. Everything marked
# "must match Infrastructure's .env" below has to be the same value as the
# one in that file.
DOTNET_ENVIRONMENT=Development DOTNET_ENVIRONMENT=Development
# --- Public URLs ------------------------------------------------------------- # --- Public URLs -------------------------------------------------------------
PUBLIC_WEB_URL=http://localhost:3100 PUBLIC_WEB_URL=http://localhost:3100
PUBLIC_API_URL=http://localhost:5211 PUBLIC_API_URL=http://localhost:5211
# Ping reuses the Zitadel and SeaweedFS instances already running for Musify instead of
# spinning up its own, so this must match Musify's PUBLIC_AUTH_URL exactly.
PUBLIC_AUTH_URL=http://host.docker.internal:8080 PUBLIC_AUTH_URL=http://host.docker.internal:8080
# --- PostgreSQL (Ping's own database — Zitadel keeps using Musify's) -------- # --- PostgreSQL (Infrastructure's shared instance, Ping's own database) -----
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.
POSTGRES_HOST=host.docker.internal
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_DB=ping_db POSTGRES_DB=ping_db
POSTGRES_HOST_PORT=59010
# --- Zitadel (shared with Musify) -------------------------------------------- # --- Zitadel (OIDC, shared instance) -----------------------------------------
# zitadel-init needs Musify's admin service-account PAT to create Ping's own project/apps # Path to the admin service-account PAT Infrastructure's Zitadel writes on
# in that same Zitadel instance. Point this at Musify's deploy/zitadel/.output folder — # init. Adjust if your Infrastructure checkout lives somewhere else relative
# adjust if your Musify checkout lives somewhere else relative to this one. # to this one.
ZITADEL_ADMIN_PAT_DIR=../../../Musify/MusifyM/deploy/zitadel/.output ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
ZITADEL_DEV_MODE=true ZITADEL_DEV_MODE=true
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills these in # OIDC clients: the zitadel-init one-shot service (see compose.yml) fills these in
@@ -31,9 +34,8 @@ ZITADEL_DEV_MODE=true
AUTH_CLIENT_ID= AUTH_CLIENT_ID=
WEB_CLIENT_ID= WEB_CLIENT_ID=
# --- SeaweedFS (S3, shared with Musify) -------------------------------------- # --- SeaweedFS (S3, shared instance, Ping's own bucket) ----------------------
# Same SeaweedFS instance as Musify, so these must be the REAL values from Musify's own # S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env.
# deploy/.env (S3_ACCESS_KEY / S3_SECRET_KEY) — not the placeholders below.
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=ping-storage S3_BUCKET=ping-storage
@@ -41,9 +43,7 @@ S3_PUBLIC_URL=http://host.docker.internal:8333
S3_USE_HTTP=true S3_USE_HTTP=true
# --- LiveKit (voice/video SFU) ------------------------------------------------ # --- LiveKit (voice/video SFU) ------------------------------------------------
# Overrides --dev's built-in "devkey/secret" pair (see compose.yml's LIVEKIT_KEYS): the # --dev mode uses this fixed dev key pair; fine for local development only.
# LiveKit .NET server SDK rejects secrets shorter than 256 bits. Generate your own with:
# $b = [byte[]]::new(32); [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b)
LIVEKIT_API_KEY=devkey LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=9U1eDtZx9eQrSWern4UcWa3lYIrfvrQqExLlkUXpP+o= LIVEKIT_API_SECRET=9U1eDtZx9eQrSWern4UcWa3lYIrfvrQqExLlkUXpP+o=
+15 -13
View File
@@ -1,20 +1,21 @@
# Ping — deploy # Ping deploy
Reuses the Zitadel and SeaweedFS containers already running for Musify instead of starting new Reuses the Zitadel and SeaweedFS instances already running in the separate
ones. Musify's stack must be up first. `Infrastructure` repository, instead of starting new ones. That stack must be
up first (see its own README for the exact command).
```bash ```bash
cp deploy/.env.example deploy/.env cp deploy/.env.example deploy/.env
# edit deploy/.env: # edit deploy/.env if your Infrastructure checkout isn't where
# - ZITADEL_ADMIN_PAT_DIR must point at Musify's deploy/zitadel/.output folder # ZITADEL_ADMIN_PAT_DIR assumes, or if its S3_ACCESS_KEY/SECRET_KEY differ
# - S3_ACCESS_KEY / S3_SECRET_KEY must match the REAL values in Musify's own deploy/.env # from the defaults
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
``` ```
This starts Postgres, LiveKit, and the one-shot jobs (EF Core migrations, the Zitadel This starts LiveKit and the one-shot jobs (waiting for Postgres, EF Core
project/apps for Ping, the `ping-storage` S3 bucket) — the usual dev loop runs the API and migrations, the Zitadel project for Ping, the `ping-storage` S3 bucket). The
frontend from the IDE against these. usual dev loop runs the API and frontend from the IDE against these.
To also build and run the API/frontend in Docker instead: To also build and run the API/frontend in Docker instead:
@@ -22,8 +23,9 @@ To also build and run the API/frontend in Docker instead:
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml --profile apps up -d --build docker compose -f deploy/compose.yml -f deploy/compose.dev.yml --profile apps up -d --build
``` ```
By then `zitadel-init` has already written `AUTH_CLIENT_ID` / `WEB_CLIENT_ID` into `deploy/.env`. By then `zitadel-init` has already written `AUTH_CLIENT_ID` / `WEB_CLIENT_ID`
into `deploy/.env`.
Ports (dev): API `5211`, web `3100`, Postgres `59010`, LiveKit `7880` (signaling) / `7881` (RTC Ports (dev): API `5211`, web `3100`, LiveKit `7880` (signaling), `7881` (RTC
TCP) / `50000-50100/udp` (media). Zitadel (`8080`) and the SeaweedFS S3 API (`8333`) come from TCP) and `50000-50100/udp` (media). Postgres, Zitadel and the SeaweedFS S3 API
Musify's own stack. come from Infrastructure's own stack. See its README for that port table.
+7 -5
View File
@@ -1,6 +1,6 @@
name: ping-dev name: ping-dev
# Development overlay: publishes the infrastructure ports to the host so the .NET apps and # Development overlay: publishes the ports to the host so the .NET apps and
# the web frontend can run from the IDE. # the web frontend can run from the IDE.
# #
# docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d # docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
@@ -10,16 +10,15 @@ x-host-alias: &host-alias
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
services: services:
postgres:
ports:
- "${POSTGRES_HOST_PORT}:5432"
livekit: livekit:
ports: ports:
- "7880:7880" # HTTP / WebSocket signaling - "7880:7880" # HTTP / WebSocket signaling
- "7881:7881" # RTC TCP fallback - "7881:7881" # RTC TCP fallback
- "50000-50100:50000-50100/udp" # WebRTC media - "50000-50100:50000-50100/udp" # WebRTC media
wait-for-postgres:
extra_hosts: *host-alias
zitadel-init: zitadel-init:
extra_hosts: *host-alias extra_hosts: *host-alias
volumes: volumes:
@@ -28,6 +27,9 @@ services:
seaweedfs-init: seaweedfs-init:
extra_hosts: *host-alias extra_hosts: *host-alias
migrate:
extra_hosts: *host-alias
api: api:
ports: ports:
- "5211:5211" - "5211:5211"
+51 -49
View File
@@ -1,25 +1,22 @@
# Ping — base stack, shared by development and production. # Ping: base stack, shared by development and production.
# #
# This intentionally does NOT run Zitadel or SeaweedFS: Ping reuses the same instances # Zitadel and SeaweedFS are NOT run here: Ping reuses the shared instances in
# already running for Musify (see deploy/.env.example — ZITADEL_ADMIN_PAT_DIR / S3_*). # ../../../Infrastructure (see .env.example, ZITADEL_ADMIN_PAT_DIR / S3_*).
# Bring Musify's stack up first, then this one. # Bring that stack up first.
# #
# dev docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d # dev docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
# #
# The `api` and `web` services sit behind the `apps` profile, so a plain `up` starts # The `api` and `web` services sit behind the `apps` profile, so a plain `up`
# Postgres, LiveKit and the one-shot bootstrap jobs (migrations, Zitadel project/apps, # starts LiveKit and the one-shot bootstrap jobs (Postgres wait, migrations,
# SeaweedFS bucket) and stops there — the usual dev setup, with the apps running from the # Zitadel project/apps, SeaweedFS bucket) and stops there. That's the usual
# IDE. Add `--profile apps up -d --build` as a SECOND command to also build and run them in # dev setup, with the apps running from the IDE. Add `--profile apps up -d
# Docker: by then zitadel-init has already written the OIDC client ids into the env file. # --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.
x-app-depends: &app-depends
postgres:
condition: service_healthy
x-backend-env: &backend-env x-backend-env: &backend-env
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}"
# The public S3 URL, not the internal one: the same address is used to sign the # The public S3 URL, not the internal one: the same address is used to sign
# presigned upload/download URLs the browser has to be able to reach. # the presigned upload/download URLs the browser has to be able to reach.
InfrastructureStorage__Address: ${S3_PUBLIC_URL} InfrastructureStorage__Address: ${S3_PUBLIC_URL}
InfrastructureStorage__AccessKey: ${S3_ACCESS_KEY} InfrastructureStorage__AccessKey: ${S3_ACCESS_KEY}
InfrastructureStorage__SecretAccessKey: ${S3_SECRET_KEY} InfrastructureStorage__SecretAccessKey: ${S3_SECRET_KEY}
@@ -30,22 +27,6 @@ x-backend-env: &backend-env
services: services:
# --- Infrastructure ------------------------------------------------------- # --- Infrastructure -------------------------------------------------------
postgres:
image: postgres:17-alpine
container_name: ping-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
# Voice/video SFU. --dev keeps it single-node with in-memory state (fine for local # Voice/video SFU. --dev keeps it single-node with in-memory state (fine for local
# development), but the key pair is overridden via LIVEKIT_KEYS: the LiveKit .NET server SDK # development), but the key pair is overridden via LIVEKIT_KEYS: the LiveKit .NET server SDK
# rejects secrets shorter than 256 bits, and --dev's built-in "devkey/secret" pair is too short. # rejects secrets shorter than 256 bits, and --dev's built-in "devkey/secret" pair is too short.
@@ -56,18 +37,36 @@ services:
environment: environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}" LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
# Node IP is "automatically determined" by default, which inside a container means the # Node IP is "automatically determined" by default, which inside a container means the
# Docker bridge network's internal address (e.g. 172.x.x.x) — unreachable from the # Docker bridge network's internal address (e.g. 172.x.x.x), unreachable from the
# browser, so ICE/media never connects even though the WebSocket signaling (which goes # browser, so ICE/media never connects even though the WebSocket signaling (which goes
# through the published port, not this address) succeeds. Every public URL in this dev # through the published port, not this address) succeeds. Every public URL in this dev
# stack is already hardcoded to localhost (see PUBLIC_WEB_URL, VITE_LIVEKIT_URL), so the # stack is already hardcoded to localhost (see PUBLIC_WEB_URL, VITE_LIVEKIT_URL), so the
# browser and this container are always on the same machine — advertise that directly. # browser and this container are always on the same machine. Advertise that directly.
NODE_IP: 127.0.0.1 NODE_IP: 127.0.0.1
restart: unless-stopped restart: unless-stopped
# One-shot: creates the Ping project and its two OIDC apps in Musify's Zitadel instance # Waits for Infrastructure's Postgres to accept connections before `migrate`
# (or reuses them if they already exist), then writes AUTH_CLIENT_ID / WEB_CLIENT_ID back # runs. A cross-compose-project health check isn't possible, since
# into the env file mounted at /workspace/.env — so a later `--profile apps up` picks up # Postgres is no longer part of this compose file.
# real values instead of empty ones. Both apps are public clients (PKCE, no secret): the wait-for-postgres:
image: postgres:17-alpine
container_name: ping-wait-for-postgres
entrypoint: ["/bin/sh", "-c"]
command:
- |
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: creates the Ping 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 back into the env file mounted at
# /workspace/.env, so a later `--profile apps up` picks up real values
# instead of empty ones. Both apps are public clients (PKCE, no secret): the
# Scalar docs UI and the React SPA. # Scalar docs UI and the React SPA.
zitadel-init: zitadel-init:
image: alpine:3.20 image: alpine:3.20
@@ -101,7 +100,7 @@ services:
} }
if [ ! -f /zitadel-output/admin-sa.pat ]; then if [ ! -f /zitadel-output/admin-sa.pat ]; then
echo "admin-sa.pat not found under ZITADEL_ADMIN_PAT_DIR — is Musify's Zitadel running?" >&2 echo "admin-sa.pat not found under ZITADEL_ADMIN_PAT_DIR. Is Infrastructure's Zitadel running?" >&2
exit 1 exit 1
fi fi
PAT=$$(tr -d '\r\n' </zitadel-output/admin-sa.pat) PAT=$$(tr -d '\r\n' </zitadel-output/admin-sa.pat)
@@ -160,7 +159,7 @@ services:
echo " created (clientId $$API_CLIENT_ID)" echo " created (clientId $$API_CLIENT_ID)"
fi fi
echo "==> web app (React SPA — public client, PKCE)" echo "==> web app (React SPA, public client, PKCE)"
WEB_APP_NAME="Ping Web" WEB_APP_NAME="Ping Web"
WEB_APP_ID=$$(find_app "$$WEB_APP_NAME") WEB_APP_ID=$$(find_app "$$WEB_APP_NAME")
if [ -n "$$WEB_APP_ID" ]; then if [ -n "$$WEB_APP_ID" ]; then
@@ -190,8 +189,9 @@ services:
echo "==> wired into $$ENV_FILE" echo "==> wired into $$ENV_FILE"
restart: "no" restart: "no"
# One-shot: SeaweedFS does not auto-create buckets. Reuses Musify's SeaweedFS instance # One-shot: SeaweedFS does not auto-create buckets. Reuses the shared
# (same S3 endpoint/credentials) but creates Ping's own, separate bucket. # SeaweedFS instance (same S3 endpoint/credentials) but creates Ping's own,
# separate bucket. `aws s3 ls` doubles as the readiness probe.
seaweedfs-init: seaweedfs-init:
image: amazon/aws-cli:latest image: amazon/aws-cli:latest
container_name: ping-seaweedfs-init container_name: ping-seaweedfs-init
@@ -202,6 +202,12 @@ services:
entrypoint: ["/bin/sh", "-c"] entrypoint: ["/bin/sh", "-c"]
command: command:
- | - |
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 mb s3://${S3_BUCKET} 2>/dev/null || true
aws --endpoint-url ${S3_PUBLIC_URL} s3 ls aws --endpoint-url ${S3_PUBLIC_URL} s3 ls
restart: "no" restart: "no"
@@ -213,10 +219,10 @@ services:
dockerfile: Ping.Migrator.Dockerfile dockerfile: Ping.Migrator.Dockerfile
container_name: ping-migrate container_name: ping-migrate
depends_on: depends_on:
postgres: wait-for-postgres:
condition: service_healthy condition: service_completed_successfully
environment: 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" restart: "no"
# --- Applications (profile: apps) ----------------------------------------- # --- Applications (profile: apps) -----------------------------------------
@@ -228,7 +234,6 @@ services:
dockerfile: Ping.Api.Dockerfile dockerfile: Ping.Api.Dockerfile
container_name: ping-api container_name: ping-api
depends_on: depends_on:
<<: *app-depends
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
environment: environment:
@@ -266,6 +271,3 @@ services:
depends_on: depends_on:
- api - api
restart: unless-stopped restart: unless-stopped
volumes:
postgres_data: