From 6e6ff63f08d597ee166eec8fd060e86a59ffac58 Mon Sep 17 00:00:00 2001 From: Dotty Date: Sat, 28 Mar 2026 19:04:45 +0100 Subject: [PATCH] Add full stack configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.yml with all services - .env.example with placeholder secrets - Landing page HTML - Giteaβ†’OpenProject webhook script - Comprehensive README with architecture docs --- .env.example | 49 ++ .gitignore | 7 + README.md | 154 +++++ docker-compose.yml | 885 ++++++++++++++++++++++++++ landing/index.html | 232 +++++++ webhooks/gitea-openproject-webhook.py | 98 +++ 6 files changed, 1425 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 landing/index.html create mode 100755 webhooks/gitea-openproject-webhook.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9c6bbeb --- /dev/null +++ b/.env.example @@ -0,0 +1,49 @@ +# ============================================================================= +# Collaboration Stack - Production Environment +# ============================================================================= +# Copy to .env and fill in real values + +# Domain +DOMAIN=toppyr.de + +# Let's Encrypt +ACME_EMAIL=mail@example.com + +# Traefik Dashboard (htpasswd format: user:hash) +TRAEFIK_AUTH=admin:$apr1$... + +# Keycloak +KEYCLOAK_DB_PASSWORD= +KEYCLOAK_ADMIN_PASSWORD= + +# OIDC Client Secret (shared across services) +OIDC_CLIENT_SECRET= + +# OpenProject +OPENPROJECT_SECRET= +OPENPROJECT_ADMIN_PASSWORD= + +# XWiki +XWIKI_DB_ROOT_PASSWORD= +XWIKI_DB_PASSWORD= + +# Nextcloud +NEXTCLOUD_DB_PASSWORD= +NEXTCLOUD_ADMIN_PASSWORD= + +# EspoCRM +ESPOCRM_DB_PASSWORD= +ESPOCRM_ADMIN_PASSWORD= + +# Mautic +MAUTIC_DB_PASSWORD= + +# FreeScout +FREESCOUT_DB_PASSWORD= +FREESCOUT_ADMIN_PASSWORD= + +# Gitea +GITEA_DB_PASSWORD= + +# OpenProject API (for Gitea webhook) +OPENPROJECT_API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7922b4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Secrets - never commit! +.env + +# Local development +*.log +*.bak +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..3db6e58 --- /dev/null +++ b/README.md @@ -0,0 +1,154 @@ +# Toppyr Stack + +Docker Compose Stack fΓΌr **toppyr.de** β€” Self-Hosted Collaboration Suite. + +## πŸ—οΈ Architektur + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Traefik (Reverse Proxy) β”‚ +β”‚ Let's Encrypt SSL Termination β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Keycloak SSO β”‚ +β”‚ sso.toppyr.de β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ β”‚ β”‚ +β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β” +β”‚ Gitea β”‚ β”‚OpenProj.β”‚ β”‚ XWiki β”‚ β”‚ Postiz β”‚ β”‚ Monitoring β”‚ +β”‚ repo. β”‚ β”‚ project.β”‚ β”‚ wiki. β”‚ β”‚ postiz. β”‚ β”‚ status./netd. β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“¦ Services + +| Service | URL | Beschreibung | +|---------|-----|--------------| +| **Keycloak** | sso.toppyr.de | Single Sign-On, Identity Provider | +| **OpenProject** | project.toppyr.de | Projektmanagement, Tasks | +| **XWiki** | wiki.toppyr.de | Dokumentation, Wiki | +| **Gitea** | repo.toppyr.de | Git Repositories, CI/CD | +| **Postiz** | postiz.toppyr.de | Social Media Scheduler | +| **EspoCRM** | crm.toppyr.de | CRM System | +| **Mautic** | marketing.toppyr.de | Marketing Automation | +| **FreeScout** | support.toppyr.de | Helpdesk, Ticketing | +| **Nextcloud** | cloud.toppyr.de | Dateiverwaltung | +| **Uptime Kuma** | status.toppyr.de | Monitoring, Alerts | +| **Netdata** | netdata.toppyr.de | System Metrics | +| **lldap** | ldap.toppyr.de | LDAP Directory | + +## πŸš€ Deployment + +### Voraussetzungen + +- Docker & Docker Compose v2 +- Domain mit DNS-EintrΓ€gen auf Server-IP +- Ports 80, 443, 2222 (SSH fΓΌr Gitea) offen + +### Installation + +```bash +# Repository klonen +git clone https://repo.toppyr.de/tobias/toppyr-stack.git +cd toppyr-stack + +# Environment konfigurieren +cp .env.example .env +# PasswΓΆrter generieren und eintragen +openssl rand -base64 32 # FΓΌr jedes Passwort-Feld + +# Stack starten +docker compose up -d +``` + +### DNS-EintrΓ€ge + +Alle Subdomains als A-Record auf die Server-IP: + +``` +@, www, sso, wiki, project, status, crm, marketing, support, +postiz, ldap, netdata, gruen, cloud, repo β†’ +``` + +## πŸ” Authentifizierung + +- **SSO via Keycloak** fΓΌr: OpenProject, XWiki, Postiz, Gitea, Netdata +- **LDAP via lldap** fΓΌr: OpenProject (read-only) +- **Standalone** fΓΌr: EspoCRM, Mautic, FreeScout, Uptime Kuma + +## πŸ“ Verzeichnisstruktur + +``` +toppyr-stack/ +β”œβ”€β”€ docker-compose.yml # Hauptkonfiguration +β”œβ”€β”€ .env.example # Umgebungsvariablen-Template +β”œβ”€β”€ .env # Echte Secrets (nicht committed!) +β”œβ”€β”€ landing/ +β”‚ └── index.html # Landing Page toppyr.de +└── webhooks/ + └── gitea-openproject-webhook.py # Gitβ†’OpenProject Integration +``` + +## πŸ”— Integrationen + +### Gitea β†’ OpenProject Webhook + +Commits mit `WP-123` im Message werden automatisch als Kommentar im Work Package gepostet. + +```bash +# Beispiel Commit +git commit -m "WP-42: Feature implementiert" +``` + +### XWiki Git Macro + +Code aus Gitea-Repos direkt im Wiki einbetten: + +```wiki +{{git repository="https://repo.toppyr.de/tobias/toppyr-stack.git" + file="docker-compose.yml" + branch="main"}} +``` + +## 🧹 Wartung + +```bash +# Logs anzeigen +docker compose logs -f + +# Service neustarten +docker compose restart + +# Updates +docker compose pull +docker compose up -d + +# Backup Volumes +docker run --rm -v toppyr-stack_:/data -v $(pwd):/backup \ + alpine tar czf /backup/.tar.gz /data +``` + +## πŸ“Š Ressourcen + +Getestete RAM-Limits (gesamt ~8-10 GB): + +| Service | RAM Limit | +|---------|-----------| +| Keycloak | 2 GB | +| OpenProject | 4 GB | +| XWiki | 4 GB | +| Gitea | 512 MB | +| Postiz | 4 GB | +| Elasticsearch (Temporal) | 1 GB | +| Andere | 64-512 MB | + +## πŸ“ Lizenz + +Private Konfiguration fΓΌr toppyr.de. + +--- + +*Erstellt: MΓ€rz 2026* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..41ac8f4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,885 @@ +# ============================================================================= +# Collaboration Stack - PRODUCTION +# ============================================================================= +# RAM-Limits nach Empfehlung gesetzt (23.03.2026) +# Gesamt-RAM-Bedarf: ~13-14 GB (nach Entwickler-Empfehlungen) +# ============================================================================= +services: + # ============================================================================= + # TRAEFIK - Reverse Proxy mit Let's Encrypt + # ============================================================================= + traefik: + image: traefik:v2.11 + container_name: traefik + deploy: + resources: + limits: + memory: 64m + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--log.level=INFO" + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik_letsencrypt:/letsencrypt + networks: + - collaboration + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.middlewares=traefik-auth" + - "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_AUTH}" + # ============================================================================= + # KEYCLOAK - Identity Provider (SSO) + # ============================================================================= + keycloak-db: + image: postgres:16-alpine + container_name: keycloak-db + deploy: + resources: + limits: + memory: 256m + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + volumes: + - keycloak_db_data:/var/lib/postgresql/data + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U keycloak"] + interval: 10s + timeout: 5s + retries: 5 + keycloak: + image: quay.io/keycloak/keycloak:24.0 + container_name: keycloak + deploy: + resources: + limits: + memory: 2g + command: start + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_HOSTNAME: sso.${DOMAIN} + KC_PROXY: edge + KC_HTTP_ENABLED: "true" + depends_on: + keycloak-db: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.keycloak.rule=Host(`sso.${DOMAIN}`)" + - "traefik.http.routers.keycloak.entrypoints=websecure" + - "traefik.http.routers.keycloak.tls.certresolver=letsencrypt" + - "traefik.http.services.keycloak.loadbalancer.server.port=8080" + networks: + - collaboration + restart: unless-stopped + # ============================================================================= + # OPENPROJECT - Projektmanagement + # ============================================================================= + openproject: + image: openproject/openproject:14 + container_name: openproject + deploy: + resources: + limits: + memory: 4g + environment: + OPENPROJECT_SECRET_KEY_BASE: ${OPENPROJECT_SECRET} + OPENPROJECT_HOST__NAME: project.${DOMAIN} + OPENPROJECT_HTTPS: "true" + OPENPROJECT_DEFAULT__LANGUAGE: de + OPENPROJECT_SEED_ADMIN_USER_PASSWORD: ${OPENPROJECT_ADMIN_PASSWORD} + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_DISPLAY__NAME: "SSO Login" + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_HOST: "sso.${DOMAIN}" + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_IDENTIFIER: "openproject" + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_SECRET: ${OIDC_CLIENT_SECRET} + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_ISSUER: "https://sso.${DOMAIN}/realms/collaboration" + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_AUTHORIZATION__ENDPOINT: "https://sso.${DOMAIN}/realms/collaboration/protocol/openid-connect/auth" + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_TOKEN__ENDPOINT: "https://sso.${DOMAIN}/realms/collaboration/protocol/openid-connect/token" + OPENPROJECT_OPENID__CONNECT_KEYCLOAK_USERINFO__ENDPOINT: "https://sso.${DOMAIN}/realms/collaboration/protocol/openid-connect/userinfo" + volumes: + - openproject_data:/var/openproject/assets + - openproject_pgdata:/var/openproject/pgdata + labels: + - "traefik.enable=true" + - "traefik.http.routers.openproject.rule=Host(`project.${DOMAIN}`)" + - "traefik.http.routers.openproject.entrypoints=websecure" + - "traefik.http.routers.openproject.tls.certresolver=letsencrypt" + - "traefik.http.services.openproject.loadbalancer.server.port=80" + networks: + - collaboration + restart: unless-stopped + # ============================================================================= + # XWIKI - Wiki + # ============================================================================= + xwiki-db: + image: mysql:8.0 + container_name: xwiki-db + deploy: + resources: + limits: + memory: 1g + environment: + MYSQL_ROOT_PASSWORD: ${XWIKI_DB_ROOT_PASSWORD} + MYSQL_DATABASE: xwiki + MYSQL_USER: xwiki + MYSQL_PASSWORD: ${XWIKI_DB_PASSWORD} + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + volumes: + - xwiki_db_data:/var/lib/mysql + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + xwiki: + image: xwiki:16-mysql-tomcat + container_name: xwiki + deploy: + resources: + limits: + memory: 4g + environment: + DB_HOST: xwiki-db + DB_USER: xwiki + DB_PASSWORD: ${XWIKI_DB_PASSWORD} + DB_DATABASE: xwiki + JAVA_OPTS: "-Xmx2g -Xms1g" + volumes: + - xwiki_data:/usr/local/xwiki + depends_on: + xwiki-db: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.xwiki.rule=Host(`wiki.${DOMAIN}`)" + - "traefik.http.routers.xwiki.entrypoints=websecure" + - "traefik.http.routers.xwiki.tls.certresolver=letsencrypt" + - "traefik.http.services.xwiki.loadbalancer.server.port=8080" + networks: + - collaboration + restart: unless-stopped + # ============================================================================= + # NEXTCLOUD - Dateiverwaltung + # ============================================================================= + nextcloud-db: + image: postgres:16-alpine + container_name: nextcloud-db + deploy: + resources: + limits: + memory: 256m + environment: + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: ${NEXTCLOUD_DB_PASSWORD} + volumes: + - nextcloud_db_data:/var/lib/postgresql/data + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U nextcloud"] + interval: 10s + timeout: 5s + retries: 5 + nextcloud-redis: + image: redis:7-alpine + container_name: nextcloud-redis + deploy: + resources: + limits: + memory: 64m + networks: + - collaboration + restart: unless-stopped + nextcloud: + image: nextcloud:29-apache + container_name: nextcloud + deploy: + resources: + limits: + memory: 512m + environment: + POSTGRES_HOST: nextcloud-db + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: ${NEXTCLOUD_DB_PASSWORD} + REDIS_HOST: nextcloud-redis + NEXTCLOUD_ADMIN_USER: admin + NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD} + NEXTCLOUD_TRUSTED_DOMAINS: "cloud.${DOMAIN}" + OVERWRITEPROTOCOL: https + OVERWRITEHOST: cloud.${DOMAIN} + volumes: + - nextcloud_data:/var/www/html + depends_on: + nextcloud-db: + condition: service_healthy + nextcloud-redis: + condition: service_started + labels: + - "traefik.enable=true" + - "traefik.http.routers.nextcloud.rule=Host(`cloud.${DOMAIN}`)" + - "traefik.http.routers.nextcloud.entrypoints=websecure" + - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt" + - "traefik.http.services.nextcloud.loadbalancer.server.port=80" + - "traefik.http.routers.nextcloud.middlewares=nextcloud-caldav" + - "traefik.http.middlewares.nextcloud-caldav.redirectregex.permanent=true" + - "traefik.http.middlewares.nextcloud-caldav.redirectregex.regex=^https://(.*)/.well-known/(card|cal)dav" + - "traefik.http.middlewares.nextcloud-caldav.redirectregex.replacement=https://$${1}/remote.php/dav/" + networks: + - collaboration + restart: unless-stopped + # ============================================================================= + # LLDAP - Lightweight LDAP Server + # ============================================================================= + lldap: + image: lldap/lldap:stable + container_name: lldap + deploy: + resources: + limits: + memory: 128m + environment: + - UID=1000 + - GID=1000 + - TZ=Europe/Berlin + - LLDAP_JWT_SECRET=ZldD2dpHj3RssRqBwdgAVmjeSu7UfRamg7PCsrl0iw8= + - LLDAP_LDAP_USER_PASS=coHfqoruajDrXzeze6qA + - LLDAP_LDAP_BASE_DN=dc=toppyr,dc=de + volumes: + - lldap_data:/data + networks: + - collaboration + labels: + - "traefik.enable=true" + - "traefik.http.routers.lldap.rule=Host(`ldap.toppyr.de`)" + - "traefik.http.routers.lldap.entrypoints=websecure" + - "traefik.http.routers.lldap.tls.certresolver=letsencrypt" + - "traefik.http.services.lldap.loadbalancer.server.port=17170" + restart: unless-stopped + # ============================================================================= + # POSTIZ - Social Media Scheduler + # ============================================================================= + postiz: + image: ghcr.io/gitroomhq/postiz-app:latest + container_name: postiz + deploy: + resources: + limits: + memory: 4g + restart: unless-stopped + environment: + MAIN_URL: "https://postiz.toppyr.de" + FRONTEND_URL: "https://postiz.toppyr.de" + NEXT_PUBLIC_BACKEND_URL: "https://postiz.toppyr.de/api" + JWT_SECRET: "Xk9mL2pQnRs5tUvWxYz1A3bC4dEf6gHi" + DATABASE_URL: "postgresql://postiz:postiz-secret@postiz-db:5432/postiz" + REDIS_URL: "redis://postiz-redis:6379" + BACKEND_INTERNAL_URL: "http://127.0.0.1:3000" + IS_GENERAL: "true" + LINKEDIN_CLIENT_ID: "78tv4ijhgx8c1e" + LINKEDIN_CLIENT_SECRET: "WPL_AP1.C0UAsznfDj7xWWfs.SDHO8g==" + THREADS_APP_ID: "1641922170163889" + THREADS_APP_SECRET: "dea32c2064e54bf3d58b70e51a330ef9" + MASTODON_CLIENT_ID: "2XmVi5O3Bmv8NRwKiJ6Yi4osjSezLUoRce0zBr2xlAQ" + MASTODON_CLIENT_SECRET: "foi-oiFSzVXvPsMHzsj0LUsR8qZe7VxN5uI-VKzR6IQ" + MASTODON_URL: "https://gruene.social" + DISABLE_REGISTRATION: "true" + STORAGE_PROVIDER: "local" + UPLOAD_DIRECTORY: "/uploads" + POSTIZ_GENERIC_OAUTH: "true" + NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME: "Toppyr SSO" + POSTIZ_OAUTH_URL: "https://sso.toppyr.de" + POSTIZ_OAUTH_AUTH_URL: "https://sso.toppyr.de/realms/collaboration/protocol/openid-connect/auth" + POSTIZ_OAUTH_TOKEN_URL: "https://sso.toppyr.de/realms/collaboration/protocol/openid-connect/token" + POSTIZ_OAUTH_USERINFO_URL: "https://sso.toppyr.de/realms/collaboration/protocol/openid-connect/userinfo" + POSTIZ_OAUTH_CLIENT_ID: "postiz" + POSTIZ_OAUTH_CLIENT_SECRET: "xU7jKdZVrHHfp4VTligJKURgm7OXzkud" + TEMPORAL_ADDRESS: "temporal:7233" + NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads" + volumes: + - postiz_config:/config + - postiz_uploads:/uploads + labels: + - "traefik.enable=true" + - "traefik.http.routers.postiz.rule=Host(`postiz.toppyr.de`)" + - "traefik.http.routers.postiz.entrypoints=websecure" + - "traefik.http.routers.postiz.tls.certresolver=letsencrypt" + - "traefik.http.services.postiz.loadbalancer.server.port=5000" + networks: + - collaboration + depends_on: + postiz-db: + condition: service_healthy + postiz-redis: + condition: service_healthy + postiz-db: + image: postgres:17-alpine + container_name: postiz-db + deploy: + resources: + limits: + memory: 256m + restart: unless-stopped + environment: + POSTGRES_DB: postiz + POSTGRES_USER: postiz + POSTGRES_PASSWORD: postiz-secret + volumes: + - postiz_db:/var/lib/postgresql/data + networks: + - collaboration + healthcheck: + test: ["CMD", "pg_isready", "-U", "postiz"] + interval: 10s + timeout: 5s + retries: 5 + postiz-redis: + image: redis:7.2-alpine + container_name: postiz-redis + deploy: + resources: + limits: + memory: 64m + restart: unless-stopped + volumes: + - postiz_redis:/data + networks: + - collaboration + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + # ============================================================================= + # LANDING PAGE + # ============================================================================= + landing: + image: nginx:alpine + container_name: landing + deploy: + resources: + limits: + memory: 32m + restart: unless-stopped + volumes: + - /opt/collaboration/landing:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.landing.rule=Host(`toppyr.de`) || Host(`www.toppyr.de`)" + - "traefik.http.routers.landing.entrypoints=websecure" + - "traefik.http.routers.landing.tls.certresolver=letsencrypt" + - "traefik.http.services.landing.loadbalancer.server.port=80" + networks: + - collaboration + # ============================================================================= + # GRÜNE ANTRAEGE - Antragstools + # ============================================================================= + gruene-antraege: + image: nginx:alpine + container_name: gruene-antraege + deploy: + resources: + limits: + memory: 32m + restart: unless-stopped + volumes: + - /opt/collaboration/gruene-antraege:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.gruene.middlewares=sso-auth@docker" + - "traefik.http.routers.gruene.rule=Host(`gruen.toppyr.de`)" + - "traefik.http.routers.gruene.entrypoints=websecure" + - "traefik.http.routers.gruene.tls.certresolver=letsencrypt" + - "traefik.http.services.gruene.loadbalancer.server.port=80" + networks: + - collaboration + # ============================================================================= + # TEMPORAL STACK - Workflow Engine for Postiz + # ============================================================================= + temporal-elasticsearch: + container_name: temporal-elasticsearch + image: elasticsearch:7.17.27 + deploy: + resources: + limits: + memory: 1g + restart: unless-stopped + environment: + - cluster.routing.allocation.disk.threshold_enabled=true + - cluster.routing.allocation.disk.watermark.low=512mb + - cluster.routing.allocation.disk.watermark.high=256mb + - cluster.routing.allocation.disk.watermark.flood_stage=128mb + - discovery.type=single-node + - ES_JAVA_OPTS=-Xms512m -Xmx512m + - xpack.security.enabled=false + networks: + - collaboration + expose: + - 9200 + volumes: + - temporal_es_data:/usr/share/elasticsearch/data + temporal-postgresql: + container_name: temporal-postgresql + image: postgres:16 + deploy: + resources: + limits: + memory: 256m + restart: unless-stopped + environment: + POSTGRES_PASSWORD: temporal + POSTGRES_USER: temporal + networks: + - collaboration + expose: + - 5432 + volumes: + - temporal_pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "temporal"] + interval: 10s + timeout: 5s + retries: 5 + temporal: + container_name: temporal + image: temporalio/auto-setup:1.28.1 + deploy: + resources: + limits: + memory: 256m + restart: unless-stopped + depends_on: + temporal-postgresql: + condition: service_healthy + temporal-elasticsearch: + condition: service_started + environment: + - DB=postgres12 + - DB_PORT=5432 + - POSTGRES_USER=temporal + - POSTGRES_PWD=temporal + - POSTGRES_SEEDS=temporal-postgresql + - ENABLE_ES=true + - ES_SEEDS=temporal-elasticsearch + - ES_VERSION=v7 + - TEMPORAL_NAMESPACE=default + networks: + - collaboration + expose: + - 7233 + # ============================================================================= + # TRAEFIK FORWARD AUTH (Keycloak SSO) + # ============================================================================= + forward-auth: + image: thomseddon/traefik-forward-auth:2 + container_name: forward-auth + deploy: + resources: + limits: + memory: 64m + restart: unless-stopped + environment: + PROVIDERS_OIDC_ISSUER_URL: "https://sso.toppyr.de/realms/collaboration" + PROVIDERS_OIDC_CLIENT_ID: "gruene-antraege" + PROVIDERS_OIDC_CLIENT_SECRET: "gruene-antraege-secret-2026" + SECRET: "forward-auth-secret-random-string-2026" + DEFAULT_PROVIDER: "oidc" + LOG_LEVEL: "debug" + COOKIE_DOMAIN: "toppyr.de" + URL_PATH: "/_oauth" + labels: + - "traefik.enable=true" + - "traefik.http.middlewares.sso-auth.forwardauth.address=http://forward-auth:4181" + - "traefik.http.middlewares.sso-auth.forwardauth.authResponseHeaders=X-Forwarded-User" + - "traefik.http.middlewares.sso-auth.forwardauth.trustForwardHeader=true" + - "traefik.http.services.forward-auth.loadbalancer.server.port=4181" + # ============================================================================= + + # ============================================================================= + # ESPOCRM - CRM System + # ============================================================================= + espocrm-db: + image: mariadb:10.11 + container_name: espocrm-db + deploy: + resources: + limits: + memory: 512m + environment: + MARIADB_ROOT_PASSWORD: ${ESPOCRM_DB_PASSWORD} + MARIADB_DATABASE: espocrm + MARIADB_USER: espocrm + MARIADB_PASSWORD: ${ESPOCRM_DB_PASSWORD} + volumes: + - espocrm_db_data:/var/lib/mysql + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + + espocrm: + image: espocrm/espocrm:latest + container_name: espocrm + deploy: + resources: + limits: + memory: 512m + environment: + ESPOCRM_DATABASE_HOST: espocrm-db + ESPOCRM_DATABASE_NAME: espocrm + ESPOCRM_DATABASE_USER: espocrm + ESPOCRM_DATABASE_PASSWORD: ${ESPOCRM_DB_PASSWORD} + ESPOCRM_ADMIN_USERNAME: admin + ESPOCRM_ADMIN_PASSWORD: ${ESPOCRM_ADMIN_PASSWORD} + ESPOCRM_SITE_URL: https://crm.toppyr.de + volumes: + - espocrm_data:/var/www/html + depends_on: + espocrm-db: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.espocrm.rule=Host(`crm.toppyr.de`)" + - "traefik.http.routers.espocrm.entrypoints=websecure" + - "traefik.http.routers.espocrm.tls.certresolver=letsencrypt" + - "traefik.http.services.espocrm.loadbalancer.server.port=80" + networks: + - collaboration + restart: unless-stopped + + # MAUTIC - Marketing Automation + # ============================================================================= + mautic-db: + image: mariadb:10.11 + container_name: mautic-db + deploy: + resources: + limits: + memory: 512m + environment: + MARIADB_ROOT_PASSWORD: ${MAUTIC_DB_PASSWORD} + MARIADB_DATABASE: mautic + MARIADB_USER: mautic + MARIADB_PASSWORD: ${MAUTIC_DB_PASSWORD} + volumes: + - mautic_db_data:/var/lib/mysql + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + mautic: + image: mautic/mautic:5-apache + container_name: mautic + deploy: + resources: + limits: + memory: 1g + environment: + MAUTIC_DB_HOST: mautic-db + MAUTIC_DB_PORT: 3306 + MAUTIC_DB_NAME: mautic + MAUTIC_DB_USER: mautic + MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD} + MAUTIC_RUN_CRON_JOBS: "true" + MAUTIC_TRUSTED_PROXIES: "[\"0.0.0.0/0\"]" + volumes: + - mautic_data:/var/www/html + depends_on: + mautic-db: + condition: service_healthy + labels: + - "traefik.enable=true" + #- "traefik.http.routers.mautic.middlewares=sso-auth@docker" # Disabled for native SAML + - "traefik.http.routers.mautic.rule=Host(`marketing.toppyr.de`)" + - "traefik.http.routers.mautic.entrypoints=websecure" + - "traefik.http.routers.mautic.tls.certresolver=letsencrypt" + - "traefik.http.services.mautic.loadbalancer.server.port=80" + networks: + - collaboration + restart: unless-stopped + # ============================================================================= + # FREESCOUT - Helpdesk / Ticketsystem + # ============================================================================= + freescout-db: + image: mariadb:10.11 + container_name: freescout-db + deploy: + resources: + limits: + memory: 256m + environment: + MARIADB_ROOT_PASSWORD: ${FREESCOUT_DB_PASSWORD} + MARIADB_DATABASE: freescout + MARIADB_USER: freescout + MARIADB_PASSWORD: ${FREESCOUT_DB_PASSWORD} + volumes: + - freescout_db_data:/var/lib/mysql + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + freescout: + image: tiredofit/freescout:latest + container_name: freescout + deploy: + resources: + limits: + memory: 512m + environment: + CONTAINER_NAME: freescout + DB_HOST: freescout-db + DB_NAME: freescout + DB_USER: freescout + DB_PASS: ${FREESCOUT_DB_PASSWORD} + SITE_URL: https://support.toppyr.de + ADMIN_EMAIL: mail@tobiasroedel.de + ADMIN_PASS: ${FREESCOUT_ADMIN_PASSWORD} + TIMEZONE: Europe/Berlin + ENABLE_SSL_PROXY: "TRUE" + volumes: + - freescout_data:/data + depends_on: + freescout-db: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.freescout.rule=Host(`support.toppyr.de`)" + - "traefik.http.routers.freescout.entrypoints=websecure" + - "traefik.http.routers.freescout.tls.certresolver=letsencrypt" + - "traefik.http.services.freescout.loadbalancer.server.port=80" + networks: + - collaboration + restart: unless-stopped + # ============================================================================= + # UPTIME KUMA - Monitoring + # ============================================================================= + + # ============================================================================= + # NETDATA PROXY (External service on host) + # ============================================================================= + netdata-proxy: + image: alpine/socat + container_name: netdata-proxy + restart: unless-stopped + command: TCP-LISTEN:19999,fork,reuseaddr TCP:host.docker.internal:19999 + extra_hosts: + - "host.docker.internal:host-gateway" + labels: + - "traefik.enable=true" + - "traefik.http.routers.netdata.rule=Host(`netdata.toppyr.de`)" + - "traefik.http.routers.netdata.entrypoints=websecure" + - "traefik.http.routers.netdata.tls.certresolver=letsencrypt" + + + - "traefik.http.services.netdata.loadbalancer.server.port=19999" + - "traefik.http.routers.netdata.middlewares=sso-auth@docker" + networks: + - collaboration + + # ============================================================================= + # MONITORING DASHBOARD (Static HTML) + # ============================================================================= + monitoring-dashboard: + image: nginx:alpine + container_name: monitoring-dashboard + restart: unless-stopped + volumes: + - /var/www/monitoring:/usr/share/nginx/html:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.monitoring.priority=100" + - "traefik.http.routers.monitoring.rule=Host(`toppyr.de`) || Host(`www.toppyr.de`)" + - "traefik.http.routers.monitoring.entrypoints=websecure" + - "traefik.http.routers.monitoring.tls.certresolver=letsencrypt" + - "traefik.http.routers.monitoring.middlewares=" + - "traefik.http.services.monitoring.loadbalancer.server.port=80" + networks: + - collaboration + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: uptime-kuma + deploy: + resources: + limits: + memory: 256m + restart: unless-stopped + volumes: + - uptime_kuma_data:/app/data + - /var/run/docker.sock:/var/run/docker.sock:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.uptime.rule=Host(`status.toppyr.de`)" + - "traefik.http.routers.uptime.entrypoints=websecure" + - "traefik.http.routers.uptime.tls.certresolver=letsencrypt" + - "traefik.http.services.uptime.loadbalancer.server.port=3001" + networks: + - collaboration + + + + # ============================================================================= + # GITEA - Git Repository Server + # ============================================================================= + gitea-db: + image: postgres:16-alpine + container_name: gitea-db + deploy: + resources: + limits: + memory: 256m + environment: + POSTGRES_DB: gitea + POSTGRES_USER: gitea + POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} + volumes: + - gitea_db_data:/var/lib/postgresql/data + networks: + - collaboration + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gitea"] + interval: 10s + timeout: 5s + retries: 5 + + gitea: + image: gitea/gitea:1.22 + container_name: gitea + deploy: + resources: + limits: + memory: 512m + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=gitea-db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=${GITEA_DB_PASSWORD} + - GITEA__server__DOMAIN=repo.${DOMAIN} + - GITEA__server__SSH_DOMAIN=repo.${DOMAIN} + - GITEA__server__ROOT_URL=https://repo.${DOMAIN}/ + - GITEA__server__SSH_PORT=2222 + - GITEA__server__SSH_LISTEN_PORT=22 + - GITEA__service__DISABLE_REGISTRATION=true + - GITEA__openid__ENABLE_OPENID_SIGNIN=false + - GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION=true + - GITEA__oauth2_client__USERNAME=nickname + volumes: + - gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "2222:22" + depends_on: + gitea-db: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.gitea.rule=Host(`repo.${DOMAIN}`)" + - "traefik.http.routers.gitea.entrypoints=websecure" + - "traefik.http.routers.gitea.tls.certresolver=letsencrypt" + - "traefik.http.services.gitea.loadbalancer.server.port=3000" + networks: + - collaboration + restart: unless-stopped + + # ============================================================================= + # WEBHOOK HANDLER - Gitea β†’ OpenProject Integration + # ============================================================================= + webhook-handler: + image: python:3.12-alpine + container_name: webhook-handler + deploy: + resources: + limits: + memory: 64m + environment: + - OPENPROJECT_URL=https://project.${DOMAIN} + - OPENPROJECT_API_KEY=${OPENPROJECT_API_KEY:-} + - GITEA_URL=https://repo.${DOMAIN} + - PORT=9000 + volumes: + - /opt/collaboration/webhooks:/app:ro + working_dir: /app + command: python gitea-openproject-webhook.py + networks: + - collaboration + restart: unless-stopped + +volumes: + gitea_db_data: + gitea_data: + traefik_letsencrypt: + traefik_certs: + keycloak_db_data: + openproject_data: + openproject_pgdata: + xwiki_db_data: + xwiki_data: + nextcloud_db_data: + nextcloud_data: + lldap_data: + postiz_config: + postiz_uploads: + postiz_db: + postiz_redis: + temporal_es_data: + temporal_pg_data: + espocrm_db_data: + espocrm_data: + mautic_db_data: + mautic_data: + freescout_db_data: + freescout_data: + uptime_kuma_data: + + +networks: + collaboration: + driver: bridge + diff --git a/landing/index.html b/landing/index.html new file mode 100644 index 0000000..8d72ef3 --- /dev/null +++ b/landing/index.html @@ -0,0 +1,232 @@ + + + + + + toppyr.de β€” Service Hub + + + +

