diff --git a/komodo.toml b/komodo.toml index 8c3001f..71ad915 100644 --- a/komodo.toml +++ b/komodo.toml @@ -1 +1,1842 @@ -## Add resources to get started +[[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 = "joplin" +tags = ["OnlineBackup"] +[stack.config] +server = "HomeLab" +file_contents = """ +services: + joplin-db: + image: postgres:16-alpine + container_name: joplin-db + restart: unless-stopped + environment: + POSTGRES_DB: joplin + POSTGRES_USER: joplin + POSTGRES_PASSWORD: ${JOPLIN_DB_PASSWORD} + volumes: + - /docker/appdata/joplin/db:/var/lib/postgresql/data + networks: + - joplin_net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U joplin"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + joplin: + image: joplin/server:latest + container_name: joplin + restart: unless-stopped + depends_on: + - joplin-db + environment: + APP_PORT: 22300 + APP_BASE_URL: https://notes.myitnetwork.de + DB_CLIENT: pg + POSTGRES_HOST: joplin-db + POSTGRES_PORT: 5432 + POSTGRES_DATABASE: joplin + POSTGRES_USER: joplin + POSTGRES_PASSWORD: ${JOPLIN_DB_PASSWORD} + networks: + - joplin_net + - proxy_net + labels: + - autoheal=true + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:22300/api/ping || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +networks: + joplin_net: + driver: bridge + proxy_net: + external: true +""" +environment = """ +JOPLIN_DB_PASSWORD=&EhM8YJ3K^U@v2*S8 +OAUTH_ENABLED: 1 +OAUTH_BASE_URL: https://sso.myitnetwork.de +OAUTH_CLIENT_ID: ${87df037b-3f09-4fe6-97e7-5689204b4ada} +OAUTH_CLIENT_SECRET: ${83ZHLIDcBBIH6Gvr0aA6wrOwozZSzx83} +""" + +## + +[[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 \ No newline at end of file