1765 lines
49 KiB
TOML
1765 lines
49 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 = "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 |