Files
homelab-komodo/komodo.toml
T

1842 lines
50 KiB
TOML

[[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
# ============================================================
<service-name>:
image: <image>:<tag> # z.B. nginx:latest oder nginx:1.25
container_name: <name> # Eindeutiger Name, wird für DNS-Auflösung verwendet
hostname: <name> # 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/<name>/config:/config
- /docker/appdata/<name>/data:/data
# Named Volume (von Docker verwaltet)
# - <name>-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!)
# - <name>_net # Internes Netzwerk z.B. für DB-Kommunikation
# ----------------------------------------------------------
# HEALTHCHECK
# ----------------------------------------------------------
healthcheck:
# HTTP Service:
test: ["CMD-SHELL", "wget -qO- http://localhost:<port>/health || exit 1"]
# Alternativ mit curl:
# test: ["CMD-SHELL", "curl -f http://localhost:<port>/health || exit 1"]
# Nur TCP Port prüfen:
# test: ["CMD-SHELL", "nc -z localhost <port> || 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:
# <db-service>:
# 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)
# ============================================================
<name>-db:
image: postgres:16-alpine
container_name: <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/<name>/db:/var/lib/postgresql/data
networks:
- <name>_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:
# <name>-data:
# driver: local
# ============================================================
# NETZWERKE
# ============================================================
networks:
proxy_net:
external: true # Bereits vorhanden (geteilt mit Zoraxy)
<name>_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<boolean> {
// 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