[[server]] name = "HomeLab" [server.config] address = "https://periphery:8120" enabled = true ## [[stack]] name = "autoheal" tags = ["NoBackup"] [stack.config] server = "HomeLab" project_name = "docker-tools" run_build = true poll_for_updates = true auto_update = true auto_update_all_services = true destroy_before_deploy = true file_contents = """ services: autoheal: image: willfarrell/autoheal:latest container_name: autoheal restart: unless-stopped environment: AUTOHEAL_INTERVAL: "10" AUTOHEAL_START_PERIOD: "60" AUTOHEAL_DEFAULT_STOP_TIMEOUT: "20" AUTOHEAL_CONTAINER_LABEL: "autoheal" volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - proxy_net networks: proxy_net: external: true """ ## [[stack]] name = "borg" tags = ["OnlineBackup"] [stack.config] server = "HomeLab" project_name = "borg-web-ui" file_contents = """ services: borg-ui: image: ainullcode/borg-ui:latest container_name: borg-web-ui restart: unless-stopped environment: - TZ=Europe/Berlin - PUID=0 - PGID=0 - BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes volumes: - /docker/appdata/borg/data:/data - /docker/appdata/borg/cache:/home/borg/.cache/borg - /docker:/local:rw - nas_backup:/repo:rw networks: proxy_net: {} docker: ipv4_address: 192.168.0.205 volumes: borg_data: borg_cache: nas_backup: driver: local driver_opts: type: nfs o: addr=192.168.0.100,nolock,soft device: ":/volume1/docker" networks: proxy_net: external: true docker: external: true """ ## [[stack]] name = "brave" tags = ["NoBackup"] [stack.config] server = "HomeLab" project_name = "web" poll_for_updates = true auto_update = true auto_update_all_services = true file_contents = """ services: brave: image: lscr.io/linuxserver/brave:latest container_name: brave environment: - PUID=0 - PGID=0 - TZ=Etc/Berlin volumes: - /path/to/config:/config shm_size: "1gb" restart: unless-stopped networks: proxy_net: {} networks: proxy_net: external: true """ ## [[stack]] name = "codeserver" tags = ["NoBackup"] [stack.config] server = "HomeLab" project_name = "codeserver" poll_for_updates = true auto_update = true auto_update_all_services = true file_contents = """ services: codeserver: image: lscr.io/linuxserver/code-server:latest container_name: codeserver environment: - DOCKER_HOST=tcp://socket-proxy-rw:2375 - PUID=990 - PGID=990 - UMASK=002 - SUDO_PASSWORD=Angel0$2012 - EXTENSIONS_GALLERY={"serviceUrl":"https://marketplace.visualstudio.com/_apis/public/gallery","itemUrl":"https://marketplace.visualstudio.com/items"} - TZ=Europe/Berlin - PROXY_DOMAIN=code.myitnetwork.de - DEFAULT_WORKSPACE=/config/workspace - DOCKER_MODS=linuxserver/mods:universal-docker|linuxserver/mods:code-server-python3|linuxserver/mods:code-server-nodejs|linuxserver/mods:code-server-npmglobal user: "990:990" volumes: - /docker/appdata/codeserver/config:/config - /docker/appdata:/docker/appdata/ restart: unless-stopped networks: - proxy_net - socket_net_rw # ← fehlt noch! labels: - "autoheal=true" networks: proxy_net: external: true socket_net_rw: external: true """ ## [[stack]] name = "gotify" tags = ["OnlineBackup"] [stack.config] server = "HomeLab" file_contents = """ services: gotify: image: gotify/server:latest container_name: gotify restart: unless-stopped security_opt: - no-new-privileges:true environment: - TZ=Europe/Berlin - GOTIFY_DEFAULTUSER_PASS=admin123 volumes: - /docker/appdata/gotify/gotify:/app/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80/health"] interval: 5s timeout: 3s retries: 5 start_period: 10s networks: proxy_net: {} igotify: image: ghcr.io/androidseb25/igotify-notification-assist:latest container_name: igotify restart: unless-stopped security_opt: - no-new-privileges:true pull_policy: always depends_on: gotify: condition: service_healthy environment: - GOTIFY_URLS=http://gotify:80 - GOTIFY_CLIENT_TOKENS=CBEGKJ1VijUFh_Y - SECNTFY_TOKENS=NTFY-DEVICE-lrbFW1YmtXJJp5hBgblf9c7WSAlZ8qyJ0pkHAWoL3UZJ51iN5J4ZIwW - ENABLE_CONSOLE_LOG=true volumes: - /docker/appdata/gotify/igotify:/app/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/Version"] interval: 5s timeout: 3s retries: 5 start_period: 10s networks: proxy_net: {} networks: proxy_net: external: true """ ## [[stack]] name = "lm_saas" tags = ["OfflineBackup"] [stack.config] server = "HomeLab" poll_for_updates = true auto_update = true auto_update_all_services = true destroy_before_deploy = true linked_repo = "lm_saas" reclone = true webhook_secret = "47woEiup!zPUoG0S4u!U" webhook_force_deploy = true file_paths = ["docker-compose.yml"] pre_deploy.path = "lm_saas" extra_args = [ "--build --pull always --force-recreate" ] environment = """ ############################ # Global ############################ TZ=Europe/Berlin ############################ # Network (Root docker-compose.yml) ############################ DOCKER_NETWORK=proxy_net #IP_DB=192.168.10.60 #IP_API=192.168.10.61 #IP_ADMIN_UI=192.168.10.62 #IP_CUSTOMER_UI=192.168.10.63 #IP_CUSTOMER_UI_V2=192.168.10.64 #IP_CONTROL=192.168.10.66 #IP_CUSTOMER_UI_V2_DEMO=192.168.10.65 ############################ # Backend / API ############################ API_PORT=8000 ENVIRONMENT=prod APP_VERSION=0.1.0 GIT_COMMIT=manual BUILD_TIMESTAMP=unknown BUILD_BRANCH=unknown IMAGE_TAG=unknown ADMIN_API_KEY=change-me JWT_SECRET=please-set-a-long-random-secret JWT_ALGORITHM=HS256 ACCESS_TOKEN_EXPIRES_MIN=15 REFRESH_TOKEN_EXPIRES_DAYS=30 REFRESH_TOKEN_GRACE_MIN=5 BASE_DOMAIN=test.myitnetwork.de BASE_ADMIN_DOMAIN=admin.test.myitnetwork.de BASE_DOMAIN_V2=lm.myitnetwork.de ############################ # Database ############################ POSTGRES_DB=lager POSTGRES_USER=lager POSTGRES_PASSWORD=change-me POSTGRES_DATA_DIR=/var/lib/postgresql/data DATABASE_URL=postgresql+asyncpg://lager:change-me@db:5432/lager ############################ # SchemaSpy (DB Dokumentation) ############################ HOST_SCHEMASPY_OUTPUT_DIR=/docker/appdata/lm_saas/postgres/schemaspy/output SCHEMASPY_DB_SCHEMA=public SCHEMASPY_CONTAINER_NAME=lm-schemaspy SCHEMASPY_DB_PORT=5432 ############################ # Admin Frontend Backup ############################ BACKUP_STORAGE_DRIVER=local BACKUP_STORAGE_PATH=/app/data/backups BACKUP_RETENTION_MAX_DAYS= BACKUP_RETENTION_MAX_COUNT= BACKUP_SCHEDULE_ENABLED=false BACKUP_SCHEDULE_INTERVAL_MINUTES=1440 ############################ # Admin-Frontend v2 ############################ VITE_API_BASE_URL=/api VITE_APP_TITLE=Admin Panel v2 BASE_DOMAIN_V2_DEMO=lm.myitnetwork.de ############################ # Storage ############################ BRANDING_STORAGE_PATH=/app/data/branding ############################ # Admin Frontend ############################ #ADMIN_UI_NODE_CACHE=C:\Users\amon\Documents\test\admin-ui\node-cache #ADMIN_UI_LOGS=C:\Users\amon\Documents\test\admin-ui\logs ############################ # Customer Frontend ############################ #CUSTOMER_UI_NODE_CACHE=C:\Users\amon\Documents\test\customer-ui\node-cache #CUSTOMER_UI_LOGS=C:\Users\amon\Documents\test\customer-ui\logs ############################ # Docker Host ############################ HOST_BRANDING_PATH=/docker/appdata/lm_saas/branding HOST_BACKUP_PATH=/docker/appdata/lm_saas/backup HOST_POSTGRES_DATA_DIR=/docker/appdata/lm_saas/postgres/data ############################ VITE_BASE_DOMAIN=test.myitnetwork.de VITE_TENANT_SLUG=kunde1 VITE_GRAFANA_URL=http://localhost:3000 VITE_BUILD_INFO=1.0.0 ############################ # Initial Seed Data ############################ SEED_TENANT_SLUG=kunde1 SEED_TENANT_NAME=Kunde1 SEED_ADMIN_EMAIL=admin@test.myitnetwork.de SEED_ADMIN_PASSWORD=admin SEED_ADMIN_ROLE=owner SEED_CREATE_SAMPLE_DATA=true """ ## [[stack]] name = "lm_sass_control" [stack.config] server = "HomeLab" ## [[stack]] name = "opencloud" [stack.config] server = "HomeLab" file_contents = """ services: opencloud: image: opencloudeu/opencloud:latest container_name: opencloud restart: unless-stopped user: "990:990" entrypoint: - /bin/sh - -c - | opencloud init || true opencloud server environment: OC_URL: https://cloud.myitnetwork.de OC_INSECURE: false PROXY_HTTP_ADDR: 0.0.0.0:9200 OC_LOG_LEVEL: warn PROXY_TLS: false # Admin Passwort beim ersten Start IDM_ADMIN_PASSWORD: ${OC_ADMIN_PASSWORD} volumes: - /docker/appdata/opencloud/config:/etc/opencloud - /docker/appdata/opencloud/data:/var/lib/opencloud - /mnt/nas-data:/mnt/nas-data ports: - 9200:9200 networks: - proxy_net labels: - autoheal=true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9200/health"] interval: 30s timeout: 10s retries: 3 logging: driver: json-file options: max-size: "10m" max-file: "3" networks: proxy_net: external: true """ environment = """ # VARIABLE = value OC_ADMIN_PASSWORD=&EhM8YJ3K^U@v2$Nt*S8 """ ## [[stack]] name = "pocket-id" tags = ["OfflineBackup"] [stack.config] server = "HomeLab" project_name = "pocket-id" poll_for_updates = true auto_update = true auto_update_all_services = true file_contents = """ services: pocket-id: container_name: pocket-id image: ghcr.io/pocket-id/pocket-id:latest restart: unless-stopped volumes: - /docker/appdata/pocket-id/data:/app/data # Optional healthcheck healthcheck: test: [ "CMD", "/app/pocket-id", "healthcheck" ] interval: 1m30s timeout: 5s retries: 2 start_period: 10s environment: - ENCRYPTION_KEY=bm90aWNlZW1wdHljZW50ZXJvdXRzaWRlc2luY2VzaG93Y2hhcmFjdGVyc3RvdmVjaG8= - TRUST_PROXY=true - PUID=990 - PGID=990 - APP_URL=https://sso.myitnetwork.de ## Mail - SMTP_HOST=webmail.myitnetwork.de - SMTP_PORT=465 - SMTP_FROM=SSO@myitnetwork.de - SMTP_USER=SSO@myitnetwork.de - SMTP_PASSWORD="X?kh#7bDA+O9*o_Od!" networks: - proxy_net labels: - "autoheal=true" networks: proxy_net: external: true """ ## [[stack]] name = "socket-proxy" tags = ["OnlineBackup"] [stack.config] server = "HomeLab" file_contents = """ services: socket-proxy-ro: # Read-Only fΓΌr Zoraxy etc. image: lscr.io/linuxserver/socket-proxy:latest container_name: socket-proxy-ro environment: CONTAINERS: 1 INFO: 1 IMAGES: 1 NETWORKS: 1 POST: 0 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: - socket_net_ro restart: unless-stopped security_opt: - no-new-privileges:true labels: - "autoheal=true" socket-proxy-rw: # Read-Write fΓΌr Autoheal, Komodo etc. image: lscr.io/linuxserver/socket-proxy:latest container_name: socket-proxy-rw environment: CONTAINERS: 1 INFO: 1 IMAGES: 1 POST: 1 START: 1 STOP: 1 RESTART: 1 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: - socket_net_rw restart: unless-stopped security_opt: - no-new-privileges:true labels: - "autoheal=true" networks: socket_net_ro: name: socket_net_ro driver: bridge socket_net_rw: name: socket_net_rw driver: bridge """ ## [[stack]] name = "sshwifty" tags = ["OfflineBackup"] [stack.config] server = "HomeLab" project_name = "sshwifty" run_build = true poll_for_updates = true auto_update = true auto_update_all_services = true file_contents = """ services: sshwifty: image: niruix/sshwifty:latest container_name: sshwifty environment: - SSHWIFTY_SHAREDKEY= extra_hosts: - "host.docker.internal:host-gateway" restart: unless-stopped networks: - proxy_net networks: proxy_net: external: true """ ## [[stack]] name = "template" template = true [stack.config] server = "HomeLab" file_contents = """ ################################ # 🦎 KOMODO STACK TEMPLATE 🦎 # Kopiere dieses Template fΓΌr neue Stacks # Ersetze alle Werte in <> mit deinen Werten ################################ services: # ============================================================ # HAUPTSERVICE # ============================================================ : image: : # z.B. nginx:latest oder nginx:1.25 container_name: # Eindeutiger Name, wird fΓΌr DNS-AuflΓΆsung verwendet hostname: # Optional: Hostname im Netzwerk (Standard = container_name) restart: unless-stopped # unless-stopped | always | on-failure | no # ---------------------------------------------------------- # KOMODO LABELS # ---------------------------------------------------------- labels: autoheal: true # Autoheal startet Container bei Healthcheck-Fehler neu komodo.skip: # Verhindert dass Komodo diesen Container stoppt (z.B. bei StopAllContainers) # komodo.managed: true # Optional: Explizit von Komodo verwaltet markieren # ---------------------------------------------------------- # UMGEBUNGSVARIABLEN # ---------------------------------------------------------- environment: TZ: Europe/Berlin # Zeitzone immer setzen! MY_VAR: ${MY_VAR} # Aus Komodo-Variablen (Secret) MY_STATIC_VAR: "wert" # Statischer Wert # Optional: .env Datei laden (bei Komodo eher selten nΓΆtig) # env_file: # - ./stack.env # ---------------------------------------------------------- # VOLUMES # ---------------------------------------------------------- volumes: # Bind Mount (Host-Pfad β†’ Container-Pfad) - /docker/appdata//config:/config - /docker/appdata//data:/data # Named Volume (von Docker verwaltet) # - -data:/data # Read-Only Mount # - /mnt/nas-data:/mnt/nas:ro # ---------------------------------------------------------- # PORTS # Nur wenn kein Reverse Proxy verwendet wird! # Mit Zoraxy als Reverse Proxy kΓΆnnen Ports weggelassen werden. # ---------------------------------------------------------- # ports: # - "8080:8080" # HOST:CONTAINER # - "127.0.0.1:8080:8080" # Nur lokal erreichbar (sicherer) # ---------------------------------------------------------- # NETZWERKE # ---------------------------------------------------------- networks: - proxy_net # FΓΌr Zoraxy-Erreichbarkeit (immer angeben!) # - _net # Internes Netzwerk z.B. fΓΌr DB-Kommunikation # ---------------------------------------------------------- # HEALTHCHECK # ---------------------------------------------------------- healthcheck: # HTTP Service: test: ["CMD-SHELL", "wget -qO- http://localhost:/health || exit 1"] # Alternativ mit curl: # test: ["CMD-SHELL", "curl -f http://localhost:/health || exit 1"] # Nur TCP Port prΓΌfen: # test: ["CMD-SHELL", "nc -z localhost || exit 1"] interval: 30s # Wie oft prΓΌfen timeout: 10s # Timeout pro Check retries: 3 # Wie oft fehlschlagen bevor unhealthy start_period: 30s # Startzeit bevor Healthcheck greift (bei langsamen Apps erhΓΆhen) # ---------------------------------------------------------- # RESSOURCEN LIMITS (optional) # ---------------------------------------------------------- # deploy: # resources: # limits: # cpus: "1.0" # memory: 512M # reservations: # memory: 128M # ---------------------------------------------------------- # LOGGING # ---------------------------------------------------------- logging: driver: json-file options: max-size: "10m" # Max Grâße pro Log-Datei max-file: "3" # Max Anzahl Log-Dateien (β†’ max 30MB) # ---------------------------------------------------------- # ABHΓ„NGIGKEITEN # ---------------------------------------------------------- # depends_on: # : # condition: service_healthy # Wartet bis DB healthy ist # ---------------------------------------------------------- # SECURITY (optional) # ---------------------------------------------------------- # user: "990:990" # Als docker:docker User laufen (990 = docker UID/GID) # read_only: true # Filesystem read-only (sicherer) # security_opt: # - no-new-privileges:true # Verhindert Privilege Escalation # ============================================================ # DATENBANK (Beispiel PostgreSQL) # ============================================================ -db: image: postgres:16-alpine container_name: -db restart: unless-stopped labels: komodo.skip: # DB nie automatisch stoppen environment: TZ: Europe/Berlin POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - /docker/appdata//db:/var/lib/postgresql/data networks: - _net # Nur im internen Netzwerk, nicht in proxy_net! healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] interval: 30s timeout: 10s retries: 3 start_period: 20s logging: driver: json-file options: max-size: "10m" max-file: "3" # ============================================================ # NAMED VOLUMES (optional, von Docker verwaltet) # ============================================================ # volumes: # -data: # driver: local # ============================================================ # NETZWERKE # ============================================================ networks: proxy_net: external: true # Bereits vorhanden (geteilt mit Zoraxy) _net: driver: bridge # Internes Netzwerk nur fΓΌr diesen Stack # Optionale Subnetz-Konfiguration: # ipam: # config: # - subnet: 172.20.0.0/24 """ ## [[stack]] name = "vaultwarden" tags = ["OfflineBackup"] [stack.config] server = "HomeLab" poll_for_updates = true auto_update = true auto_update_all_services = true file_contents = """ services: vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: unless-stopped volumes: # - /docker/appdata/vaultwarden/data:/data:rw - /mnt/nas/vaultwarden/data:/data:rw environment: - TZ=Europe/Berlin - DOMAIN=https://key.myitnetwork.de - WEBSOCKET_ENABLED=true - SIGNUPS_ALLOWED=false # - ADMIN_TOKEN=HIER_LANGEN_RANDOM_TOKEN_SETZEN - INVITATIONS_ALLOWED=false - PASSWORD_HINTS_ALLOWED=false - LOG_LEVEL=warn ### Mail - SMTP_FROM_NAME=Vaultwarden - SMTP_TIMEOUT=15 - SMTP_HOST=webmail.myitnetwork.de - SMTP_FROM=mail@myitnetwork.de - SMTP_PORT=465 - SMTP_SECURITY=force_tls - SMTP_USERNAME=mail@myitnetwork.de - SMTP_PASSWORD=glzYDP1^M_b[Ek^!5! labels: - "autoheal=true" networks: - proxy_net networks: proxy_net: external: true """ ## [[stack]] name = "zoraxy" tags = ["OnlineBackup"] [stack.config] server = "HomeLab" project_name = "zoraxy" poll_for_updates = true auto_update = true auto_update_all_services = true file_contents = """ services: zoraxy: image: zoraxydocker/zoraxy:latest container_name: zoraxy restart: unless-stopped ports: - "80:80" - "443:443" - "8010:8010" volumes: - /docker/appdata/zoraxy/config:/opt/zoraxy/config - /docker/appdata/zoraxy/plugin:/opt/zoraxy/plugin - /etc/localtime:/etc/localtime:ro environment: FASTGEOIP: "true" NOAUTH: "false" DOCKER: "true" PORT: "8010" TZ: "Europe/Berlin" CFGUPGRADE: "true" DOCKER_HOST: "tcp://socket-proxy-ro:2375" # ← neu extra_hosts: - "host.docker.internal:host-gateway" networks: proxy_net: {} socket_net_ro: {} docker: ipv4_address: 192.168.0.230 logging: driver: "json-file" options: max-size: "10m" max-file: "3" healthcheck: test: > sh -c " nc -z localhost 80 && nc -z localhost 443 && nc -z localhost 8010 && wget -qO- --header='Host: health.myitnetwork.de' http://localhost/ | grep -q '' " interval: 30s timeout: 10s retries: 3 start_period: 15s labels: - "autoheal=true" health-web: image: traefik/whoami:latest container_name: health-web restart: unless-stopped networks: proxy_net: {} labels: - "autoheal=true" crowdsec: image: crowdsecurity/crowdsec:latest container_name: zoraxy-crowdsec restart: unless-stopped networks: proxy_net: {} environment: GID: "1000" TZ: "Europe/Berlin" COLLECTIONS: "crowdsecurity/linux crowdsecurity/traefik" volumes: - /docker/appdata/crowdsec/acquis.d:/etc/crowdsec/acquis.d - /docker/appdata/crowdsec/db:/var/lib/crowdsec/data/ - /docker/appdata/crowdsec/config:/etc/crowdsec/ - /docker/appdata/zoraxy/config/log:/var/log/zoraxy:ro security_opt: - no-new-privileges:true logging: driver: "json-file" options: max-size: "10m" max-file: "3" labels: - "autoheal=true" networks: proxy_net: external: true docker: external: true socket_net_ro: external: true """ ## [[build]] name = "borg-web-ui" [build.config] builder = "Local" repo = "karanhudia/borg-ui" ## [[repo]] name = "Gitea_Nas" [repo.config] server = "HomeLab" builder = "Local" git_provider = "git.myitnetwork.de" git_account = "komodo" repo = "angelo/homelab-komodo" ## [[repo]] name = "borg-ui" [repo.config] server = "HomeLab" builder = "Local" ## [[repo]] name = "lm_saas" [repo.config] server = "HomeLab" builder = "Local" git_account = "MontesanoAngelo" repo = "AngeloMontesano/Dashboard-backend" ## [[procedure]] name = "Backup Core Database" description = "Triggers the Core database backup at the scheduled time." tags = ["system"] config.schedule = "Every day at 01:00" [[procedure.config.stage]] name = "Stage 1" enabled = true executions = [ { execution.type = "BackupCoreDatabase", execution.params = {}, enabled = true } ] ## [[procedure]] name = "Global Auto Update" description = "Pulls and auto updates Stacks and Deployments using 'poll_for_updates' or 'auto_update'." tags = ["system"] config.schedule = "Every day at 03:00" [[procedure.config.stage]] name = "Stage 1" enabled = true executions = [ { execution.type = "GlobalAutoUpdate", execution.params.skip_auto_update = false, enabled = true } ] ## [[procedure]] name = "Weekly Backup" config.webhook_enabled = false config.schedule = "Run every day at 4:00 pm" [[procedure.config.stage]] name = "StopStacks" enabled = true executions = [ { execution.type = "RunAction", execution.params.action = "auto-stop-stacks", execution.params.args = {}, enabled = true } ] [[procedure.config.stage]] name = "Backup&StartStacks" enabled = true executions = [ { execution.type = "RunAction", execution.params.action = "run-backup", execution.params.args = {}, enabled = true } ] ## [[action]] name = "auto-start-stacks" [action.config] run_at_startup = true file_contents = """ const TAG = 'OfflineBackup'; const MAX_WAIT_TIME = 10 * 60 * 1000; const COMMAND = 'StartStack'; // Busy Handling const BUSY_TEXT = 'ERROR: Resource is busy'; const BUSY_RETRIES = 3; const BUSY_WAIT_MS = 10_000; // 10s let errors: any[] = []; async function getOrCreateTag(tagName: string) { const tags = await komodo.read('ListTags', {}); const existing = tags.find((t: any) => t.name === tagName); if (existing) return existing._id.$oid; const tagInfo = await komodo.write('CreateTag', { name: tagName }); return tagInfo._id.$oid; } function formatLogs(logs: any): string { if (!logs) return '(no logs)'; if (Array.isArray(logs)) { return logs .map((l) => (typeof l === 'string' ? l : JSON.stringify(l))) .join('\n'); } return typeof logs === 'string' ? logs : JSON.stringify(logs, null, 2); } function extractStderrFromLogs(logs: any): string { if (!logs) return ''; const arr = Array.isArray(logs) ? logs : [logs]; // Nimm den letzten Eintrag mit stderr, falls vorhanden for (let i = arr.length - 1; i >= 0; i--) { const item = arr[i]; const stderr = item?.stderr; if (typeof stderr === 'string' && stderr.length > 0) return stderr; } return ''; } async function listStacksByTag(tagName: string, tagId: string) { const byName = await komodo.read('ListStacks', { query: { tags: [tagName] } }); if (Array.isArray(byName) && byName.length > 0) return byName; const byId = await komodo.read('ListStacks', { query: { tags: [tagId] } }); return byId || []; } async function isStackRunning(stack: any): Promise { // Feldnamen kΓΆnnen je nach Komodo Version variieren. // Wir prΓΌfen mehrere typische Kandidaten. const fresh = await komodo.read('GetStack', { id: stack.id }); const s = String(fresh?.status ?? fresh?.state ?? '').toLowerCase(); if (s.includes('running') || s.includes('started') || s.includes('up')) return true; if (typeof fresh?.running === 'boolean') return fresh.running; if (typeof fresh?.is_running === 'boolean') return fresh.is_running; // Fallback: wenn Komodo Container Infos liefert const containers = fresh?.containers; if (Array.isArray(containers) && containers.length > 0) { // Wenn mind. ein Container "running" ist, betrachten wir den Stack als gestartet return containers.some((c: any) => String(c?.state ?? c?.status ?? '').toLowerCase().includes('running')); } return false; } async function waitForUpdateComplete(stack: any, update: any) { const startTime = Date.now(); while (update.status !== 'Complete') { if (Date.now() - startTime > MAX_WAIT_TIME) { throw new Error( `Timeout: Stack "${stack.name}" did not perform ${COMMAND} within ${MAX_WAIT_TIME / 1000 / 60} min` ); } await new Promise((r) => setTimeout(r, 2000)); update = await komodo.read('GetUpdate', { id: update._id.$oid }); console.log(`⏳ "${stack.name}" ${COMMAND} status: ${update.status}`); } if (update.success) { console.log(`βœ… Stack "${stack.name}" has fully performed ${COMMAND}`); return; } // --- Busy Handling --- const stderr = extractStderrFromLogs(update.logs); if (stderr.includes(BUSY_TEXT)) { for (let attempt = 1; attempt <= BUSY_RETRIES; attempt++) { console.log(`⚠️ "${stack.name}" busy. Waiting ${BUSY_WAIT_MS / 1000}s (attempt ${attempt}/${BUSY_RETRIES})...`); await new Promise((r) => setTimeout(r, BUSY_WAIT_MS)); // Wenn der Stack inzwischen lΓ€uft, werten wir das als Erfolg if (await isStackRunning(stack)) { console.log(`βœ… Stack "${stack.name}" is running (busy lock was transient)`); return; } // Optional: erneut StartStack versuchen try { console.log(`πŸ” Retrying ${COMMAND} on "${stack.name}"...`); const retryUpdate = await komodo.execute(COMMAND, { stack: stack.id }); update = retryUpdate; // Warte wieder bis Complete const retryStart = Date.now(); while (update.status !== 'Complete') { if (Date.now() - retryStart > MAX_WAIT_TIME) break; await new Promise((r) => setTimeout(r, 2000)); update = await komodo.read('GetUpdate', { id: update._id.$oid }); console.log(`⏳ "${stack.name}" ${COMMAND} retry status: ${update.status}`); } if (update.status === 'Complete' && update.success) { console.log(`βœ… Stack "${stack.name}" has fully performed ${COMMAND} (after retry)`); return; } // Wenn wieder busy, loop geht weiter, ansonsten brechen wir nach den Retries raus const retryStderr = extractStderrFromLogs(update.logs); if (!retryStderr.includes(BUSY_TEXT)) break; } catch { // Falls der Retry selbst wegen busy/lock scheitert, nΓ€chster Versuch } } // Nach Retries: letzter Statuscheck, falls es trotzdem lΓ€uft if (await isStackRunning(stack)) { console.log(`βœ… Stack "${stack.name}" is running (busy but started elsewhere)`); return; } } // --- Real Failure --- const logs = formatLogs(update.logs); throw new Error(`Stack "${stack.name}" failed to perform ${COMMAND}.\n--- logs ---\n${logs}`); } async function run() { const tagId = await getOrCreateTag(TAG); const stacks = await listStacksByTag(TAG, tagId); for (const stack of stacks) { try { console.log(`πŸš€ Launching ${COMMAND} on stack "${stack.name}"...`); const update = await komodo.execute(COMMAND, { stack: stack.id }); await waitForUpdateComplete(stack, update); } catch (e: any) { console.log(`❌ Error performing ${COMMAND} on stack "${stack.name}":\n${e.message}`); errors.push(e); } } } await run(); if (errors.length > 0) { throw new Error(`⚠️ Encountered ${errors.length} errors during stacks ${COMMAND} commands.`); } """ ## [[action]] name = "auto-stop-stacks" [action.config] file_contents = """ const TAG = 'OfflineBackup'; const MAX_WAIT_TIME = 10 * 60 * 1000; const COMMAND = 'StopStack'; // Error tracking let errors: any[] = []; // Helper: ensure a tag exists and return its ID async function getOrCreateTag(tagName: string) { const tags = await komodo.read('ListTags', {}); const existing = tags.find(t => t.name === tagName); if (existing) return existing._id.$oid; try { const tagInfo = await komodo.write('CreateTag', { name: tagName }); return tagInfo._id.$oid; } catch(e) { console.log(`⚠️ Failed to create tag "${tagName}":`, e); errors.push(e); return null; } } // *COMMAND* all stacks with the tag async function startCommandOnStacks() { const autoTagId = await getOrCreateTag(TAG); if (!autoTagId) return; // Fetch all stacks that have the tag const stacks = await komodo.read('ListStacks', { query: { tags: [TAG] } }); // Step 1: Fire ALL stop commands and track their Update objects const updates = await Promise.all( stacks.map(async (stack) => { try { console.log(`πŸš€ Launching ${COMMAND} on stack "${stack.name}"...`); const update = await komodo.execute(COMMAND, { stack: stack.id }); return { stack, update }; } catch (e) { console.log(`⚠️ Failed to launch ${COMMAND} on stack "${stack.name}":`, e); errors.push(e); return { stack, update: null }; // Mark as failed } }) ); // Step 2: Wait for ALL updates to reach "Complete" status (with timeout) // See doc : https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html const waitForCompletion = async ({ stack, update }) => { if (!update) return; // Skip if the command failed earlier const startTime = Date.now(); while (update.status !== 'Complete') { // Check for timeout if (Date.now() - startTime > MAX_WAIT_TIME) { throw new Error(`Timeout: Stack "${stack.name}" did not succeed to perform ${COMMAND} within ${MAX_WAIT_TIME/1000/60} min`); } // Wait before polling again await new Promise(resolve => setTimeout(resolve, 2000)); update = await komodo.read('GetUpdate', { id: update._id.$oid }); // Refresh update status console.log(`⏳ "${stack.name}" ${COMMAND} status: ${update.status}`); } // Check for failure if (!update.success) { throw new Error(`Stack "${stack.name}" failed to to perform ${COMMAND}: ${update.logs.join(', ')}`); } console.log(`βœ… Stack "${stack.name}" has fully performed ${COMMAND}`); }; // Wait for all stacks to complete (or fail) await Promise.all( updates.map(update => waitForCompletion(update).catch(e => { console.log(`❌ Error performing ${COMMAND} on stack "${update.stack.name}":`, e.message); errors.push(e); }) ) ); } // Execute await startCommandOnStacks(); if (errors.length > 0) { throw `⚠️ Encountered ${errors.length} errors during stacks ${COMMAND} commands.`; } """ ## [[action]] name = "auto-update" tags = ["Komodo"] [action.config] file_contents = """ // ============================================================ // 🦎 Komodo Action: auto-update // Holt das neueste STABLE Release von GitHub, // aktualisiert den Image-Tag und deployt den Stack neu. // ============================================================ const GITHUB_REPO = "moghtech/komodo"; const VARIABLE_NAME = "COMPOSE_KOMODO_IMAGE_TAG"; const ROLLBACK_VARIABLE = "KOMODO_ROLLBACK_TAG"; const STACK_NAME = "komodo"; // ← Dein Stack-Name anpassen! // --- Neuestes stabiles Release holen --- console.log("β–Ά PrΓΌfe GitHub auf neuestes stabiles Release..."); const res = await fetch( `https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=15`, { headers: { Accept: "application/vnd.github+json" } } ); if (!res.ok) { throw new Error(`GitHub API Fehler: ${res.status}`); } const releases: any[] = await res.json(); const stable = releases.find((r: any) => !r.prerelease && !r.draft); if (!stable) { throw new Error("Kein stabiles Release auf GitHub gefunden!"); } // --- Image-Tag extrahieren --- function extractImageTag(release: any): string { const body = release?.body || ""; const match = body.match(/komodo-core:([^\s`\\"]+)/); if (match) return match[1]; return (release?.tag_name || "").replace(/^v/, ""); } const newTag = extractImageTag(stable); console.log(` Release: ${stable.tag_name} β†’ Image-Tag: ${newTag}`); // --- Aktuellen Tag lesen & Rollback speichern --- let currentTag = "unknown"; try { const variables = await komodo.read("ListVariables", {}); const tagVar = variables.find((v: any) => v.name === VARIABLE_NAME); if (tagVar) { currentTag = tagVar.value; } } catch { console.warn(" Konnte aktuellen Tag nicht lesen"); } console.log(` Aktueller Tag: ${currentTag}`); if (currentTag === newTag) { console.log("βœ” Bereits auf dem neuesten Stand β€” kein Update nΓΆtig."); } else { // Rollback-Tag speichern console.log(`β–Ά Speichere Rollback-Tag: ${currentTag}`); try { await komodo.write("UpdateVariableValue", { name: ROLLBACK_VARIABLE, value: currentTag, }); } catch { // Variable existiert evtl. noch nicht β€” anlegen try { await komodo.write("CreateVariable", { name: ROLLBACK_VARIABLE, value: currentTag, description: "Letzter Tag vor Auto-Update (fΓΌr Rollback)", }); } catch { console.warn(" Rollback-Variable konnte nicht gespeichert werden"); } } // Neuen Tag setzen console.log(`β–Ά Setze Image-Tag auf: ${newTag}`); await komodo.write("UpdateVariableValue", { name: VARIABLE_NAME, value: newTag, }); // Stack neu deployen console.log(`β–Ά Deploye Stack '${STACK_NAME}' neu...`); const update = await komodo.execute("DeployStack", { stack: STACK_NAME, }); console.log(""); console.log("═══════════════════════════════════════════"); console.log(` βœ” Update auf '${newTag}' gestartet!`); console.log(` Release: ${stable.tag_name}`); console.log(` Rollback-Tag: ${currentTag}`); console.log(` Update-ID: ${update.id}`); console.log("═══════════════════════════════════════════"); } """ ## [[action]] name = "rollback" tags = ["Komodo"] [action.config] file_contents = """ // ============================================================ // 🦎 Komodo Action: rollback // Setzt den Image-Tag auf den vorherigen Wert zurΓΌck // und deployt den Stack neu. // ============================================================ const VARIABLE_NAME = "COMPOSE_KOMODO_IMAGE_TAG"; const ROLLBACK_VARIABLE = "KOMODO_ROLLBACK_TAG"; const STACK_NAME = "komodo"; // ← Dein Stack-Name anpassen! // --- Rollback-Tag lesen --- console.log("β–Ά Lese Rollback-Tag..."); const variables = await komodo.read("ListVariables", {}); const rollbackVar = variables.find((v: any) => v.name === ROLLBACK_VARIABLE); const currentVar = variables.find((v: any) => v.name === VARIABLE_NAME); if (!rollbackVar || !rollbackVar.value) { throw new Error( `Kein Rollback-Tag gefunden! Variable '${ROLLBACK_VARIABLE}' existiert nicht oder ist leer. ` + `Wurde vorher ein Update mit der auto-update Action durchgefΓΌhrt?` ); } const rollbackTag = rollbackVar.value; const currentTag = currentVar?.value || "unknown"; console.log(` Aktueller Tag: ${currentTag}`); console.log(` Rollback zu: ${rollbackTag}`); if (currentTag === rollbackTag) { console.log("βœ” Aktueller Tag ist bereits der Rollback-Tag β€” nichts zu tun."); } else { // Tag setzen console.log(`β–Ά Setze Image-Tag auf: ${rollbackTag}`); await komodo.write("UpdateVariableValue", { name: VARIABLE_NAME, value: rollbackTag, }); // Deploy console.log(`β–Ά Deploye Stack '${STACK_NAME}' neu...`); const update = await komodo.execute("DeployStack", { stack: STACK_NAME, }); console.log(""); console.log("═══════════════════════════════════════════"); console.log(` βœ” Rollback auf '${rollbackTag}' gestartet!`); console.log(` Vorheriger Tag: ${currentTag}`); console.log(` Update-ID: ${update.id}`); console.log("═══════════════════════════════════════════"); } """ ## [[action]] name = "run-backup" [action.config] file_contents = """ // Variablen aus Komodo holen const vars = await komodo.read('ListVariables', {}); const BORG_USER = vars.find(v => v.name === 'BORG_USER')?.value || 'admin'; const BORG_PASSWORD = vars.find(v => v.name === 'BORG_PASSWORD')?.value || ''; const GOTIFY_TOKEN = vars.find(v => v.name === 'GOTIFY_BACKUP_TOKEN')?.value || ''; const BORG_URL = 'http://borg-web-ui:8081'; const GOTIFY_URL = 'http://gotify:80'; // Notification Helper async function notify(title: string, message: string, priority = 5) { try { await fetch(`${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, message, priority }) }); } catch (e) { console.log(`Gotify Fehler: ${e.message}`); } } // Login Borg Web UI const loginRes = await fetch(`${BORG_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `username=${BORG_USER}&password=${BORG_PASSWORD}` }); const { access_token } = await loginRes.json(); const headers = { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }; // Erst Lock brechen falls vorhanden console.log('Lock brechen falls vorhanden...'); const repoRes = await fetch(`${BORG_URL}/api/repositories/`, { headers }); const repoData = await repoRes.json(); const repo = repoData.repositories.find(r => r.path === '/repo/borg'); if (repo) { await fetch(`${BORG_URL}/api/repositories/${repo.id}/break-lock`, { method: 'POST', headers }); console.log('Lock OK'); } // Stacks auflisten const allStacks = await komodo.read('ListStacks', { query: {} }); const excludeStacks = await komodo.read('ListStacks', { query: { tags: ['NoBackup'] } }); const backedUp = allStacks.filter(s => !excludeStacks.find(e => e.id === s.id)); const backedUpNames = backedUp.map(s => s.name); const excludeNames = excludeStacks.map(s => s.name); console.log('Gesichert: ' + backedUpNames.join(', ')); console.log('Ausgeschlossen: ' + excludeNames.join(', ')); // Backup starten console.log('\nBackup wird gestartet...'); const startRes = await fetch(`${BORG_URL}/api/backup/start`, { method: 'POST', headers, body: JSON.stringify({ repository: '/repo/borg' }) }); const startData = await startRes.json(); const jobId = startData.job_id; console.log(`Job ID: ${jobId}`); let backupSuccess = false; const backupStartTime = Date.now(); const MAX_WAIT = 15 * 60 * 1000; while (true) { await new Promise(r => setTimeout(r, 5000)); const statusRes = await fetch(`${BORG_URL}/api/backup/status/${jobId}`, { headers }); const raw = await statusRes.text(); console.log(`RAW: ${raw}`); const statusData = JSON.parse(raw); const elapsed = ((Date.now() - backupStartTime) / 1000).toFixed(0); console.log(`[${elapsed}s] Status: ${statusData.status}`); if (statusData.status === 'completed' || statusData.status === 'success' || statusData.status === 'completed_with_warnings') { const p = statusData.progress_details || {}; const origMB = ((p.original_size || 0) / 1024 / 1024).toFixed(1); const compMB = ((p.compressed_size || 0) / 1024 / 1024).toFixed(1); const dedupMB = ((p.deduplicated_size || 0) / 1024 / 1024).toFixed(1); const msg = [ 'Gesichert: ' + backedUpNames.join(', '), excludeNames.length > 0 ? 'Ausgeschlossen: ' + excludeNames.join(', ') : '', '', 'Original: ' + origMB + ' MB', 'Komprimiert: ' + compMB + ' MB', 'Dedupliziert: ' + dedupMB + ' MB', 'Dateien: ' + p.nfiles, 'Dauer: ' + elapsed + 's' ].filter(Boolean).join('\n'); await notify('Backup erfolgreich', msg, 2); console.log('BACKUP ERFOLGREICH'); backupSuccess = true; break; } if (statusData.status === 'failed' || statusData.status === 'error') { await notify('BACKUP FEHLGESCHLAGEN', `Fehler: ${statusData.error_message}`, 9); console.log(`FEHLGESCHLAGEN: ${statusData.error_message}`); break; } if (Date.now() - backupStartTime > MAX_WAIT) { await notify('BACKUP TIMEOUT', 'Timeout nach 15 Min', 9); console.log('TIMEOUT'); break; } } // Container starten console.log('\nStarte Container...'); try { await komodo.execute('RunAction', { action: 'auto-start-stacks' }); console.log('Container gestartet!'); } catch (e) { console.log(`KRITISCH: ${e.message}`); await notify('KRITISCH: Container nicht gestartet!', e.message, 10); } if (!backupSuccess) { throw new Error('Backup nicht erfolgreich!'); } """ ## [[action]] name = "stack-status" tags = ["Komodo"] [action.config] file_contents = """ // ============================================================ // 🦎 Komodo Action: stack-status // Zeigt den Status aller Komodo-Stacks und Server. // ============================================================ console.log("═══════════════════════════════════════════"); console.log(" 🦎 Komodo Stack & Server Status"); console.log("═══════════════════════════════════════════"); console.log(""); // --- Server Status --- console.log("── Server ──"); try { const servers = await komodo.read("ListServers", {}); for (const server of servers) { const state = server.info?.state || "Unknown"; const icon = state === "Ok" ? "●" : state === "NotOk" ? "✘" : "?"; console.log(` ${icon} ${server.name} (${state})`); } } catch (e: any) { console.warn(` Fehler beim Abrufen: ${e.message}`); } console.log(""); // --- Stacks Status --- console.log("── Stacks ──"); try { const stacks = await komodo.read("ListStacks", {}); for (const stack of stacks) { const state = stack.info?.state || "Unknown"; const icon = state === "Running" ? "●" : state === "Paused" ? "⏸" : state === "Stopped" ? "β—‹" : "?"; const server = stack.info?.server_name || "β€”"; console.log(` ${icon} ${stack.name.padEnd(30)} ${state.padEnd(12)} (${server})`); } console.log(""); console.log(` Gesamt: ${stacks.length} Stacks`); } catch (e: any) { console.warn(` Fehler beim Abrufen: ${e.message}`); } // --- Variablen --- console.log(""); console.log("── Image-Tag Variable ──"); try { const variables = await komodo.read("ListVariables", {}); const tagVar = variables.find((v: any) => v.name === "COMPOSE_KOMODO_IMAGE_TAG"); const rollbackVar = variables.find((v: any) => v.name === "KOMODO_ROLLBACK_TAG"); console.log(` COMPOSE_KOMODO_IMAGE_TAG: ${tagVar?.value || "(nicht gesetzt)"}`); console.log(` KOMODO_ROLLBACK_TAG: ${rollbackVar?.value || "(nicht gesetzt)"}`); } catch { console.warn(" Variablen konnten nicht gelesen werden"); } console.log(""); console.log("═══════════════════════════════════════════"); """ ## [[action]] name = "update-check" [action.config] file_contents = """ // ============================================================ // 🦎 Komodo Action: update-check // PrΓΌft GitHub auf neue Komodo-Versionen und vergleicht mit // dem aktuell konfigurierten Tag. // ============================================================ const GITHUB_REPO = "moghtech/komodo"; const VARIABLE_NAME = "COMPOSE_KOMODO_IMAGE_TAG"; // --- GitHub Releases abrufen --- const res = await fetch( `https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=15`, { headers: { Accept: "application/vnd.github+json" } } ); if (!res.ok) { console.error(`GitHub API Fehler: ${res.status} ${res.statusText}`); throw new Error("GitHub API nicht erreichbar"); } const releases: any[] = await res.json(); // Neuestes stabiles Release const stable = releases.find((r: any) => !r.prerelease && !r.draft); // Neuestes Pre-Release const prerelease = releases.find((r: any) => r.prerelease && !r.draft); // --- Aktuellen Tag aus Komodo-Variablen lesen --- let currentTag = "unknown"; try { const variables = await komodo.read("ListVariables", {}); const tagVar = variables.find((v: any) => v.name === VARIABLE_NAME); if (tagVar) { currentTag = tagVar.value; } } catch { console.warn("Variable nicht gefunden β€” prΓΌfe ob COMPOSE_KOMODO_IMAGE_TAG angelegt ist"); } // --- Image-Tag aus Release extrahieren --- function extractImageTag(release: any): string { const body = release?.body || ""; const match = body.match(/komodo-core:([^\s`\\"]+)/); if (match) return match[1]; const tag = release?.tag_name || ""; return tag.replace(/^v/, ""); } // --- Ergebnisse ausgeben --- console.log("═══════════════════════════════════════════"); console.log(" 🦎 Komodo Version Check"); console.log("═══════════════════════════════════════════"); console.log(""); console.log(` Aktueller Tag: ${currentTag}`); console.log(""); if (stable) { const stableTag = extractImageTag(stable); const isCurrent = currentTag === stableTag || currentTag === stable.tag_name.replace(/^v/, ""); console.log(` Neuestes Stable: ${stable.tag_name} β†’ Image: ${stableTag}`); console.log(` Status: ${isCurrent ? "βœ” Aktuell" : "⚠ Update verfΓΌgbar!"}`); console.log(` VerΓΆffentlicht: ${new Date(stable.published_at).toLocaleDateString("de-DE")}`); } else { console.log(" Neuestes Stable: (keines gefunden)"); } console.log(""); if (prerelease) { const preTag = extractImageTag(prerelease); console.log(` Neuestes Pre-Release: ${prerelease.tag_name} β†’ Image: ${preTag}`); console.log(` VerΓΆffentlicht: ${new Date(prerelease.published_at).toLocaleDateString("de-DE")}`); } else { console.log(" Neuestes Pre-Release: (keines gefunden)"); } console.log(""); console.log("═══════════════════════════════════════════"); """ ## [[builder]] name = "Local" [builder.config] type = "Server" params.server_id = "HomeLab" ## [[resource_sync]] name = "Gitea_Nas" [resource_sync.config] linked_repo = "Gitea_Nas" resource_path = ["komodo.toml"] managed = true include_variables = true include_user_groups = true ## [[variable]] name = "BORG_USER" value = "admin" is_secret = false ## [[variable]] name = "BORG_PASSWORD" value = "Pkfk#C56n8WaS4*U@1jC" is_secret = true ## [[variable]] name = "GOTIFY_BACKUP_TOKEN" value = "AH8giLhqNJn5f0G" is_secret = true ## [[variable]] name = "COMPOSE_KOMODO_IMAGE_TAG" is_secret = false ## [[variable]] name = "Gitea_token" value = "7bcbed9a85e29987f49c7538debb873a7eba925f" is_secret = true