From 404a012829883fed6145f4c185ff9dbb4d82ca72 Mon Sep 17 00:00:00 2001 From: samuelbiasgit Date: Thu, 27 Aug 2026 13:08:12 +0200 Subject: [PATCH 1/3] make scheduler integrations optional --- docker-compose.yml | 2 ++ docker/scheduler.py | 79 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 583e49f..4123c80 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,8 @@ services: HA_TOKEN: ${HA_TOKEN:?set HA_TOKEN} OPENWEATHER_API_KEY: ${OPENWEATHER_API_KEY:?set OPENWEATHER_API_KEY} CKAL_XLSX_DIR: /data/ckal-xlsx + STRATO_SYNC_ENABLED: ${STRATO_SYNC_ENABLED:-true} + BRANDSCHUTZ_MAIL_ENABLED: ${BRANDSCHUTZ_MAIL_ENABLED:-true} volumes: - profile_pictures:/var/www/html/intranet_repo/general_files/profile_pictures - content_news:/var/www/html/intranet_repo/projects/contentpanel/insertion/news diff --git a/docker/scheduler.py b/docker/scheduler.py index 3cf2ad9..7f05d83 100644 --- a/docker/scheduler.py +++ b/docker/scheduler.py @@ -37,23 +37,65 @@ def prepare_ssh_key() -> Path: def calendar_sync(): - run([PYTHON, str(ROOT / 'ckal_upload_client/client.py')], cwd=ROOT / 'ckal_upload_client') - run([PYTHON, str(ROOT / 'pdf_python/create_pdf.py')], cwd=ROOT / 'pdf_python') - run(['/bin/bash', str(ROOT / 'pdf_python/pdf_to_png.sh')], cwd=ROOT / 'pdf_python') + # CKalender-XLSX verarbeiten + run( + [PYTHON, str(ROOT / 'ckal_upload_client/client.py')], + cwd=ROOT / 'ckal_upload_client' + ) + + # PDF erzeugen + run( + [PYTHON, str(ROOT / 'pdf_python/create_pdf.py')], + cwd=ROOT / 'pdf_python' + ) + + # PDF in Dashboard-PNGs umwandeln + run( + ['/bin/bash', str(ROOT / 'pdf_python/pdf_to_png.sh')], + cwd=ROOT / 'pdf_python' + ) + + # STRATO-Synchronisation kann z. B. im DEV deaktiviert werden + strato_sync_enabled = os.getenv( + 'STRATO_SYNC_ENABLED', + 'true' + ).strip().lower() in ('1', 'true', 'yes', 'on') + + if not strato_sync_enabled: + print('STRATO Sync deaktiviert', flush=True) + print('Calendar Sync erfolgreich (ohne STRATO)', flush=True) + return required('STRATO_SSH_USER') required('STRATO_REMOTE_PATH') + host = os.getenv('STRATO_SSH_HOST', 'ssh.strato.de') port = os.getenv('STRATO_SSH_PORT', '22') - known_hosts = Path(os.getenv('STRATO_KNOWN_HOSTS_FILE', '/run/secrets/known_hosts')) + + known_hosts = Path( + os.getenv( + 'STRATO_KNOWN_HOSTS_FILE', + '/run/secrets/known_hosts' + ) + ) + if not known_hosts.is_file(): - raise RuntimeError(f'known_hosts missing: {known_hosts}') + raise RuntimeError( + f'known_hosts missing: {known_hosts}' + ) + key = prepare_ssh_key() + if not PDF_OUTPUT.is_file() or PDF_OUTPUT.stat().st_size == 0: - raise RuntimeError(f'PDF missing/empty: {PDF_OUTPUT}') + raise RuntimeError( + f'PDF missing/empty: {PDF_OUTPUT}' + ) run([ - '/usr/bin/scp', '-O', '-q', '-P', port, + '/usr/bin/scp', + '-O', + '-q', + '-P', port, '-i', str(key), '-o', 'IdentitiesOnly=yes', '-o', 'BatchMode=yes', @@ -62,24 +104,41 @@ def calendar_sync(): str(PDF_OUTPUT), f"{required('STRATO_SSH_USER')}@{host}:{required('STRATO_REMOTE_PATH')}", ]) + print('Calendar Sync erfolgreich', flush=True) def brandschutz_once_per_month(): now = datetime.now() + if now.day != 1 or now.hour < 8: return + brandschutz_enabled = os.getenv( + 'BRANDSCHUTZ_MAIL_ENABLED', + 'true' + ).strip().lower() in ('1', 'true', 'yes', 'on') + + if not brandschutz_enabled: + print('Brandschutz-Mail deaktiviert', flush=True) + return + marker = STATE_DIR / 'brandschutz-last-attempt' month = now.strftime('%Y-%m') + if marker.exists() and marker.read_text().strip() == month: return - # At-most-once: erst markieren, dann ausführen. Bei Fehler manuell prüfen und Marker löschen. + # At-most-once: erst markieren, dann ausführen marker.write_text(month + '\n') - print(f'Brandschutz-Lauf {month}', flush=True) - run(['/usr/local/bin/php', '/var/www/html/intranet_repo/projects/brandschutz/brandschutz_cron.php']) + print(f'Brandschutz-Lauf {month}', flush=True) + + run([ + '/usr/local/bin/php', + '/var/www/html/intranet_repo/projects/brandschutz/brandschutz_cron.php' + ]) + def main(): STATE_DIR.mkdir(parents=True, exist_ok=True) From a8aa2268752bb47a9114fb96f942ffa8d835ccbc Mon Sep 17 00:00:00 2001 From: samuelbiasgit Date: Thu, 27 Aug 2026 15:45:10 +0200 Subject: [PATCH 2/3] web/contentpanel - refactor Aushang-Upload to store PNGs only --- .../contentpanel/insertion/insert.php | 139 +++++------------- .../contentpanel/insertion/news_cleanup.php | 116 ++------------- 2 files changed, 51 insertions(+), 204 deletions(-) diff --git a/web/intranet_repo/projects/contentpanel/insertion/insert.php b/web/intranet_repo/projects/contentpanel/insertion/insert.php index 53c01d9..83866ab 100644 --- a/web/intranet_repo/projects/contentpanel/insertion/insert.php +++ b/web/intranet_repo/projects/contentpanel/insertion/insert.php @@ -8,7 +8,7 @@ if (!isset($groups["Allgemein"]) || $groups["Allgemein"] != 1) { exit(); } -// Abgelaufene News samt PDF und PNGs entfernen +// Abgelaufene Aushänge entfernen cleanupExpiredNews(); @@ -22,11 +22,6 @@ function sanitizeFileName($fileName) { return preg_replace('/[^A-Za-z0-9_\-]/', '_', str_replace(' ', '_', $fileName)); } -// Hilfsfunktion, um den Dateinamen zu generieren -function generateFileName($date, $name, $extension) { - return $date . '-' . $name . '.' . $extension; -} - /** PDF -> Text per pdftotext (Poppler) */ function pdfToText($pdfFullPath) { $bin = trim(shell_exec('command -v pdftotext')); @@ -186,23 +181,18 @@ if ( exit(); } -// Neuen Aushang als PDF hochladen und in PNG-Seiten umwandeln +// Neuen Aushang hochladen und direkt in PNG-Seiten umwandeln if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) { $date = (string) ($_POST['date'] ?? ''); $dateObject = DateTimeImmutable::createFromFormat('!Y-m-d', $date); - if ( - $dateObject === false || - $dateObject->format('Y-m-d') !== $date - ) { + if ($dateObject === false || $dateObject->format('Y-m-d') !== $date) { $_SESSION['meldung_dateien'] = 'Ungültiges Verfallsdatum.'; header('Location: insert.php#dateien'); exit(); } - // Bereinige den eingegebenen Namen. - $name = sanitizeFileName((string) ($_POST['name'] ?? '')); - $name = trim($name, '_-'); + $name = trim(sanitizeFileName((string) ($_POST['name'] ?? '')), '_-'); if ($name === '') { $_SESSION['meldung_dateien'] = 'Bitte einen gültigen Titel eingeben.'; @@ -210,68 +200,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) { exit(); } - if ($_FILES['fileToUpload']['error'] !== UPLOAD_ERR_OK) { - $_SESSION['meldung_dateien'] = 'Fehler beim Upload der PDF-Datei.'; - header('Location: insert.php#dateien'); - exit(); - } - - $fileExtension = strtolower( - pathinfo($_FILES['fileToUpload']['name'], PATHINFO_EXTENSION) - ); - - if ($fileExtension !== 'pdf') { - $_SESSION['meldung_dateien'] = 'Nur PDF-Dateien sind erlaubt.'; - header('Location: insert.php#dateien'); - exit(); - } - - $directoryPath = - '/var/www/html/intranet_repo/projects/contentpanel/insertion/news/'; - $imageOutputDirectory = $directoryPath . 'png/'; - if ( - !is_dir($imageOutputDirectory) && - !mkdir($imageOutputDirectory, 0755, true) && - !is_dir($imageOutputDirectory) + $_FILES['fileToUpload']['error'] !== UPLOAD_ERR_OK || + strtolower(pathinfo($_FILES['fileToUpload']['name'], PATHINFO_EXTENSION)) !== 'pdf' ) { - $_SESSION['meldung_dateien'] = - 'Das Aushang-Verzeichnis konnte nicht angelegt werden.'; + $_SESSION['meldung_dateien'] = 'Bitte eine gültige PDF-Datei hochladen.'; header('Location: insert.php#dateien'); exit(); } - $fileName = generateFileName($date, $name, 'pdf'); - $targetFilePath = $directoryPath . $fileName; - $baseFileName = pathinfo($fileName, PATHINFO_FILENAME); + $baseFileName = $date . '-' . $name; - if (!isValidNewsFileBase($baseFileName)) { - $_SESSION['meldung_dateien'] = 'Ungültiger Dateiname.'; - header('Location: insert.php#dateien'); - exit(); - } - - if (!move_uploaded_file( - $_FILES['fileToUpload']['tmp_name'], - $targetFilePath - )) { - $_SESSION['meldung_dateien'] = 'Fehler beim Speichern der PDF-Datei.'; + if (!is_dir(NEWS_PNG_DIRECTORY) && !mkdir(NEWS_PNG_DIRECTORY, 0755, true)) { + $_SESSION['meldung_dateien'] = 'Das Aushang-Verzeichnis konnte nicht angelegt werden.'; header('Location: insert.php#dateien'); exit(); } if (!convertPDFtoPNG( - $targetFilePath, - $imageOutputDirectory, + $_FILES['fileToUpload']['tmp_name'], + NEWS_PNG_DIRECTORY, $baseFileName )) { - // Fehlgeschlagenen Upload vollständig aufräumen. deleteNewsFiles($baseFileName); - - $_SESSION['meldung_dateien'] = - 'Fehler bei der Umwandlung der PDF. ' - . 'Die unvollständigen Dateien wurden entfernt.'; - + $_SESSION['meldung_dateien'] = 'Fehler bei der Umwandlung der PDF.'; header('Location: insert.php#dateien'); exit(); } @@ -281,6 +233,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) { exit(); } + // Löschen eines Gottesdienstes if (isset($_POST['delete_id'])) { $delete_id = $_POST['delete_id']; @@ -898,51 +851,29 @@ $pdo = null;
format('d.m.Y') : $expiryDate; - - $numPages = 0; - foreach ($pngFiles as $png) { - if (strpos($png, $baseFileName . '_page_') !== false) $numPages++; - } - - $aushangData[] = [ - 'fileBase' => $baseFileName, - 'formattedExpiry' => $formattedExpiryDate, - 'title' => $title, - 'numPages' => $numPages, - ]; - } - } - - if (empty($aushangData)) { - echo '

Keine Aushänge vorhanden.

'; - } else { - foreach ($aushangData as $aushang) { - echo '
'; - echo 'Aushang'; - echo '
' . htmlspecialchars($aushang['title']) . '
'; - echo '
' . $aushang['numPages'] . ($aushang['numPages'] == 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $aushang['formattedExpiry'] . '
'; - echo '
'; - echo ''; - echo ''; - echo '
'; - echo '
'; - } - } + if (!$aushangFiles) { + echo '

Keine Aushänge vorhanden.

'; } else { - echo '

Verzeichnis nicht gefunden.

'; + foreach ($aushangFiles as $file) { + $baseFileName = basename($file, '_page_1.png'); + $expiryDate = substr($baseFileName, 0, 10); + $title = substr($baseFileName, 11); + $numPages = count(glob(NEWS_PNG_DIRECTORY . '/' . $baseFileName . '_page_*.png') ?: []); + $formattedExpiryDate = DateTime::createFromFormat('!Y-m-d', $expiryDate)?->format('d.m.Y') ?? $expiryDate; + $safeBase = htmlspecialchars($baseFileName, ENT_QUOTES, 'UTF-8'); + + echo '
'; + echo 'Aushang'; + echo '
' . htmlspecialchars($title) . '
'; + echo '
' . $numPages . ($numPages === 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $formattedExpiryDate . '
'; + echo '
'; + echo ''; + echo ''; + echo '
'; + echo '
'; + } } ?>
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php b/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php index d9b301f..dc7fe84 100644 --- a/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php +++ b/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php @@ -1,133 +1,49 @@ format('Y-m-d') !== $expiryDateString + $expiryDate !== false && + $expiryDate->format('Y-m-d') === $dateString && + $today > $expiryDate ) { - error_log( - 'Ungültiges Ablaufdatum bei News-Datei: ' . $fileBase - ); - continue; - } - - /* - * Aushang bleibt den gesamten angegebenen Tag sichtbar. - * Beispiel Ablaufdatum 2026-09-05: - * Löschung ab 2026-09-06 00:00 Uhr. - */ - $deleteFrom = $expiryDate->modify('+1 day'); - - if ($now >= $deleteFrom) { - if (!deleteNewsFiles($fileBase)) { - error_log( - 'Abgelaufene News konnte nicht vollständig gelöscht werden: ' - . $fileBase - ); - } + deleteNewsFiles($fileBase); } } -} \ No newline at end of file +} From 909029ca34ea608d28e7bd5d62b0bc483f84cbeb Mon Sep 17 00:00:00 2001 From: samuelbiasgit Date: Thu, 27 Aug 2026 18:01:16 +0200 Subject: [PATCH 3/3] develop - introduce local-setup --- README.md | 323 +++++++++++++++++++++++++++++++++++++++ docker-compose.local.yml | 63 ++++++++ 2 files changed, 386 insertions(+) create mode 100644 docker-compose.local.yml diff --git a/README.md b/README.md index e69de29..bd170f2 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,323 @@ +# Intranet lokal aufsetzen + +## 1. Docker installieren + +### Windows + WSL 2 — empfohlen + +PowerShell als Administrator: + +```powershell +wsl --install -d Ubuntu-24.04 +wsl --update +``` + +Windows neu starten und prüfen: + +```powershell +wsl -l -v +``` + +Ubuntu muss mit **WSL-Version 2** laufen. + +Falls `Ubuntu-24.04` über `wsl --install` nicht installiert werden kann, Ubuntu 24.04 alternativ über den Microsoft Store installieren. + +Danach **Docker Desktop for Windows** installieren und das WSL-2-Backend verwenden. + +In Docker Desktop: + +```text +Settings → Resources → WSL Integration +``` + +Integration für Ubuntu aktivieren und in WSL testen: + +```bash +docker --version +docker compose version +docker run --rm hello-world +``` + +> Docker Engine nicht zusätzlich innerhalb von WSL installieren, wenn Docker Desktop verwendet wird. +> +> Für bessere Dateisystem-Performance das Repository möglichst im WSL-Dateisystem ablegen, z. B. `~/src/intranet`, nicht unter `/mnt/c/...`. + +--- + +### Linux — Ubuntu 24.04 LTS + +**1. Konfliktierende Pakete entfernen** + +```bash +sudo apt remove docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc +``` + +**2. Voraussetzungen installieren** + +```bash +sudo apt update +sudo apt install ca-certificates curl -y +``` + +**3. Docker GPG-Key hinzufügen** + +```bash +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc +``` + +**4. Docker-Repository einrichten** + +```bash +sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null < **Debian 13:** Das Projekt funktioniert ebenfalls unter Debian. Dafür das Docker-Repository entsprechend auf `linux/debian` umstellen: +> + +--- + +### macOS + +1. **Docker Desktop for Mac** passend für Apple Silicon oder Intel installieren. +2. Docker Desktop starten. +3. Im Terminal testen: + +```bash +docker --version +docker compose version +docker run --rm hello-world +``` + +Dokumentation: + + + +--- + +## 2. Lokale Docker-Konfiguration + +Im Projektroot müssen folgende Dateien liegen: + +```text +Dockerfile +docker-compose.yml +docker-compose.local.yml +web/ +``` + +Die produktive `docker-compose.yml` wird lokal **nicht** verwendet. Dadurch werden insbesondere folgende Produktionsbestandteile nicht gestartet: + +- `scheduler` +- `dokploy-network` +- Strato-SSH +- produktive Scheduler-Secrets + +Die lokale Compose-Datei startet ausschließlich: + +- `web` +- `db` (PostgreSQL) + +Sie verwendet denselben `Dockerfile` wie Produktion. + +Der Web-Container ist lokal unter `127.0.0.1:8080` erreichbar. PostgreSQL wird für pgAdmin unter `127.0.0.1:5432` freigegeben. + +Durch den Bind-Mount + +```text +./web → /var/www/html +``` + +werden Änderungen an PHP, CSS und JavaScript ohne erneuten Image-Build sichtbar. Laufzeitdaten wie Profilbilder und Aushänge liegen weiterhin in lokalen Docker-Volumes. + +--- + +## 3. Environment-Variablen anlegen + +Im Projektroot eine `.env` anlegen: + +```dotenv +# PostgreSQL +POSTGRES_VERSION=16 +DB_NAME=intranet +DB_USER=intranet +DB_PASSWORD=local-dev-password + +# Intranet +INTRANET_FQDN=localhost:8080 + +# Lokale Dummy-Werte für externe Dienste +SMTP_HOST=127.0.0.1 +SMTP_PORT=1025 +SMTP_USER=local +SMTP_PASSWORD=local +ADMIN_EMAIL=local@example.invalid + +SMS_GATEWAY_URL=http://127.0.0.1:9 +HA_BASE_URL=http://127.0.0.1:9 +HA_TOKEN=local +OPENWEATHER_API_KEY=local +``` + +### PostgreSQL-Version prüfen + +Lokal sollte möglichst dieselbe PostgreSQL-Major-Version wie in Produktion verwendet werden. Wir verwenden **PostgreSQL 18**. + +--- + +## 4. Produktivdatenbank mit pgAdmin sichern + +In pgAdmin: + +1. Produktivserver öffnen. +2. Unter **Databases** die Intranet-Datenbank rechtsklicken. +3. **Backup…** auswählen. +4. Datei außerhalb des Repositorys speichern, z. B. `intranet.dump`. +5. **Format: Custom** wählen. +6. Unter **Data Options → Do not save**: + - **Owner: Yes** + - **Privileges: Yes** +7. **Include CREATE DATABASE statement: No** lassen. +8. **Backup** starten. + +> Der Dump enthält Produktivdaten. Nicht committen oder weitergeben. + +--- + +## 5. Lokale Datenbank starten + +Im Projektroot: + +```bash +docker compose -f docker-compose.local.yml up -d db +``` + +Status prüfen: + +```bash +docker compose -f docker-compose.local.yml ps +``` + +--- + +## 6. Dump mit pgAdmin importieren + +In pgAdmin einen neuen Server registrieren: + +```text +Host: localhost +Port: 5432 +Maintenance database: intranet +Username: intranet +Password: local-dev-password +``` + +Die Werte müssen den Angaben in `.env` entsprechen. + +Danach: + +1. Lokale Datenbank `intranet` rechtsklicken. +2. **Restore…** auswählen. +3. **Format: Custom**. +4. `intranet.dump` auswählen. +5. Unter **Data Options → Do not save**: + - **Owner: Yes** + - **Privileges: Yes** +6. **Include CREATE DATABASE statement: No** +7. **Clean before restore: No** beim ersten Import. +8. **Restore** starten. + +Soll die lokale Datenbank später vollständig durch einen neuen Dump ersetzt werden: + +```bash +docker compose -f docker-compose.local.yml down -v +docker compose -f docker-compose.local.yml up -d db +``` + +Danach den Dump erneut importieren. + +> `down -v` löscht neben PostgreSQL auch die übrigen lokalen Intranet-Volumes. + +--- + +## 7. Intranet starten + +```bash +docker compose -f docker-compose.local.yml up -d --build +``` + +Aufrufen: + + + +Die Benutzer aus dem importierten Dump können anschließend für den Login verwendet werden. + +Die lokale Compose-Datei setzt `session.cookie_secure=0`, da lokal HTTP statt HTTPS verwendet wird. + +--- + +## Entwicklung + +Status: + +```bash +docker compose -f docker-compose.local.yml ps +``` + +Logs: + +```bash +docker compose -f docker-compose.local.yml logs -f web +``` + +Nach Änderungen am `Dockerfile` neu bauen: + +```bash +docker compose -f docker-compose.local.yml up -d --build +``` + +Normale Änderungen unter `web/` benötigen durch den Bind-Mount keinen Rebuild. + +Stoppen: + +```bash +docker compose -f docker-compose.local.yml down +``` + +Komplett zurücksetzen: + +```bash +docker compose -f docker-compose.local.yml down -v +``` \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..744a116 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,63 @@ +# LOCAL DEVELOPMENT ONLY + +name: intranet-local + +services: + web: + build: + context: . + dockerfile: Dockerfile + ports: + - "127.0.0.1:8080:80" + env_file: + - .env + environment: + TZ: Europe/Berlin + DB_HOST: db + DB_PORT: "5432" + CKAL_XLSX_DIR: /data/ckal-xlsx + STRATO_SYNC_ENABLED: "false" + BRANDSCHUTZ_MAIL_ENABLED: "false" + depends_on: + db: + condition: service_healthy + command: > + sh -c "printf '%s\n' 'session.cookie_secure = 0' + > /usr/local/etc/php/conf.d/zz-local.ini + && exec apache2-foreground" + volumes: + - ./web:/var/www/html + - profile_pictures:/var/www/html/intranet_repo/general_files/profile_pictures + - content_news:/var/www/html/intranet_repo/projects/contentpanel/insertion/news + - developer_work:/var/www/html/intranet_repo/projects/developerpanel/work + - seelsorge_data:/var/www/html/intranet_repo/projects/seelsorge/data + - dashboard_month:/var/www/html/intranet_repo/projects/dashboard/month + - ckal_xlsx:/data/ckal-xlsx + - calendar_pdf:/var/www/html/pdf + + db: + image: postgres:${POSTGRES_VERSION:-16} + ports: + - "127.0.0.1:5432:5432" + environment: + TZ: Europe/Berlin + POSTGRES_DB: ${DB_NAME:?set DB_NAME in .env} + POSTGRES_USER: ${DB_USER:?set DB_USER in .env} + POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 5s + timeout: 5s + retries: 12 + +volumes: + postgres_data: + profile_pictures: + content_news: + developer_work: + seelsorge_data: + dashboard_month: + ckal_xlsx: + calendar_pdf: