Add full stack configuration
- docker-compose.yml with all services - .env.example with placeholder secrets - Landing page HTML - Gitea→OpenProject webhook script - Comprehensive README with architecture docs
This commit is contained in:
commit
6e6ff63f08
49
.env.example
Normal file
49
.env.example
Normal file
@ -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=<generate-secure-password>
|
||||
KEYCLOAK_ADMIN_PASSWORD=<generate-secure-password>
|
||||
|
||||
# OIDC Client Secret (shared across services)
|
||||
OIDC_CLIENT_SECRET=<generate-secure-password>
|
||||
|
||||
# OpenProject
|
||||
OPENPROJECT_SECRET=<generate-64-char-hex>
|
||||
OPENPROJECT_ADMIN_PASSWORD=<generate-secure-password>
|
||||
|
||||
# XWiki
|
||||
XWIKI_DB_ROOT_PASSWORD=<generate-secure-password>
|
||||
XWIKI_DB_PASSWORD=<generate-secure-password>
|
||||
|
||||
# Nextcloud
|
||||
NEXTCLOUD_DB_PASSWORD=<generate-secure-password>
|
||||
NEXTCLOUD_ADMIN_PASSWORD=<generate-secure-password>
|
||||
|
||||
# EspoCRM
|
||||
ESPOCRM_DB_PASSWORD=<generate-secure-password>
|
||||
ESPOCRM_ADMIN_PASSWORD=<generate-secure-password>
|
||||
|
||||
# Mautic
|
||||
MAUTIC_DB_PASSWORD=<generate-secure-password>
|
||||
|
||||
# FreeScout
|
||||
FREESCOUT_DB_PASSWORD=<generate-secure-password>
|
||||
FREESCOUT_ADMIN_PASSWORD=<generate-secure-password>
|
||||
|
||||
# Gitea
|
||||
GITEA_DB_PASSWORD=<generate-secure-password>
|
||||
|
||||
# OpenProject API (for Gitea webhook)
|
||||
OPENPROJECT_API_KEY=<generate-from-openproject>
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Secrets - never commit!
|
||||
.env
|
||||
|
||||
# Local development
|
||||
*.log
|
||||
*.bak
|
||||
.DS_Store
|
||||
154
README.md
Normal file
154
README.md
Normal file
@ -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 → <SERVER_IP>
|
||||
```
|
||||
|
||||
## 🔐 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>
|
||||
|
||||
# Service neustarten
|
||||
docker compose restart <service>
|
||||
|
||||
# Updates
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
# Backup Volumes
|
||||
docker run --rm -v toppyr-stack_<volume>:/data -v $(pwd):/backup \
|
||||
alpine tar czf /backup/<volume>.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*
|
||||
885
docker-compose.yml
Normal file
885
docker-compose.yml
Normal file
@ -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
|
||||
|
||||
232
landing/index.html
Normal file
232
landing/index.html
Normal file
@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>toppyr.de — Service Hub</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
min-height: 100vh;
|
||||
color: #eee;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.5rem;
|
||||
background: linear-gradient(90deg, #00d4aa, #00a8cc);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.card {
|
||||
background: rgba(22, 33, 62, 0.8);
|
||||
border-radius: 16px;
|
||||
padding: 25px;
|
||||
border: 1px solid rgba(0, 212, 170, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: #00d4aa;
|
||||
box-shadow: 0 10px 40px rgba(0, 212, 170, 0.2);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
.card p {
|
||||
font-size: 0.9rem;
|
||||
color: #aaa;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card .tag {
|
||||
display: inline-block;
|
||||
background: rgba(0, 212, 170, 0.2);
|
||||
color: #00d4aa;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.section-title {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 1rem;
|
||||
color: #e94560;
|
||||
margin: 30px 0 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(233, 69, 96, 0.3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.section-title:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.status-item {
|
||||
text-align: center;
|
||||
}
|
||||
.status-item .value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #00d4aa;
|
||||
}
|
||||
.status-item .label {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
color: #555;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
footer a { color: #00d4aa; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>toppyr.de</h1>
|
||||
<p class="subtitle">Collaboration & Infrastructure Hub</p>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<div class="value">26</div>
|
||||
<div class="label">Services</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<div class="value">✓</div>
|
||||
<div class="label">All Systems</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="section-title">🔧 Collaboration</div>
|
||||
|
||||
<a href="https://crm.toppyr.de" class="card">
|
||||
<div class="card-icon">👥</div>
|
||||
<h2>CRM</h2>
|
||||
<p>Kontakte, Leads und Kundenbeziehungen verwalten</p>
|
||||
<span class="tag">EspoCRM</span>
|
||||
</a>
|
||||
|
||||
<a href="https://project.toppyr.de" class="card">
|
||||
<div class="card-icon">📋</div>
|
||||
<h2>Projekte</h2>
|
||||
<p>Projektmanagement, Tasks und Zeiterfassung</p>
|
||||
<span class="tag">OpenProject</span>
|
||||
</a>
|
||||
|
||||
<a href="https://wiki.toppyr.de" class="card">
|
||||
<div class="card-icon">📚</div>
|
||||
<h2>Wiki</h2>
|
||||
<p>Dokumentation und Wissensmanagement</p>
|
||||
<span class="tag">XWiki</span>
|
||||
</a>
|
||||
|
||||
<a href="https://support.toppyr.de" class="card">
|
||||
<div class="card-icon">🎫</div>
|
||||
<h2>Support</h2>
|
||||
<p>Helpdesk und Ticket-System</p>
|
||||
<span class="tag">FreeScout</span>
|
||||
</a>
|
||||
|
||||
<a href="https://marketing.toppyr.de" class="card">
|
||||
<div class="card-icon">📧</div>
|
||||
<h2>Marketing</h2>
|
||||
<p>E-Mail-Kampagnen und Automation</p>
|
||||
<span class="tag">Mautic</span>
|
||||
</a>
|
||||
|
||||
<a href="https://postiz.toppyr.de" class="card">
|
||||
<div class="card-icon">📱</div>
|
||||
<h2>Social Media</h2>
|
||||
<p>Cross-Posting und Scheduling</p>
|
||||
<span class="tag">Postiz</span>
|
||||
</a>
|
||||
|
||||
<div class="section-title">💻 Development</div>
|
||||
|
||||
<a href="https://repo.toppyr.de" class="card">
|
||||
<div class="card-icon">🦊</div>
|
||||
<h2>Git</h2>
|
||||
<p>Code-Repositories, Issues und CI/CD</p>
|
||||
<span class="tag">Gitea</span>
|
||||
</a>
|
||||
|
||||
<div class="section-title">🔐 Administration</div>
|
||||
|
||||
<a href="https://sso.toppyr.de" class="card">
|
||||
<div class="card-icon">🔑</div>
|
||||
<h2>SSO</h2>
|
||||
<p>Single Sign-On und Benutzerverwaltung</p>
|
||||
<span class="tag">Keycloak</span>
|
||||
</a>
|
||||
|
||||
<a href="https://ldap.toppyr.de" class="card">
|
||||
<div class="card-icon">👤</div>
|
||||
<h2>LDAP</h2>
|
||||
<p>Verzeichnisdienst</p>
|
||||
<span class="tag">lldap</span>
|
||||
</a>
|
||||
|
||||
<div class="section-title">📊 Monitoring</div>
|
||||
|
||||
<a href="https://status.toppyr.de" class="card">
|
||||
<div class="card-icon">💚</div>
|
||||
<h2>Uptime</h2>
|
||||
<p>Service-Status und Alerts</p>
|
||||
<span class="tag">Uptime Kuma</span>
|
||||
</a>
|
||||
|
||||
<a href="https://netdata.toppyr.de" class="card">
|
||||
<div class="card-icon">📈</div>
|
||||
<h2>Metrics</h2>
|
||||
<p>Server-Performance und Echtzeit-Metriken</p>
|
||||
<span class="tag">Netdata</span>
|
||||
</a>
|
||||
|
||||
<div class="section-title">🗃️ Daten</div>
|
||||
|
||||
<a href="https://gruen.toppyr.de" class="card">
|
||||
<div class="card-icon">🌱</div>
|
||||
<h2>Grüne Anträge</h2>
|
||||
<p>ALLRIS-Datenbank Hagen</p>
|
||||
<span class="tag">Datasette</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>VServer 152.53.119.77 · 16 GB RAM · 8 Cores · Debian 13</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
98
webhooks/gitea-openproject-webhook.py
Executable file
98
webhooks/gitea-openproject-webhook.py
Executable file
@ -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()
|
||||
Loading…
Reference in New Issue
Block a user