toppyr.de

+

Collaboration & Infrastructure Hub

+ +
+
+
26
+
Services
+
+
+
βœ“
+
All Systems
+
+
+ + + + + + diff --git a/webhooks/gitea-openproject-webhook.py b/webhooks/gitea-openproject-webhook.py new file mode 100755 index 0000000..b2e09c9 --- /dev/null +++ b/webhooks/gitea-openproject-webhook.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +""" +Gitea β†’ OpenProject Webhook Handler +Parses commit messages for WP-XXX patterns and adds comments to Work Packages +""" +import os +import re +import json +import base64 +import urllib.request +from http.server import HTTPServer, BaseHTTPRequestHandler + +OPENPROJECT_URL = os.getenv("OPENPROJECT_URL", "https://project.toppyr.de") +OPENPROJECT_API_KEY = os.getenv("OPENPROJECT_API_KEY", "") +GITEA_URL = os.getenv("GITEA_URL", "https://git.toppyr.de") +WP_PATTERN = re.compile(r"WP-(\d+)", re.IGNORECASE) + +class WebhookHandler(BaseHTTPRequestHandler): + def do_POST(self): + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + + try: + payload = json.loads(body) + self.process_push(payload) + self.send_response(200) + self.end_headers() + self.wfile.write(b"OK") + except Exception as e: + print(f"Error: {e}") + self.send_response(500) + self.end_headers() + self.wfile.write(str(e).encode()) + + def process_push(self, payload): + repo_name = payload.get("repository", {}).get("full_name", "unknown") + commits = payload.get("commits", []) + + for commit in commits: + message = commit.get("message", "") + sha = commit.get("id", "")[:8] + author = commit.get("author", {}).get("name", "Unknown") + url = commit.get("url", "") + + # Find all WP-XXX references + matches = WP_PATTERN.findall(message) + for wp_id in matches: + self.add_openproject_comment( + wp_id=wp_id, + repo=repo_name, + sha=sha, + message=message, + author=author, + url=url + ) + + def add_openproject_comment(self, wp_id, repo, sha, message, author, url): + if not OPENPROJECT_API_KEY: + print(f"No API key - would comment on WP-{wp_id}") + return + + comment = f"""**Git Commit** [{sha}]({url}) +**Repository:** {repo} +**Author:** {author} + +``` +{message} +``` +""" + api_url = f"{OPENPROJECT_URL}/api/v3/work_packages/{wp_id}/activities" + data = json.dumps({"comment": {"raw": comment}}).encode() + + # Basic auth with apikey as username + auth = base64.b64encode(f"apikey:{OPENPROJECT_API_KEY}".encode()).decode() + + req = urllib.request.Request( + api_url, + data=data, + headers={ + "Content-Type": "application/json", + "Authorization": f"Basic {auth}" + }, + method="POST" + ) + + try: + with urllib.request.urlopen(req) as resp: + print(f"Added comment to WP-{wp_id}: {resp.status}") + except Exception as e: + print(f"Failed to comment on WP-{wp_id}: {e}") + + def log_message(self, format, *args): + print(f"[Webhook] {args[0]}") + +if __name__ == "__main__": + port = int(os.getenv("PORT", 9000)) + print(f"Starting webhook handler on port {port}") + HTTPServer(("0.0.0.0", port), WebhookHandler).serve_forever()