Its OIDC login isn't implemented yet, but it's meant to use the same Zitadel as Musify and Ping. Provision its Zitadel project and service account now, so its future zitadel-init only has to create its apps. Document that every project shares one Zitadel organization: one account per person across all projects, with single sign-on between them.
301 lines
13 KiB
YAML
301 lines
13 KiB
YAML
# Shared infrastructure: identity, storage, database, messaging and tracing
|
|
# for every project (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 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.
|
|
|
|
# Docker's default json-file driver never rotates, so logs would grow until
|
|
# the disk fills up.
|
|
x-logging: &default-logging
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
|
|
# The consuming projects that get their own credentials (see provision/).
|
|
# Adding one: add it here, give it the <PROJECT>_* settings in both
|
|
# `provision` and `seaweedfs` below, and their values in the env files.
|
|
x-projects: &projects musify ping dockermanager
|
|
|
|
networks:
|
|
infra-net:
|
|
name: infra-net
|
|
|
|
services:
|
|
# --- Database ---------------------------------------------------------
|
|
# One instance, one database and one role per consuming project (created
|
|
# by `provision` below). Zitadel creates its own `zitadel` database and
|
|
# user itself, from the admin credentials. The superuser is only for this
|
|
# stack: no project gets it.
|
|
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
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks: [infra-net]
|
|
logging: *default-logging
|
|
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:
|
|
# Pinned together with zitadel-login below: bump both at once.
|
|
image: ghcr.io/zitadel/zitadel:v4.19.1
|
|
container_name: infra-zitadel
|
|
# The image runs as uid 1000, but Docker creates both the bind mount
|
|
# (on Linux) and the bootstrap volume as root, so Zitadel couldn't write
|
|
# the PATs into them. Same as Zitadel's own compose.
|
|
user: "0"
|
|
# `disabled` (dev, plain http) makes Zitadel advertise http URLs; behind a
|
|
# TLS-terminating proxy it must be `external` or the console calls
|
|
# http://<domain> and the CSP blocks it.
|
|
command: start-from-init --masterkeyFromEnv --tlsMode ${ZITADEL_TLS_MODE:-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
|
|
# First organization and its console admin. Only read when the instance
|
|
# is created; changing them later doesn't rename anything that exists.
|
|
# A username without "@" gets `@<org domain>` appended; one with "@" is
|
|
# kept as is (e.g. admin@example.com).
|
|
ZITADEL_FIRSTINSTANCE_ORG_NAME: ${ZITADEL_ORG_NAME:-ZITADEL}
|
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: ${ZITADEL_ADMIN_USERNAME}
|
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_FIRSTNAME: ${ZITADEL_ADMIN_FIRST_NAME:-ZITADEL}
|
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_LASTNAME: ${ZITADEL_ADMIN_LAST_NAME:-Admin}
|
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_DISPLAYNAME: ${ZITADEL_ADMIN_DISPLAY_NAME:-ZITADEL Admin}
|
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_ADMIN_PASSWORD}
|
|
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false"
|
|
# Instance-admin service account + PAT written on init. Only `provision`
|
|
# uses it, to give each project its own, project-scoped account.
|
|
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
|
|
# Service account the login UI (zitadel-login) calls the API with. Its
|
|
# PAT goes to a volume only that container mounts, not to /output.
|
|
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
|
|
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized IAM_LOGIN_CLIENT
|
|
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: "2099-01-01T00:00:00Z"
|
|
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /bootstrap/login-client.pat
|
|
# Login V2: sign-in pages are served by zitadel-login, not by Zitadel
|
|
# itself. Like the FIRSTINSTANCE values, these are only read when the
|
|
# instance is created (see README for an existing instance).
|
|
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true"
|
|
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: ${ZITADEL_LOGIN_BASE_URL}/
|
|
ZITADEL_OIDC_DEFAULTLOGINURLV2: ${ZITADEL_LOGIN_BASE_URL}/login?authRequest=
|
|
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: ${ZITADEL_LOGIN_BASE_URL}/logout?post_logout_redirect=
|
|
ZITADEL_SAML_DEFAULTLOGINURLV2: ${ZITADEL_LOGIN_BASE_URL}/login?samlRequest=
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
volumes:
|
|
- ./zitadel/.output:/output
|
|
- zitadel_bootstrap:/bootstrap
|
|
healthcheck:
|
|
test: ["CMD", "/app/zitadel", "ready"]
|
|
interval: 10s
|
|
timeout: 30s
|
|
retries: 12
|
|
start_period: 20s
|
|
networks: [infra-net]
|
|
logging: *default-logging
|
|
restart: unless-stopped
|
|
|
|
# Login V2 UI (Next.js), served under /ui/v2/login. It calls the API
|
|
# directly on infra-net, so it has to present the public Host/scheme for
|
|
# Zitadel to find the instance.
|
|
zitadel-login:
|
|
image: ghcr.io/zitadel/zitadel-login:v4.19.1
|
|
container_name: infra-zitadel-login
|
|
environment:
|
|
ZITADEL_API_URL: http://zitadel:8080
|
|
NEXT_PUBLIC_BASE_PATH: /ui/v2/login
|
|
ZITADEL_SERVICE_USER_TOKEN_FILE: /bootstrap/login-client.pat
|
|
CUSTOM_REQUEST_HEADERS: Host:${ZITADEL_EXTERNAL_DOMAIN},X-Forwarded-Proto:${ZITADEL_PUBLIC_SCHEME}
|
|
healthcheck:
|
|
test: ["CMD", "/bin/sh", "-c", "node /app/healthcheck.mjs http://localhost:3000/ui/v2/login/healthy"]
|
|
interval: 10s
|
|
timeout: 30s
|
|
retries: 12
|
|
start_period: 20s
|
|
depends_on:
|
|
zitadel:
|
|
condition: service_healthy
|
|
volumes:
|
|
- zitadel_bootstrap:/bootstrap:ro
|
|
networks: [infra-net]
|
|
logging: *default-logging
|
|
restart: unless-stopped
|
|
|
|
# --- Storage --------------------------------------------------------------
|
|
# S3-compatible object store shared by every project. All-in-one: master +
|
|
# volume + filer + S3 gateway. The S3 identities are rendered from the
|
|
# environment on start (provision/seaweedfs-s3-config.sh): an admin one
|
|
# for this stack, and one per project limited to that project's bucket.
|
|
# No bucket is created here; each consuming project's own seaweedfs-init
|
|
# one-shot creates its own.
|
|
seaweedfs:
|
|
image: chrislusf/seaweedfs:4.47
|
|
container_name: infra-seaweedfs
|
|
environment:
|
|
PROJECTS: *projects
|
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
|
MUSIFY_S3_ACCESS_KEY: ${MUSIFY_S3_ACCESS_KEY:-}
|
|
MUSIFY_S3_SECRET_KEY: ${MUSIFY_S3_SECRET_KEY:-}
|
|
MUSIFY_S3_BUCKET: webapi-storage
|
|
PING_S3_ACCESS_KEY: ${PING_S3_ACCESS_KEY:-}
|
|
PING_S3_SECRET_KEY: ${PING_S3_SECRET_KEY:-}
|
|
PING_S3_BUCKET: ping-storage
|
|
DOCKERMANAGER_S3_ACCESS_KEY: ${DOCKERMANAGER_S3_ACCESS_KEY:-}
|
|
DOCKERMANAGER_S3_SECRET_KEY: ${DOCKERMANAGER_S3_SECRET_KEY:-}
|
|
DOCKERMANAGER_S3_BUCKET: dockermanager
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command:
|
|
- |
|
|
sh /provision/seaweedfs-s3-config.sh >/tmp/s3.conf || exit 1
|
|
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
|
|
- ./provision:/provision:ro
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks: [infra-net]
|
|
logging: *default-logging
|
|
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
|
|
# The node is named rabbit@<hostname> and its data lives under that name.
|
|
# Without a fixed hostname every recreated container gets a new one and
|
|
# starts on an empty data directory (users, vhosts, queues all gone).
|
|
hostname: 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]
|
|
logging: *default-logging
|
|
restart: unless-stopped
|
|
|
|
# --- Tracing --------------------------------------------------------------
|
|
# OTLP collector + trace UI, shared by any project's OpenTelemetry exporter.
|
|
# Last Jaeger v1 release (v1 is deprecated; v2 is jaegertracing/jaeger).
|
|
jaeger:
|
|
image: jaegertracing/all-in-one:1.76.0
|
|
container_name: infra-jaeger
|
|
environment:
|
|
COLLECTOR_OTLP_ENABLED: "true"
|
|
# Traces are kept in memory; without a cap they grow until the
|
|
# container runs out of RAM. The oldest are dropped first.
|
|
MEMORY_MAX_TRACES: "100000"
|
|
healthcheck:
|
|
# Admin port: answers 200 once the collector and query service are up.
|
|
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:14269/"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks: [infra-net]
|
|
logging: *default-logging
|
|
restart: unless-stopped
|
|
|
|
# --- Per-project credentials ----------------------------------------------
|
|
# One-shot, runs on every `up` (idempotent): each project's own Postgres
|
|
# role/database, RabbitMQ user/vhost and Zitadel service account, from the
|
|
# <PROJECT>_* settings (see provision/provision.sh). An empty setting skips
|
|
# that service for that project.
|
|
provision:
|
|
image: postgres:17-alpine
|
|
container_name: infra-provision
|
|
entrypoint: ["/bin/sh", "/provision/provision.sh"]
|
|
environment:
|
|
PROJECTS: *projects
|
|
POSTGRES_USER: ${POSTGRES_USER}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
|
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
|
|
ZITADEL_EXTERNAL_DOMAIN: ${ZITADEL_EXTERNAL_DOMAIN}
|
|
ZITADEL_PUBLIC_SCHEME: ${ZITADEL_PUBLIC_SCHEME}
|
|
MUSIFY_DB_PASSWORD: ${MUSIFY_DB_PASSWORD:-}
|
|
MUSIFY_RABBITMQ_PASSWORD: ${MUSIFY_RABBITMQ_PASSWORD:-}
|
|
MUSIFY_ZITADEL_PROJECT: Musify
|
|
PING_DB_PASSWORD: ${PING_DB_PASSWORD:-}
|
|
PING_RABBITMQ_PASSWORD: ${PING_RABBITMQ_PASSWORD:-}
|
|
PING_ZITADEL_PROJECT: Ping
|
|
DOCKERMANAGER_DB_PASSWORD: ${DOCKERMANAGER_DB_PASSWORD:-}
|
|
DOCKERMANAGER_RABBITMQ_PASSWORD: ${DOCKERMANAGER_RABBITMQ_PASSWORD:-}
|
|
DOCKERMANAGER_ZITADEL_PROJECT: Docker Manager
|
|
volumes:
|
|
- ./provision:/provision:ro
|
|
- ./zitadel/.output:/output
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rabbitmq:
|
|
condition: service_healthy
|
|
zitadel:
|
|
condition: service_healthy
|
|
networks: [infra-net]
|
|
logging: *default-logging
|
|
restart: "no"
|
|
|
|
volumes:
|
|
postgres_data:
|
|
zitadel_bootstrap:
|
|
seaweedfs_data:
|
|
rabbitmq_data:
|