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).
- 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.
- Storage goes through an S3 compatible bucket, reusing the SeaweedFS instance from the Musify stack instead of running a second one.
- Zitadel for OIDC/OAuth2 login, also shared with Musify so a single account works across both apps.
- 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 through Infrastructure so a single account works across every project.
**Frontend**: React 19 with TypeScript, built with Vite.
- 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.
- 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
@@ -43,13 +46,22 @@ backend/deploy/ Compose files and env templates for local development
## 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
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
```
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.
# Copy to deploy/.env and adjust. Not versioned.
# Copy to deploy/.env and adjust.
#
# 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
# --- Public URLs -------------------------------------------------------------
PUBLIC_WEB_URL=http://localhost:3100
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
# --- 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_PASSWORD=postgres
POSTGRES_DB=ping_db
POSTGRES_HOST_PORT=59010
# --- Zitadel (shared with Musify) --------------------------------------------
# zitadel-init needs Musify's admin service-account PAT to create Ping's own project/apps
# in that same Zitadel instance. Point this at Musify's deploy/zitadel/.output folder —
# adjust if your Musify checkout lives somewhere else relative to this one.
ZITADEL_ADMIN_PAT_DIR=../../../Musify/MusifyM/deploy/zitadel/.output
# --- Zitadel (OIDC, shared instance) -----------------------------------------
# Path to the admin service-account PAT Infrastructure's Zitadel writes on
# init. Adjust if your Infrastructure checkout lives somewhere else relative
# to this one.
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
ZITADEL_DEV_MODE=true
# 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=
WEB_CLIENT_ID=
# --- SeaweedFS (S3, shared with Musify) --------------------------------------
# Same SeaweedFS instance as Musify, so these must be the REAL values from Musify's own
# deploy/.env (S3_ACCESS_KEY / S3_SECRET_KEY) — not the placeholders below.
# --- SeaweedFS (S3, shared instance, Ping's own bucket) ----------------------
# S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env.
S3_ACCESS_KEY=admin_access_key
S3_SECRET_KEY=admin_secret_key
S3_BUCKET=ping-storage
@@ -41,9 +43,7 @@ S3_PUBLIC_URL=http://host.docker.internal:8333
S3_USE_HTTP=true
# --- LiveKit (voice/video SFU) ------------------------------------------------
# Overrides --dev's built-in "devkey/secret" pair (see compose.yml's LIVEKIT_KEYS): the
# 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)
# --dev mode uses this fixed dev key pair; fine for local development only.
LIVEKIT_API_KEY=devkey
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
ones. Musify's stack must be up first.
Reuses the Zitadel and SeaweedFS instances already running in the separate
`Infrastructure` repository, instead of starting new ones. That stack must be
up first (see its own README for the exact command).
```bash
cp deploy/.env.example deploy/.env
# edit deploy/.env:
# - ZITADEL_ADMIN_PAT_DIR must point at Musify's deploy/zitadel/.output folder
# - S3_ACCESS_KEY / S3_SECRET_KEY must match the REAL values in Musify's own deploy/.env
# edit deploy/.env if your Infrastructure checkout isn't where
# ZITADEL_ADMIN_PAT_DIR assumes, or if its S3_ACCESS_KEY/SECRET_KEY differ
# from the defaults
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
project/apps for Ping, the `ping-storage` S3 bucket) — the usual dev loop runs the API and
frontend from the IDE against these.
This starts LiveKit and the one-shot jobs (waiting for Postgres, EF Core
migrations, the Zitadel project for Ping, the `ping-storage` S3 bucket). The
usual dev loop runs the API and frontend from the IDE against these.
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
```
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
TCP) / `50000-50100/udp` (media). Zitadel (`8080`) and the SeaweedFS S3 API (`8333`) come from
Musify's own stack.
Ports (dev): API `5211`, web `3100`, LiveKit `7880` (signaling), `7881` (RTC
TCP) and `50000-50100/udp` (media). Postgres, Zitadel and the SeaweedFS S3 API
come from Infrastructure's own stack. See its README for that port table.
+7 -5
View File
@@ -1,6 +1,6 @@
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.
#
# 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"
services:
postgres:
ports:
- "${POSTGRES_HOST_PORT}:5432"
livekit:
ports:
- "7880:7880" # HTTP / WebSocket signaling
- "7881:7881" # RTC TCP fallback
- "50000-50100:50000-50100/udp" # WebRTC media
wait-for-postgres:
extra_hosts: *host-alias
zitadel-init:
extra_hosts: *host-alias
volumes:
@@ -28,6 +27,9 @@ services:
seaweedfs-init:
extra_hosts: *host-alias
migrate:
extra_hosts: *host-alias
api:
ports:
- "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
# already running for Musify (see deploy/.env.example — ZITADEL_ADMIN_PAT_DIR / S3_*).
# Bring Musify's stack up first, then this one.
# Zitadel and SeaweedFS are NOT run here: Ping reuses the shared instances in
# ../../../Infrastructure (see .env.example, ZITADEL_ADMIN_PAT_DIR / S3_*).
# Bring that stack up first.
#
# 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
# Postgres, LiveKit and the one-shot bootstrap jobs (migrations, Zitadel project/apps,
# SeaweedFS bucket) 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.
x-app-depends: &app-depends
postgres:
condition: service_healthy
# The `api` and `web` services sit behind the `apps` profile, so a plain `up`
# starts LiveKit and the one-shot bootstrap jobs (Postgres wait, migrations,
# Zitadel project/apps, SeaweedFS 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.
x-backend-env: &backend-env
Database__ConnectionString: "Host=postgres;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
# presigned upload/download URLs the browser has to be able to reach.
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 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}
@@ -30,22 +27,6 @@ x-backend-env: &backend-env
services:
# --- 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
# 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.
@@ -56,18 +37,36 @@ services:
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
# 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
# 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
# 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
restart: unless-stopped
# One-shot: creates the Ping project and its two OIDC apps in Musify's Zitadel instance
# (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
# Waits for Infrastructure's Postgres to accept connections before `migrate`
# runs. A cross-compose-project health check isn't possible, since
# Postgres is no longer part of this compose file.
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.
zitadel-init:
image: alpine:3.20
@@ -101,7 +100,7 @@ services:
}
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
fi
PAT=$$(tr -d '\r\n' </zitadel-output/admin-sa.pat)
@@ -160,7 +159,7 @@ services:
echo " created (clientId $$API_CLIENT_ID)"
fi
echo "==> web app (React SPA — public client, PKCE)"
echo "==> web app (React SPA, public client, PKCE)"
WEB_APP_NAME="Ping Web"
WEB_APP_ID=$$(find_app "$$WEB_APP_NAME")
if [ -n "$$WEB_APP_ID" ]; then
@@ -190,8 +189,9 @@ services:
echo "==> wired into $$ENV_FILE"
restart: "no"
# One-shot: SeaweedFS does not auto-create buckets. Reuses Musify's SeaweedFS instance
# (same S3 endpoint/credentials) but creates Ping's own, separate bucket.
# One-shot: SeaweedFS does not auto-create buckets. Reuses the shared
# SeaweedFS instance (same S3 endpoint/credentials) but creates Ping's own,
# separate bucket. `aws s3 ls` doubles as the readiness probe.
seaweedfs-init:
image: amazon/aws-cli:latest
container_name: ping-seaweedfs-init
@@ -202,6 +202,12 @@ services:
entrypoint: ["/bin/sh", "-c"]
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 ls
restart: "no"
@@ -213,10 +219,10 @@ services:
dockerfile: Ping.Migrator.Dockerfile
container_name: ping-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"
# --- Applications (profile: apps) -----------------------------------------
@@ -228,7 +234,6 @@ services:
dockerfile: Ping.Api.Dockerfile
container_name: ping-api
depends_on:
<<: *app-depends
migrate:
condition: service_completed_successfully
environment:
@@ -266,6 +271,3 @@ services:
depends_on:
- api
restart: unless-stopped
volumes:
postgres_data: