From c35881f4719b01385687ba470da513530f6d875d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 21 Sep 2026 21:11:26 +0200 Subject: [PATCH] Add shared infrastructure base stack Postgres, Zitadel and SeaweedFS were previously duplicated or fragilely cross-referenced across the Musify, Ping and docker-manager repos. This consolidates them into one place: compose.yml is the production shape (no published ports, joins infra-net), compose.dev.yml is a thin overlay that publishes ports for local development. --- .env.example | 36 +++++ .env.prod.example | 32 +++++ .gitignore | 12 ++ compose.dev.yml | 54 +++++++ compose.yml | 160 +++++++++++++++++++++ postgres-init/010-create-app-databases.sql | 9 ++ 6 files changed, 303 insertions(+) create mode 100644 .env.example create mode 100644 .env.prod.example create mode 100644 .gitignore create mode 100644 compose.dev.yml create mode 100644 compose.yml create mode 100644 postgres-init/010-create-app-databases.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f059b1c --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Development configuration for compose.yml + compose.dev.yml. +# Copy to .env and adjust. +# +# cp .env.example .env + +# --- PostgreSQL (shared instance, one database per project) ----------------- +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_HOST_PORT=5432 + +# --- Zitadel (OIDC) ---------------------------------------------------------- +# The masterkey MUST be exactly 32 characters. host.docker.internal resolves +# to the same address from the browser, from apps on the host and from +# inside every project's containers, which is what makes the OIDC issuer +# valid everywhere without a shared Docker network in dev. +ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters +ZITADEL_EXTERNAL_DOMAIN=host.docker.internal +ZITADEL_EXTERNAL_PORT=8080 +ZITADEL_EXTERNAL_SECURE=false +ZITADEL_DATABASE_PASSWORD=zitadel +# Console login: @zitadel. +ZITADEL_ADMIN_USERNAME=admin +ZITADEL_ADMIN_PASSWORD=Password1! + +# --- SeaweedFS (S3) ---------------------------------------------------------- +S3_ACCESS_KEY=admin_access_key +S3_SECRET_KEY=admin_secret_key + +# --- RabbitMQ ----------------------------------------------------------------- +RABBITMQ_DEFAULT_USER=guest +RABBITMQ_DEFAULT_PASS=guest + +# --- pgAdmin (optional, `--profile tools`) ----------------------------------- +PGADMIN_DEFAULT_EMAIL=admin@infra.dev +PGADMIN_DEFAULT_PASSWORD=admin +PGADMIN_HOST_PORT=5050 diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..6cddafe --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,32 @@ +# Production configuration for compose.yml (no overlay needed — the base +# file is already the production shape: no published ports, everything +# reachable only on infra-net). +# +# cp .env.prod.example .env.prod +# chmod 600 .env.prod +# docker compose --env-file .env.prod -f compose.yml up -d +# +# Replace every CHANGE_ME with a strong random value (`openssl rand -hex 24`; +# the Zitadel masterkey needs exactly 32 characters) and auth.example.com +# with the real hostname the edge (e.g. Musify's nginx) terminates TLS for. + +# --- PostgreSQL --------------------------------------------------------------- +POSTGRES_USER=postgres +POSTGRES_PASSWORD=CHANGE_ME + +# --- Zitadel (OIDC) ---------------------------------------------------------- +ZITADEL_MASTERKEY=CHANGE_ME_32_CHARACTERS_EXACTLY_ +ZITADEL_EXTERNAL_DOMAIN=auth.example.com +ZITADEL_EXTERNAL_PORT=443 +ZITADEL_EXTERNAL_SECURE=true +ZITADEL_DATABASE_PASSWORD=CHANGE_ME +ZITADEL_ADMIN_USERNAME=admin +ZITADEL_ADMIN_PASSWORD=CHANGE_ME + +# --- SeaweedFS (S3) ---------------------------------------------------------- +S3_ACCESS_KEY=CHANGE_ME +S3_SECRET_KEY=CHANGE_ME + +# --- RabbitMQ ----------------------------------------------------------------- +RABBITMQ_DEFAULT_USER=CHANGE_ME +RABBITMQ_DEFAULT_PASS=CHANGE_ME diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34c9a37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Environment files (.env, .env.prod) +.env +.env.* +!.env.example +!.env.prod.example + +# Runtime state written by the containers themselves +zitadel/.output/ + +# OS +Thumbs.db +.DS_Store diff --git a/compose.dev.yml b/compose.dev.yml new file mode 100644 index 0000000..77d2974 --- /dev/null +++ b/compose.dev.yml @@ -0,0 +1,54 @@ +name: infra-dev + +# Development overlay: publishes every service's port to the host, so any +# project's dev stack can reach them at host.docker.internal: instead +# of joining infra-net. Adds pgAdmin. +# +# docker compose -f compose.yml -f compose.dev.yml up -d + +services: + postgres: + ports: + - "${POSTGRES_HOST_PORT}:5432" + + zitadel: + ports: + - "${ZITADEL_EXTERNAL_PORT}:8080" + + seaweedfs: + ports: + - "9333:9333" # master / admin UI + - "8888:8888" # filer + - "8333:8333" # S3 API + + rabbitmq: + ports: + - "5672:5672" # AMQP + - "15672:15672" # management UI + + jaeger: + ports: + - "16686:16686" # UI + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + + # Optional Postgres UI: docker compose ... --profile tools up -d + pgadmin: + profiles: [tools] + image: dpage/pgadmin4:latest + container_name: infra-pgadmin + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} + PGADMIN_CONFIG_SERVER_MODE: "False" + depends_on: + postgres: + condition: service_healthy + ports: + - "${PGADMIN_HOST_PORT}:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + restart: unless-stopped + +volumes: + pgadmin_data: diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..5f308fe --- /dev/null +++ b/compose.yml @@ -0,0 +1,160 @@ +# Shared infrastructure — identity, storage, database, messaging and tracing +# for every project under Documentos/ (Musify, Ping, docker-manager, ...). +# +# This file is the production shape: no published ports, no domains. Every +# service is reachable only on the `infra-net` network. Development reaches +# it through compose.dev.yml, which publishes the ports to the host instead +# of joining a network — see README.md for why. +# +# dev docker compose -f compose.yml -f compose.dev.yml up -d +# prod docker compose --env-file .env.prod -f compose.yml -f compose.prod.yml up -d +# +# A consuming project's own deploy stack does NOT get its own copy of any of +# these services. It only needs this stack to already be up, and its own +# one-shot bootstrap jobs (zitadel-init, seaweedfs-init) to create its own +# OIDC apps / bucket against the shared instances below. + +networks: + infra-net: + name: infra-net + +services: + # --- Database --------------------------------------------------------- + # One instance, one database per consuming project (see postgres-init/). + # Zitadel provisions its own `zitadel` database itself, from the admin + # credentials below — it does not need an init script. + postgres: + image: postgres:17-alpine + container_name: infra-postgres + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres-init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 10 + networks: [infra-net] + restart: unless-stopped + + # --- Identity ----------------------------------------------------------- + # OIDC provider shared by every project. `start-from-init` runs init + + # setup (both idempotent) and then starts, so the same command works on a + # cold and on a warm volume. + zitadel: + image: ghcr.io/zitadel/zitadel:latest + container_name: infra-zitadel + command: start-from-init --masterkeyFromEnv --tlsMode disabled + environment: + ZITADEL_MASTERKEY: ${ZITADEL_MASTERKEY} + # The external domain/port form the token issuer and must be identical + # for the browser, every project's 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: @zitadel. + 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; each consuming project's own + # zitadel-init one-shot uses it to create its own project/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 + networks: [infra-net] + restart: unless-stopped + + # --- Storage -------------------------------------------------------------- + # S3-compatible object store shared by every project. All-in-one: 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 consuming projects. No bucket is created here — each + # consuming project's own seaweedfs-init one-shot creates its own. + seaweedfs: + image: chrislusf/seaweedfs:latest + container_name: infra-seaweedfs + entrypoint: ["/bin/sh", "-c"] + command: + - | + cat > /tmp/s3.conf <<'JSON' + { + "identities": [ + { + "name": "infra", + "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 + networks: [infra-net] + restart: unless-stopped + + # --- Messaging -------------------------------------------------------- + # Broker shared by every project. Each project's own consumers/exchanges + # are project-specific; only the broker itself lives here. + rabbitmq: + image: rabbitmq:4-management-alpine + container_name: infra-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 + networks: [infra-net] + restart: unless-stopped + + # --- Tracing -------------------------------------------------------------- + # OTLP collector + trace UI, shared by any project's OpenTelemetry exporter. + jaeger: + image: jaegertracing/all-in-one:latest + container_name: infra-jaeger + environment: + COLLECTOR_OTLP_ENABLED: "true" + networks: [infra-net] + restart: unless-stopped + +volumes: + postgres_data: + seaweedfs_data: + rabbitmq_data: diff --git a/postgres-init/010-create-app-databases.sql b/postgres-init/010-create-app-databases.sql new file mode 100644 index 0000000..14a872f --- /dev/null +++ b/postgres-init/010-create-app-databases.sql @@ -0,0 +1,9 @@ +-- Runs once, only on first init of the postgres_data volume. +-- Zitadel provisions its own "zitadel" database itself, from the +-- ZITADEL_DATABASE_POSTGRES_ADMIN_* credentials in compose.yml — it does not +-- need to be created here. This script only creates the plain application +-- databases each project's own connection string points at. + +CREATE DATABASE musify_db; +CREATE DATABASE ping_db; +CREATE DATABASE dockermanager_db;