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