From b3af0c2109ced8ff1f2040e0859f9bd05e4e3904 Mon Sep 17 00:00:00 2001
From: menzelj
Date: Mon, 31 Aug 2026 09:12:34 +0200
Subject: [PATCH] Grow the bundled template library to 83 homelab apps (0.43.0)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The library shipped five templates. This adds 78 more, covering what
homelab lists and the self-hosted community actually run: media servers
and the *arr automation chain, DNS ad-blocking, reverse proxies, VPN,
SSO, monitoring and dashboards, files/backup, notes and wikis, home
automation, dev tooling, databases, local AI, finance and notifications.
Every template follows the existing shape — compose.yaml, .env.example,
template.json — with PUID/PGID/TZ/DATA_PATH/HTTP_PORT knobs and no
literal secrets: anything that must be set uses ${VAR:?...} so deploy
fails loudly instead of coming up with a default password. Six ship the
extra config file their app needs (prometheus.yml, Caddyfile,
mosquitto.conf, frigate config.yml, Authelia's two files,
zigbee2mqtt configuration.yaml), which the folder-copy pull already
carries into the new stack.
All 95 referenced images were verified pullable against their
registries. Default host ports were deconflicted so several templates
can be pulled side by side; the only remaining overlaps are between
services you would never run together anyway (two DNS blockers on 53,
three reverse proxies on 80/443).
The Templates page would have been an unusable 83-card grid, so it now
has a search box and tag filter chips, with the tags on each card
clickable to filter by.
Co-Authored-By: Claude Opus 5
Claude-Session: https://claude.ai/code/session_01Dk43rmEeRfYi5wsLDbmfyG
---
README.md | 14 ++-
backend/templates/actual-budget/.env.example | 3 +
backend/templates/actual-budget/compose.yaml | 11 +++
backend/templates/actual-budget/template.json | 9 ++
backend/templates/adguard-home/.env.example | 4 +
backend/templates/adguard-home/compose.yaml | 14 +++
backend/templates/adguard-home/template.json | 10 ++
backend/templates/audiobookshelf/.env.example | 5 +
backend/templates/audiobookshelf/compose.yaml | 14 +++
.../templates/audiobookshelf/template.json | 10 ++
backend/templates/authelia/.env.example | 3 +
backend/templates/authelia/compose.yaml | 13 +++
backend/templates/authelia/configuration.yml | 34 +++++++
backend/templates/authelia/template.json | 10 ++
backend/templates/authelia/users_database.yml | 10 ++
backend/templates/authentik/.env.example | 7 ++
backend/templates/authentik/compose.yaml | 61 ++++++++++++
backend/templates/authentik/template.json | 10 ++
backend/templates/bazarr/.env.example | 6 ++
backend/templates/bazarr/compose.yaml | 14 +++
backend/templates/bazarr/template.json | 10 ++
backend/templates/beszel/.env.example | 4 +
backend/templates/beszel/compose.yaml | 22 +++++
backend/templates/beszel/template.json | 10 ++
backend/templates/bookstack/.env.example | 10 ++
backend/templates/bookstack/compose.yaml | 38 ++++++++
backend/templates/bookstack/template.json | 10 ++
backend/templates/caddy/.env.example | 3 +
backend/templates/caddy/Caddyfile | 11 +++
backend/templates/caddy/compose.yaml | 13 +++
backend/templates/caddy/template.json | 9 ++
backend/templates/calibre-web/.env.example | 6 ++
backend/templates/calibre-web/compose.yaml | 16 ++++
backend/templates/calibre-web/template.json | 9 ++
.../templates/changedetection-io/.env.example | 3 +
.../templates/changedetection-io/compose.yaml | 25 +++++
.../changedetection-io/template.json | 9 ++
backend/templates/code-server/.env.example | 9 ++
backend/templates/code-server/compose.yaml | 17 ++++
backend/templates/code-server/template.json | 8 ++
backend/templates/dashy/.env.example | 2 +
backend/templates/dashy/compose.yaml | 12 +++
backend/templates/dashy/template.json | 8 ++
.../templates/docker-registry/.env.example | 4 +
.../templates/docker-registry/compose.yaml | 29 ++++++
.../templates/docker-registry/template.json | 9 ++
backend/templates/dockge/.env.example | 3 +
backend/templates/dockge/compose.yaml | 14 +++
backend/templates/dockge/template.json | 9 ++
backend/templates/dozzle/.env.example | 1 +
backend/templates/dozzle/compose.yaml | 12 +++
backend/templates/dozzle/template.json | 10 ++
backend/templates/duplicati/.env.example | 10 ++
backend/templates/duplicati/compose.yaml | 17 ++++
backend/templates/duplicati/template.json | 9 ++
backend/templates/emby/.env.example | 6 ++
backend/templates/emby/compose.yaml | 17 ++++
backend/templates/emby/template.json | 9 ++
backend/templates/esphome/.env.example | 6 ++
backend/templates/esphome/compose.yaml | 15 +++
backend/templates/esphome/template.json | 10 ++
backend/templates/filebrowser/.env.example | 5 +
backend/templates/filebrowser/compose.yaml | 16 ++++
backend/templates/filebrowser/template.json | 8 ++
backend/templates/firefly-iii/.env.example | 7 ++
backend/templates/firefly-iii/compose.yaml | 37 ++++++++
backend/templates/firefly-iii/template.json | 9 ++
backend/templates/forgejo/.env.example | 7 ++
backend/templates/forgejo/compose.yaml | 17 ++++
backend/templates/forgejo/template.json | 9 ++
backend/templates/freshrss/.env.example | 4 +
backend/templates/freshrss/compose.yaml | 13 +++
backend/templates/freshrss/template.json | 9 ++
backend/templates/frigate/.env.example | 8 ++
backend/templates/frigate/compose.yaml | 32 +++++++
backend/templates/frigate/config.yml | 38 ++++++++
backend/templates/frigate/template.json | 10 ++
backend/templates/gotify/.env.example | 6 ++
backend/templates/gotify/compose.yaml | 13 +++
backend/templates/gotify/template.json | 9 ++
backend/templates/grafana/.env.example | 8 ++
backend/templates/grafana/compose.yaml | 15 +++
backend/templates/grafana/template.json | 10 ++
backend/templates/grocy/.env.example | 5 +
backend/templates/grocy/compose.yaml | 13 +++
backend/templates/grocy/template.json | 9 ++
backend/templates/heimdall/.env.example | 6 ++
backend/templates/heimdall/compose.yaml | 14 +++
backend/templates/heimdall/template.json | 8 ++
backend/templates/homarr/.env.example | 4 +
backend/templates/homarr/compose.yaml | 14 +++
backend/templates/homarr/template.json | 8 ++
backend/templates/home-assistant/.env.example | 3 +
backend/templates/home-assistant/compose.yaml | 17 ++++
.../templates/home-assistant/template.json | 9 ++
backend/templates/homepage/.env.example | 5 +
backend/templates/homepage/compose.yaml | 15 +++
backend/templates/homepage/template.json | 8 ++
backend/templates/immich/.env.example | 11 +++
backend/templates/immich/compose.yaml | 51 ++++++++++
backend/templates/immich/template.json | 10 ++
backend/templates/it-tools/.env.example | 1 +
backend/templates/it-tools/compose.yaml | 7 ++
backend/templates/it-tools/template.json | 9 ++
backend/templates/jellyseerr/.env.example | 3 +
backend/templates/jellyseerr/compose.yaml | 12 +++
backend/templates/jellyseerr/template.json | 10 ++
backend/templates/kavita/.env.example | 4 +
backend/templates/kavita/compose.yaml | 12 +++
backend/templates/kavita/template.json | 9 ++
backend/templates/komga/.env.example | 6 ++
backend/templates/komga/compose.yaml | 13 +++
backend/templates/komga/template.json | 9 ++
backend/templates/lidarr/.env.example | 7 ++
backend/templates/lidarr/compose.yaml | 15 +++
backend/templates/lidarr/template.json | 11 +++
backend/templates/linkwarden/.env.example | 7 ++
backend/templates/linkwarden/compose.yaml | 38 ++++++++
backend/templates/linkwarden/template.json | 9 ++
backend/templates/mariadb/.env.example | 9 ++
backend/templates/mariadb/compose.yaml | 28 ++++++
backend/templates/mariadb/template.json | 9 ++
backend/templates/mealie/.env.example | 6 ++
backend/templates/mealie/compose.yaml | 19 ++++
backend/templates/mealie/template.json | 9 ++
backend/templates/memos/.env.example | 3 +
backend/templates/memos/compose.yaml | 11 +++
backend/templates/memos/template.json | 9 ++
.../templates/minecraft-server/.env.example | 11 +++
.../templates/minecraft-server/compose.yaml | 20 ++++
.../templates/minecraft-server/template.json | 8 ++
backend/templates/miniflux/.env.example | 6 ++
backend/templates/miniflux/compose.yaml | 32 +++++++
backend/templates/miniflux/template.json | 9 ++
backend/templates/minio/.env.example | 7 ++
backend/templates/minio/compose.yaml | 15 +++
backend/templates/minio/template.json | 10 ++
backend/templates/mosquitto/.env.example | 3 +
backend/templates/mosquitto/compose.yaml | 12 +++
backend/templates/mosquitto/mosquitto.conf | 13 +++
backend/templates/mosquitto/template.json | 10 ++
backend/templates/n8n/.env.example | 8 ++
backend/templates/n8n/compose.yaml | 19 ++++
backend/templates/n8n/template.json | 10 ++
backend/templates/navidrome/.env.example | 5 +
backend/templates/navidrome/compose.yaml | 15 +++
backend/templates/navidrome/template.json | 10 ++
backend/templates/netdata/.env.example | 3 +
backend/templates/netdata/compose.yaml | 26 +++++
backend/templates/netdata/template.json | 9 ++
backend/templates/nextcloud/.env.example | 9 ++
backend/templates/nextcloud/compose.yaml | 42 +++++++++
backend/templates/nextcloud/template.json | 10 ++
.../nginx-proxy-manager/.env.example | 4 +
.../nginx-proxy-manager/compose.yaml | 12 +++
.../nginx-proxy-manager/template.json | 9 ++
backend/templates/node-red/.env.example | 5 +
backend/templates/node-red/compose.yaml | 12 +++
backend/templates/node-red/template.json | 10 ++
backend/templates/ntfy/.env.example | 6 ++
backend/templates/ntfy/compose.yaml | 21 +++++
backend/templates/ntfy/template.json | 9 ++
.../templates/ollama-open-webui/.env.example | 5 +
.../templates/ollama-open-webui/compose.yaml | 31 ++++++
.../templates/ollama-open-webui/template.json | 9 ++
backend/templates/photoprism/.env.example | 10 ++
backend/templates/photoprism/compose.yaml | 41 ++++++++
backend/templates/photoprism/template.json | 9 ++
backend/templates/pihole/.env.example | 5 +
backend/templates/pihole/compose.yaml | 20 ++++
backend/templates/pihole/template.json | 10 ++
backend/templates/plex/.env.example | 7 ++
backend/templates/plex/compose.yaml | 20 ++++
backend/templates/plex/template.json | 9 ++
backend/templates/portainer/.env.example | 3 +
backend/templates/portainer/compose.yaml | 11 +++
backend/templates/portainer/template.json | 9 ++
backend/templates/postgres/.env.example | 8 ++
backend/templates/postgres/compose.yaml | 30 ++++++
backend/templates/postgres/template.json | 9 ++
backend/templates/prometheus/.env.example | 5 +
backend/templates/prometheus/compose.yaml | 39 ++++++++
backend/templates/prometheus/prometheus.yml | 16 ++++
backend/templates/prometheus/template.json | 9 ++
backend/templates/prowlarr/.env.example | 5 +
backend/templates/prowlarr/compose.yaml | 13 +++
backend/templates/prowlarr/template.json | 10 ++
backend/templates/qbittorrent/.env.example | 7 ++
backend/templates/qbittorrent/compose.yaml | 18 ++++
backend/templates/qbittorrent/template.json | 9 ++
backend/templates/radarr/.env.example | 7 ++
backend/templates/radarr/compose.yaml | 15 +++
backend/templates/radarr/template.json | 10 ++
backend/templates/redis/.env.example | 4 +
backend/templates/redis/compose.yaml | 20 ++++
backend/templates/redis/template.json | 9 ++
backend/templates/sabnzbd/.env.example | 7 ++
backend/templates/sabnzbd/compose.yaml | 15 +++
backend/templates/sabnzbd/template.json | 9 ++
backend/templates/scrutiny/.env.example | 2 +
backend/templates/scrutiny/compose.yaml | 19 ++++
backend/templates/scrutiny/template.json | 9 ++
backend/templates/searxng/.env.example | 5 +
backend/templates/searxng/compose.yaml | 20 ++++
backend/templates/searxng/template.json | 9 ++
backend/templates/sonarr/.env.example | 7 ++
backend/templates/sonarr/compose.yaml | 15 +++
backend/templates/sonarr/template.json | 10 ++
.../templates/speedtest-tracker/.env.example | 10 ++
.../templates/speedtest-tracker/compose.yaml | 20 ++++
.../templates/speedtest-tracker/template.json | 9 ++
backend/templates/stirling-pdf/.env.example | 3 +
backend/templates/stirling-pdf/compose.yaml | 13 +++
backend/templates/stirling-pdf/template.json | 9 ++
backend/templates/syncthing/.env.example | 9 ++
backend/templates/syncthing/compose.yaml | 18 ++++
backend/templates/syncthing/template.json | 10 ++
backend/templates/tailscale/.env.example | 5 +
backend/templates/tailscale/compose.yaml | 20 ++++
backend/templates/tailscale/template.json | 10 ++
backend/templates/traefik/.env.example | 6 ++
backend/templates/traefik/compose.yaml | 29 ++++++
backend/templates/traefik/template.json | 9 ++
backend/templates/trilium/.env.example | 3 +
backend/templates/trilium/compose.yaml | 11 +++
backend/templates/trilium/template.json | 9 ++
backend/templates/vikunja/.env.example | 6 ++
backend/templates/vikunja/compose.yaml | 36 +++++++
backend/templates/vikunja/template.json | 9 ++
backend/templates/watchtower/.env.example | 5 +
backend/templates/watchtower/compose.yaml | 16 ++++
backend/templates/watchtower/template.json | 9 ++
backend/templates/wg-easy/.env.example | 3 +
backend/templates/wg-easy/compose.yaml | 17 ++++
backend/templates/wg-easy/template.json | 10 ++
backend/templates/wiki-js/.env.example | 3 +
backend/templates/wiki-js/compose.yaml | 27 ++++++
backend/templates/wiki-js/template.json | 10 ++
backend/templates/zigbee2mqtt/.env.example | 4 +
backend/templates/zigbee2mqtt/compose.yaml | 18 ++++
.../templates/zigbee2mqtt/configuration.yaml | 13 +++
backend/templates/zigbee2mqtt/template.json | 10 ++
backend/version.py | 2 +-
frontend/package.json | 2 +-
frontend/src/pages/Templates.tsx | 94 +++++++++++++++++--
245 files changed, 2963 insertions(+), 14 deletions(-)
create mode 100644 backend/templates/actual-budget/.env.example
create mode 100644 backend/templates/actual-budget/compose.yaml
create mode 100644 backend/templates/actual-budget/template.json
create mode 100644 backend/templates/adguard-home/.env.example
create mode 100644 backend/templates/adguard-home/compose.yaml
create mode 100644 backend/templates/adguard-home/template.json
create mode 100644 backend/templates/audiobookshelf/.env.example
create mode 100644 backend/templates/audiobookshelf/compose.yaml
create mode 100644 backend/templates/audiobookshelf/template.json
create mode 100644 backend/templates/authelia/.env.example
create mode 100644 backend/templates/authelia/compose.yaml
create mode 100644 backend/templates/authelia/configuration.yml
create mode 100644 backend/templates/authelia/template.json
create mode 100644 backend/templates/authelia/users_database.yml
create mode 100644 backend/templates/authentik/.env.example
create mode 100644 backend/templates/authentik/compose.yaml
create mode 100644 backend/templates/authentik/template.json
create mode 100644 backend/templates/bazarr/.env.example
create mode 100644 backend/templates/bazarr/compose.yaml
create mode 100644 backend/templates/bazarr/template.json
create mode 100644 backend/templates/beszel/.env.example
create mode 100644 backend/templates/beszel/compose.yaml
create mode 100644 backend/templates/beszel/template.json
create mode 100644 backend/templates/bookstack/.env.example
create mode 100644 backend/templates/bookstack/compose.yaml
create mode 100644 backend/templates/bookstack/template.json
create mode 100644 backend/templates/caddy/.env.example
create mode 100644 backend/templates/caddy/Caddyfile
create mode 100644 backend/templates/caddy/compose.yaml
create mode 100644 backend/templates/caddy/template.json
create mode 100644 backend/templates/calibre-web/.env.example
create mode 100644 backend/templates/calibre-web/compose.yaml
create mode 100644 backend/templates/calibre-web/template.json
create mode 100644 backend/templates/changedetection-io/.env.example
create mode 100644 backend/templates/changedetection-io/compose.yaml
create mode 100644 backend/templates/changedetection-io/template.json
create mode 100644 backend/templates/code-server/.env.example
create mode 100644 backend/templates/code-server/compose.yaml
create mode 100644 backend/templates/code-server/template.json
create mode 100644 backend/templates/dashy/.env.example
create mode 100644 backend/templates/dashy/compose.yaml
create mode 100644 backend/templates/dashy/template.json
create mode 100644 backend/templates/docker-registry/.env.example
create mode 100644 backend/templates/docker-registry/compose.yaml
create mode 100644 backend/templates/docker-registry/template.json
create mode 100644 backend/templates/dockge/.env.example
create mode 100644 backend/templates/dockge/compose.yaml
create mode 100644 backend/templates/dockge/template.json
create mode 100644 backend/templates/dozzle/.env.example
create mode 100644 backend/templates/dozzle/compose.yaml
create mode 100644 backend/templates/dozzle/template.json
create mode 100644 backend/templates/duplicati/.env.example
create mode 100644 backend/templates/duplicati/compose.yaml
create mode 100644 backend/templates/duplicati/template.json
create mode 100644 backend/templates/emby/.env.example
create mode 100644 backend/templates/emby/compose.yaml
create mode 100644 backend/templates/emby/template.json
create mode 100644 backend/templates/esphome/.env.example
create mode 100644 backend/templates/esphome/compose.yaml
create mode 100644 backend/templates/esphome/template.json
create mode 100644 backend/templates/filebrowser/.env.example
create mode 100644 backend/templates/filebrowser/compose.yaml
create mode 100644 backend/templates/filebrowser/template.json
create mode 100644 backend/templates/firefly-iii/.env.example
create mode 100644 backend/templates/firefly-iii/compose.yaml
create mode 100644 backend/templates/firefly-iii/template.json
create mode 100644 backend/templates/forgejo/.env.example
create mode 100644 backend/templates/forgejo/compose.yaml
create mode 100644 backend/templates/forgejo/template.json
create mode 100644 backend/templates/freshrss/.env.example
create mode 100644 backend/templates/freshrss/compose.yaml
create mode 100644 backend/templates/freshrss/template.json
create mode 100644 backend/templates/frigate/.env.example
create mode 100644 backend/templates/frigate/compose.yaml
create mode 100644 backend/templates/frigate/config.yml
create mode 100644 backend/templates/frigate/template.json
create mode 100644 backend/templates/gotify/.env.example
create mode 100644 backend/templates/gotify/compose.yaml
create mode 100644 backend/templates/gotify/template.json
create mode 100644 backend/templates/grafana/.env.example
create mode 100644 backend/templates/grafana/compose.yaml
create mode 100644 backend/templates/grafana/template.json
create mode 100644 backend/templates/grocy/.env.example
create mode 100644 backend/templates/grocy/compose.yaml
create mode 100644 backend/templates/grocy/template.json
create mode 100644 backend/templates/heimdall/.env.example
create mode 100644 backend/templates/heimdall/compose.yaml
create mode 100644 backend/templates/heimdall/template.json
create mode 100644 backend/templates/homarr/.env.example
create mode 100644 backend/templates/homarr/compose.yaml
create mode 100644 backend/templates/homarr/template.json
create mode 100644 backend/templates/home-assistant/.env.example
create mode 100644 backend/templates/home-assistant/compose.yaml
create mode 100644 backend/templates/home-assistant/template.json
create mode 100644 backend/templates/homepage/.env.example
create mode 100644 backend/templates/homepage/compose.yaml
create mode 100644 backend/templates/homepage/template.json
create mode 100644 backend/templates/immich/.env.example
create mode 100644 backend/templates/immich/compose.yaml
create mode 100644 backend/templates/immich/template.json
create mode 100644 backend/templates/it-tools/.env.example
create mode 100644 backend/templates/it-tools/compose.yaml
create mode 100644 backend/templates/it-tools/template.json
create mode 100644 backend/templates/jellyseerr/.env.example
create mode 100644 backend/templates/jellyseerr/compose.yaml
create mode 100644 backend/templates/jellyseerr/template.json
create mode 100644 backend/templates/kavita/.env.example
create mode 100644 backend/templates/kavita/compose.yaml
create mode 100644 backend/templates/kavita/template.json
create mode 100644 backend/templates/komga/.env.example
create mode 100644 backend/templates/komga/compose.yaml
create mode 100644 backend/templates/komga/template.json
create mode 100644 backend/templates/lidarr/.env.example
create mode 100644 backend/templates/lidarr/compose.yaml
create mode 100644 backend/templates/lidarr/template.json
create mode 100644 backend/templates/linkwarden/.env.example
create mode 100644 backend/templates/linkwarden/compose.yaml
create mode 100644 backend/templates/linkwarden/template.json
create mode 100644 backend/templates/mariadb/.env.example
create mode 100644 backend/templates/mariadb/compose.yaml
create mode 100644 backend/templates/mariadb/template.json
create mode 100644 backend/templates/mealie/.env.example
create mode 100644 backend/templates/mealie/compose.yaml
create mode 100644 backend/templates/mealie/template.json
create mode 100644 backend/templates/memos/.env.example
create mode 100644 backend/templates/memos/compose.yaml
create mode 100644 backend/templates/memos/template.json
create mode 100644 backend/templates/minecraft-server/.env.example
create mode 100644 backend/templates/minecraft-server/compose.yaml
create mode 100644 backend/templates/minecraft-server/template.json
create mode 100644 backend/templates/miniflux/.env.example
create mode 100644 backend/templates/miniflux/compose.yaml
create mode 100644 backend/templates/miniflux/template.json
create mode 100644 backend/templates/minio/.env.example
create mode 100644 backend/templates/minio/compose.yaml
create mode 100644 backend/templates/minio/template.json
create mode 100644 backend/templates/mosquitto/.env.example
create mode 100644 backend/templates/mosquitto/compose.yaml
create mode 100644 backend/templates/mosquitto/mosquitto.conf
create mode 100644 backend/templates/mosquitto/template.json
create mode 100644 backend/templates/n8n/.env.example
create mode 100644 backend/templates/n8n/compose.yaml
create mode 100644 backend/templates/n8n/template.json
create mode 100644 backend/templates/navidrome/.env.example
create mode 100644 backend/templates/navidrome/compose.yaml
create mode 100644 backend/templates/navidrome/template.json
create mode 100644 backend/templates/netdata/.env.example
create mode 100644 backend/templates/netdata/compose.yaml
create mode 100644 backend/templates/netdata/template.json
create mode 100644 backend/templates/nextcloud/.env.example
create mode 100644 backend/templates/nextcloud/compose.yaml
create mode 100644 backend/templates/nextcloud/template.json
create mode 100644 backend/templates/nginx-proxy-manager/.env.example
create mode 100644 backend/templates/nginx-proxy-manager/compose.yaml
create mode 100644 backend/templates/nginx-proxy-manager/template.json
create mode 100644 backend/templates/node-red/.env.example
create mode 100644 backend/templates/node-red/compose.yaml
create mode 100644 backend/templates/node-red/template.json
create mode 100644 backend/templates/ntfy/.env.example
create mode 100644 backend/templates/ntfy/compose.yaml
create mode 100644 backend/templates/ntfy/template.json
create mode 100644 backend/templates/ollama-open-webui/.env.example
create mode 100644 backend/templates/ollama-open-webui/compose.yaml
create mode 100644 backend/templates/ollama-open-webui/template.json
create mode 100644 backend/templates/photoprism/.env.example
create mode 100644 backend/templates/photoprism/compose.yaml
create mode 100644 backend/templates/photoprism/template.json
create mode 100644 backend/templates/pihole/.env.example
create mode 100644 backend/templates/pihole/compose.yaml
create mode 100644 backend/templates/pihole/template.json
create mode 100644 backend/templates/plex/.env.example
create mode 100644 backend/templates/plex/compose.yaml
create mode 100644 backend/templates/plex/template.json
create mode 100644 backend/templates/portainer/.env.example
create mode 100644 backend/templates/portainer/compose.yaml
create mode 100644 backend/templates/portainer/template.json
create mode 100644 backend/templates/postgres/.env.example
create mode 100644 backend/templates/postgres/compose.yaml
create mode 100644 backend/templates/postgres/template.json
create mode 100644 backend/templates/prometheus/.env.example
create mode 100644 backend/templates/prometheus/compose.yaml
create mode 100644 backend/templates/prometheus/prometheus.yml
create mode 100644 backend/templates/prometheus/template.json
create mode 100644 backend/templates/prowlarr/.env.example
create mode 100644 backend/templates/prowlarr/compose.yaml
create mode 100644 backend/templates/prowlarr/template.json
create mode 100644 backend/templates/qbittorrent/.env.example
create mode 100644 backend/templates/qbittorrent/compose.yaml
create mode 100644 backend/templates/qbittorrent/template.json
create mode 100644 backend/templates/radarr/.env.example
create mode 100644 backend/templates/radarr/compose.yaml
create mode 100644 backend/templates/radarr/template.json
create mode 100644 backend/templates/redis/.env.example
create mode 100644 backend/templates/redis/compose.yaml
create mode 100644 backend/templates/redis/template.json
create mode 100644 backend/templates/sabnzbd/.env.example
create mode 100644 backend/templates/sabnzbd/compose.yaml
create mode 100644 backend/templates/sabnzbd/template.json
create mode 100644 backend/templates/scrutiny/.env.example
create mode 100644 backend/templates/scrutiny/compose.yaml
create mode 100644 backend/templates/scrutiny/template.json
create mode 100644 backend/templates/searxng/.env.example
create mode 100644 backend/templates/searxng/compose.yaml
create mode 100644 backend/templates/searxng/template.json
create mode 100644 backend/templates/sonarr/.env.example
create mode 100644 backend/templates/sonarr/compose.yaml
create mode 100644 backend/templates/sonarr/template.json
create mode 100644 backend/templates/speedtest-tracker/.env.example
create mode 100644 backend/templates/speedtest-tracker/compose.yaml
create mode 100644 backend/templates/speedtest-tracker/template.json
create mode 100644 backend/templates/stirling-pdf/.env.example
create mode 100644 backend/templates/stirling-pdf/compose.yaml
create mode 100644 backend/templates/stirling-pdf/template.json
create mode 100644 backend/templates/syncthing/.env.example
create mode 100644 backend/templates/syncthing/compose.yaml
create mode 100644 backend/templates/syncthing/template.json
create mode 100644 backend/templates/tailscale/.env.example
create mode 100644 backend/templates/tailscale/compose.yaml
create mode 100644 backend/templates/tailscale/template.json
create mode 100644 backend/templates/traefik/.env.example
create mode 100644 backend/templates/traefik/compose.yaml
create mode 100644 backend/templates/traefik/template.json
create mode 100644 backend/templates/trilium/.env.example
create mode 100644 backend/templates/trilium/compose.yaml
create mode 100644 backend/templates/trilium/template.json
create mode 100644 backend/templates/vikunja/.env.example
create mode 100644 backend/templates/vikunja/compose.yaml
create mode 100644 backend/templates/vikunja/template.json
create mode 100644 backend/templates/watchtower/.env.example
create mode 100644 backend/templates/watchtower/compose.yaml
create mode 100644 backend/templates/watchtower/template.json
create mode 100644 backend/templates/wg-easy/.env.example
create mode 100644 backend/templates/wg-easy/compose.yaml
create mode 100644 backend/templates/wg-easy/template.json
create mode 100644 backend/templates/wiki-js/.env.example
create mode 100644 backend/templates/wiki-js/compose.yaml
create mode 100644 backend/templates/wiki-js/template.json
create mode 100644 backend/templates/zigbee2mqtt/.env.example
create mode 100644 backend/templates/zigbee2mqtt/compose.yaml
create mode 100644 backend/templates/zigbee2mqtt/configuration.yaml
create mode 100644 backend/templates/zigbee2mqtt/template.json
diff --git a/README.md b/README.md
index b48897e..54c237c 100644
--- a/README.md
+++ b/README.md
@@ -76,11 +76,15 @@ as intuitive as Dockge, as capable as Portainer for Compose workflows.
(`/proc/net/tcp[6]`) and running container bindings, with a confirm dialog.
- **Resource limits**: CPU/memory sliders in the editor → `deploy.resources.limits`.
- **Template library**: each template is a ready-to-run **stack folder** in git
- (`backend/templates//` — `compose.yaml` + `.env.example` + `template.json`).
- Bundled set: Jellyfin, Vaultwarden, Uptime-Kuma, Paperless-NGX, Gitea. "Pull"
- copies the whole folder into a new stack (`.env.example` → `.env`) which you then
- edit and deploy. Save any stack back as a custom template (stored under
- `${DATA_DIR}/templates/`). Add your own by dropping a folder into the templates dir.
+ (`backend/templates//` — `compose.yaml` + `.env.example` + `template.json`,
+ plus any extra config the app needs, e.g. `prometheus.yml` or a `Caddyfile`).
+ 83 bundled homelab apps across media, *arr automation, networking, reverse
+ proxies, VPN, SSO, monitoring, dashboards, files/backup, notes and wikis, home
+ automation, dev tooling, databases, local AI and more — searchable and
+ filterable by tag on the Templates page. "Pull" copies the whole folder into a
+ new stack (`.env.example` → `.env`) which you then edit and deploy. Save any
+ stack back as a custom template (stored under `${DATA_DIR}/templates/`). Add
+ your own by dropping a folder into the templates dir.
- **Healthcheck status** surfaced per container in the stack overview.
### Phase 4 — Operations
diff --git a/backend/templates/actual-budget/.env.example b/backend/templates/actual-budget/.env.example
new file mode 100644
index 0000000..09a0c6d
--- /dev/null
+++ b/backend/templates/actual-budget/.env.example
@@ -0,0 +1,3 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/actual
+HTTP_PORT=5006
diff --git a/backend/templates/actual-budget/compose.yaml b/backend/templates/actual-budget/compose.yaml
new file mode 100644
index 0000000..49ae6b3
--- /dev/null
+++ b/backend/templates/actual-budget/compose.yaml
@@ -0,0 +1,11 @@
+services:
+ actual:
+ image: ghcr.io/actualbudget/actual-server:latest
+ container_name: actual-budget
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-5006}:5006"
+ volumes:
+ - ${DATA_PATH:-/srv/actual}:/data
diff --git a/backend/templates/actual-budget/template.json b/backend/templates/actual-budget/template.json
new file mode 100644
index 0000000..2bfd8f4
--- /dev/null
+++ b/backend/templates/actual-budget/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Actual Budget",
+ "description": "Local-first envelope budgeting with end-to-end encrypted sync across devices.",
+ "tags": [
+ "finance",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/adguard-home/.env.example b/backend/templates/adguard-home/.env.example
new file mode 100644
index 0000000..adc5bad
--- /dev/null
+++ b/backend/templates/adguard-home/.env.example
@@ -0,0 +1,4 @@
+DATA_PATH=/srv/adguardhome
+DNS_PORT=53
+SETUP_PORT=3300
+HTTP_PORT=8082
diff --git a/backend/templates/adguard-home/compose.yaml b/backend/templates/adguard-home/compose.yaml
new file mode 100644
index 0000000..5be2531
--- /dev/null
+++ b/backend/templates/adguard-home/compose.yaml
@@ -0,0 +1,14 @@
+services:
+ adguardhome:
+ image: adguard/adguardhome:latest
+ container_name: adguardhome
+ restart: unless-stopped
+ ports:
+ - "${DNS_PORT:-53}:53/tcp"
+ - "${DNS_PORT:-53}:53/udp"
+ # Setup wizard on first run; the UI moves to HTTP_PORT afterwards.
+ - "${SETUP_PORT:-3300}:3000/tcp"
+ - "${HTTP_PORT:-8082}:80/tcp"
+ volumes:
+ - ${DATA_PATH:-/srv/adguardhome}/work:/opt/adguardhome/work
+ - ${DATA_PATH:-/srv/adguardhome}/conf:/opt/adguardhome/conf
diff --git a/backend/templates/adguard-home/template.json b/backend/templates/adguard-home/template.json
new file mode 100644
index 0000000..21da9ec
--- /dev/null
+++ b/backend/templates/adguard-home/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "AdGuard Home",
+ "description": "DNS server with ad and tracker blocking, DoH/DoT and per-client rules.",
+ "tags": [
+ "network",
+ "dns",
+ "ad-blocking"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/audiobookshelf/.env.example b/backend/templates/audiobookshelf/.env.example
new file mode 100644
index 0000000..97e504e
--- /dev/null
+++ b/backend/templates/audiobookshelf/.env.example
@@ -0,0 +1,5 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/audiobookshelf
+AUDIOBOOKS_PATH=/srv/media/audiobooks
+PODCASTS_PATH=/srv/media/podcasts
+HTTP_PORT=13378
diff --git a/backend/templates/audiobookshelf/compose.yaml b/backend/templates/audiobookshelf/compose.yaml
new file mode 100644
index 0000000..3c99de1
--- /dev/null
+++ b/backend/templates/audiobookshelf/compose.yaml
@@ -0,0 +1,14 @@
+services:
+ audiobookshelf:
+ image: ghcr.io/advplyr/audiobookshelf:latest
+ container_name: audiobookshelf
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-13378}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/audiobookshelf}/config:/config
+ - ${DATA_PATH:-/srv/audiobookshelf}/metadata:/metadata
+ - ${AUDIOBOOKS_PATH:-/srv/media/audiobooks}:/audiobooks
+ - ${PODCASTS_PATH:-/srv/media/podcasts}:/podcasts
diff --git a/backend/templates/audiobookshelf/template.json b/backend/templates/audiobookshelf/template.json
new file mode 100644
index 0000000..435ffc1
--- /dev/null
+++ b/backend/templates/audiobookshelf/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Audiobookshelf",
+ "description": "Audiobook and podcast server that keeps progress in sync across devices.",
+ "tags": [
+ "media",
+ "books",
+ "streaming"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/authelia/.env.example b/backend/templates/authelia/.env.example
new file mode 100644
index 0000000..ce9fcc4
--- /dev/null
+++ b/backend/templates/authelia/.env.example
@@ -0,0 +1,3 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/authelia
+HTTP_PORT=9091
diff --git a/backend/templates/authelia/compose.yaml b/backend/templates/authelia/compose.yaml
new file mode 100644
index 0000000..7e270b7
--- /dev/null
+++ b/backend/templates/authelia/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ authelia:
+ image: authelia/authelia:latest
+ container_name: authelia
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-9091}:9091"
+ volumes:
+ - ./configuration.yml:/config/configuration.yml:ro
+ - ./users_database.yml:/config/users_database.yml
+ - ${DATA_PATH:-/srv/authelia}:/config/db
diff --git a/backend/templates/authelia/configuration.yml b/backend/templates/authelia/configuration.yml
new file mode 100644
index 0000000..072ab2e
--- /dev/null
+++ b/backend/templates/authelia/configuration.yml
@@ -0,0 +1,34 @@
+# Minimal Authelia config. Replace every "change-me" and the example domains.
+theme: dark
+
+identity_validation:
+ reset_password:
+ jwt_secret: change-me-jwt-secret
+
+server:
+ address: tcp://0.0.0.0:9091
+
+authentication_backend:
+ file:
+ path: /config/users_database.yml
+
+access_control:
+ default_policy: deny
+ rules:
+ - domain: "*.example.com"
+ policy: two_factor
+
+session:
+ secret: change-me-session-secret
+ cookies:
+ - domain: example.com
+ authelia_url: https://auth.example.com
+
+storage:
+ encryption_key: change-me-encryption-key-at-least-20-chars
+ local:
+ path: /config/db/db.sqlite3
+
+notifier:
+ filesystem:
+ filename: /config/db/notification.txt
diff --git a/backend/templates/authelia/template.json b/backend/templates/authelia/template.json
new file mode 100644
index 0000000..dd0ef12
--- /dev/null
+++ b/backend/templates/authelia/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Authelia",
+ "description": "Lightweight authentication and 2FA portal for reverse-proxy forward auth.",
+ "tags": [
+ "security",
+ "identity",
+ "sso"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/authelia/users_database.yml b/backend/templates/authelia/users_database.yml
new file mode 100644
index 0000000..1febf05
--- /dev/null
+++ b/backend/templates/authelia/users_database.yml
@@ -0,0 +1,10 @@
+# Generate a password hash with:
+# docker run --rm authelia/authelia:latest authelia crypto hash generate argon2 --password 'yourpassword'
+users:
+ admin:
+ disabled: false
+ displayname: "Admin"
+ password: "$argon2id$v=19$m=65536,t=3,p=4$REPLACE_ME"
+ email: admin@example.com
+ groups:
+ - admins
diff --git a/backend/templates/authentik/.env.example b/backend/templates/authentik/.env.example
new file mode 100644
index 0000000..72c4742
--- /dev/null
+++ b/backend/templates/authentik/.env.example
@@ -0,0 +1,7 @@
+DATA_PATH=/srv/authentik
+AUTHENTIK_TAG=2026.8.0
+HTTP_PORT=9200
+HTTPS_PORT=9243
+# Both required. Generate with: openssl rand -base64 36
+PG_PASS=change-me
+AUTHENTIK_SECRET_KEY=change-me
diff --git a/backend/templates/authentik/compose.yaml b/backend/templates/authentik/compose.yaml
new file mode 100644
index 0000000..5a4909e
--- /dev/null
+++ b/backend/templates/authentik/compose.yaml
@@ -0,0 +1,61 @@
+services:
+ postgresql:
+ image: postgres:16-alpine
+ container_name: authentik-db
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=authentik
+ - POSTGRES_USER=authentik
+ - POSTGRES_PASSWORD=${PG_PASS:?database password required}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -d authentik -U authentik"]
+ interval: 30s
+ timeout: 5s
+ retries: 5
+ start_period: 20s
+ volumes:
+ - ${DATA_PATH:-/srv/authentik}/database:/var/lib/postgresql/data
+
+ server:
+ image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2026.8.0}
+ container_name: authentik-server
+ command: server
+ restart: unless-stopped
+ shm_size: 512mb
+ depends_on:
+ postgresql:
+ condition: service_healthy
+ environment:
+ - AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY:?secret key required}
+ - AUTHENTIK_POSTGRESQL__HOST=postgresql
+ - AUTHENTIK_POSTGRESQL__NAME=authentik
+ - AUTHENTIK_POSTGRESQL__USER=authentik
+ - AUTHENTIK_POSTGRESQL__PASSWORD=${PG_PASS}
+ ports:
+ - "${HTTP_PORT:-9200}:9000"
+ - "${HTTPS_PORT:-9243}:9443"
+ volumes:
+ - ${DATA_PATH:-/srv/authentik}/data:/data
+ - ${DATA_PATH:-/srv/authentik}/custom-templates:/templates
+
+ worker:
+ image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2026.8.0}
+ container_name: authentik-worker
+ command: worker
+ restart: unless-stopped
+ shm_size: 512mb
+ user: root
+ depends_on:
+ postgresql:
+ condition: service_healthy
+ environment:
+ - AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}
+ - AUTHENTIK_POSTGRESQL__HOST=postgresql
+ - AUTHENTIK_POSTGRESQL__NAME=authentik
+ - AUTHENTIK_POSTGRESQL__USER=authentik
+ - AUTHENTIK_POSTGRESQL__PASSWORD=${PG_PASS}
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ${DATA_PATH:-/srv/authentik}/data:/data
+ - ${DATA_PATH:-/srv/authentik}/certs:/certs
+ - ${DATA_PATH:-/srv/authentik}/custom-templates:/templates
diff --git a/backend/templates/authentik/template.json b/backend/templates/authentik/template.json
new file mode 100644
index 0000000..a814da2
--- /dev/null
+++ b/backend/templates/authentik/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "authentik",
+ "description": "Identity provider and SSO gateway (OAuth2, SAML, LDAP, forward auth). Finish setup at /if/flow/initial-setup/.",
+ "tags": [
+ "security",
+ "identity",
+ "sso"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/bazarr/.env.example b/backend/templates/bazarr/.env.example
new file mode 100644
index 0000000..6a82641
--- /dev/null
+++ b/backend/templates/bazarr/.env.example
@@ -0,0 +1,6 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/bazarr
+MEDIA_PATH=/srv/media
+HTTP_PORT=6767
diff --git a/backend/templates/bazarr/compose.yaml b/backend/templates/bazarr/compose.yaml
new file mode 100644
index 0000000..46fd88d
--- /dev/null
+++ b/backend/templates/bazarr/compose.yaml
@@ -0,0 +1,14 @@
+services:
+ bazarr:
+ image: lscr.io/linuxserver/bazarr:latest
+ container_name: bazarr
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-6767}:6767"
+ volumes:
+ - ${DATA_PATH:-/srv/bazarr}:/config
+ - ${MEDIA_PATH:-/srv/media}:/media
diff --git a/backend/templates/bazarr/template.json b/backend/templates/bazarr/template.json
new file mode 100644
index 0000000..4094c23
--- /dev/null
+++ b/backend/templates/bazarr/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Bazarr",
+ "description": "Companion to Sonarr and Radarr that downloads matching subtitles.",
+ "tags": [
+ "media",
+ "automation",
+ "arr"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/beszel/.env.example b/backend/templates/beszel/.env.example
new file mode 100644
index 0000000..54eff02
--- /dev/null
+++ b/backend/templates/beszel/.env.example
@@ -0,0 +1,4 @@
+DATA_PATH=/srv/beszel
+HTTP_PORT=8090
+AGENT_PORT=45876
+AGENT_KEY=
diff --git a/backend/templates/beszel/compose.yaml b/backend/templates/beszel/compose.yaml
new file mode 100644
index 0000000..0ea37f1
--- /dev/null
+++ b/backend/templates/beszel/compose.yaml
@@ -0,0 +1,22 @@
+services:
+ beszel:
+ image: henrygd/beszel:latest
+ container_name: beszel
+ restart: unless-stopped
+ ports:
+ - "${HTTP_PORT:-8090}:8090"
+ volumes:
+ - ${DATA_PATH:-/srv/beszel}/data:/beszel_data
+
+ beszel-agent:
+ image: henrygd/beszel-agent:latest
+ container_name: beszel-agent
+ restart: unless-stopped
+ network_mode: host
+ environment:
+ - LISTEN=${AGENT_PORT:-45876}
+ # Copy this from the "add system" dialog in the Beszel UI.
+ - KEY=${AGENT_KEY:-}
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ${DATA_PATH:-/srv/beszel}/agent:/var/lib/beszel-agent
diff --git a/backend/templates/beszel/template.json b/backend/templates/beszel/template.json
new file mode 100644
index 0000000..762baa5
--- /dev/null
+++ b/backend/templates/beszel/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Beszel",
+ "description": "Lightweight server monitoring with historical charts, alerts and Docker stats.",
+ "tags": [
+ "monitoring",
+ "metrics",
+ "docker"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/bookstack/.env.example b/backend/templates/bookstack/.env.example
new file mode 100644
index 0000000..4ae0beb
--- /dev/null
+++ b/backend/templates/bookstack/.env.example
@@ -0,0 +1,10 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/bookstack
+HTTP_PORT=6875
+APP_URL=http://localhost:6875
+# Required, must be "base64:..." — generate with: echo "base64:$(openssl rand -base64 32)"
+APP_KEY=
+DB_PASSWORD=change-me
+DB_ROOT_PASSWORD=change-me
diff --git a/backend/templates/bookstack/compose.yaml b/backend/templates/bookstack/compose.yaml
new file mode 100644
index 0000000..b8ec2e5
--- /dev/null
+++ b/backend/templates/bookstack/compose.yaml
@@ -0,0 +1,38 @@
+services:
+ bookstack:
+ image: lscr.io/linuxserver/bookstack:latest
+ container_name: bookstack
+ restart: unless-stopped
+ depends_on:
+ - bookstack-db
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - APP_URL=${APP_URL:-http://localhost:6875}
+ # Required. Generate with: openssl rand -base64 32
+ - APP_KEY=${APP_KEY:?app key required}
+ - DB_HOST=bookstack-db
+ - DB_PORT=3306
+ - DB_DATABASE=bookstackapp
+ - DB_USERNAME=bookstack
+ - DB_PASSWORD=${DB_PASSWORD:-bookstack}
+ ports:
+ - "${HTTP_PORT:-6875}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/bookstack}/config:/config
+
+ bookstack-db:
+ image: lscr.io/linuxserver/mariadb:latest
+ container_name: bookstack-db
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-bookstack}
+ - MYSQL_DATABASE=bookstackapp
+ - MYSQL_USER=bookstack
+ - MYSQL_PASSWORD=${DB_PASSWORD:-bookstack}
+ volumes:
+ - ${DATA_PATH:-/srv/bookstack}/db:/config
diff --git a/backend/templates/bookstack/template.json b/backend/templates/bookstack/template.json
new file mode 100644
index 0000000..28bf788
--- /dev/null
+++ b/backend/templates/bookstack/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "BookStack",
+ "description": "Documentation platform organised into shelves, books, chapters and pages. First login: admin@admin.com / password.",
+ "tags": [
+ "documents",
+ "wiki",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/caddy/.env.example b/backend/templates/caddy/.env.example
new file mode 100644
index 0000000..857334f
--- /dev/null
+++ b/backend/templates/caddy/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/caddy
+HTTP_PORT=80
+HTTPS_PORT=443
diff --git a/backend/templates/caddy/Caddyfile b/backend/templates/caddy/Caddyfile
new file mode 100644
index 0000000..b0d29a6
--- /dev/null
+++ b/backend/templates/caddy/Caddyfile
@@ -0,0 +1,11 @@
+# Caddy issues and renews TLS certificates automatically for any real hostname.
+# Replace the examples below with your own, then restart the stack.
+
+app.example.com {
+ reverse_proxy host.docker.internal:8080
+}
+
+# Local-only site on plain HTTP (no certificate needed):
+# http://nas.lan {
+# reverse_proxy 192.168.1.10:5000
+# }
diff --git a/backend/templates/caddy/compose.yaml b/backend/templates/caddy/compose.yaml
new file mode 100644
index 0000000..16a7088
--- /dev/null
+++ b/backend/templates/caddy/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ caddy:
+ image: caddy:2-alpine
+ container_name: caddy
+ restart: unless-stopped
+ ports:
+ - "${HTTP_PORT:-80}:80"
+ - "${HTTPS_PORT:-443}:443"
+ - "${HTTPS_PORT:-443}:443/udp"
+ volumes:
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
+ - ${DATA_PATH:-/srv/caddy}/data:/data
+ - ${DATA_PATH:-/srv/caddy}/config:/config
diff --git a/backend/templates/caddy/template.json b/backend/templates/caddy/template.json
new file mode 100644
index 0000000..bd39158
--- /dev/null
+++ b/backend/templates/caddy/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Caddy",
+ "description": "Reverse proxy with automatic HTTPS, configured through a short Caddyfile.",
+ "tags": [
+ "network",
+ "proxy"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/calibre-web/.env.example b/backend/templates/calibre-web/.env.example
new file mode 100644
index 0000000..76f671b
--- /dev/null
+++ b/backend/templates/calibre-web/.env.example
@@ -0,0 +1,6 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/calibre-web
+LIBRARY_PATH=/srv/media/books
+HTTP_PORT=8083
diff --git a/backend/templates/calibre-web/compose.yaml b/backend/templates/calibre-web/compose.yaml
new file mode 100644
index 0000000..de48c33
--- /dev/null
+++ b/backend/templates/calibre-web/compose.yaml
@@ -0,0 +1,16 @@
+services:
+ calibre-web:
+ image: lscr.io/linuxserver/calibre-web:latest
+ container_name: calibre-web
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ # Adds ebook format conversion (downloads Calibre on first start)
+ - DOCKER_MODS=linuxserver/mods:universal-calibre
+ ports:
+ - "${HTTP_PORT:-8083}:8083"
+ volumes:
+ - ${DATA_PATH:-/srv/calibre-web}:/config
+ - ${LIBRARY_PATH:-/srv/media/books}:/books
diff --git a/backend/templates/calibre-web/template.json b/backend/templates/calibre-web/template.json
new file mode 100644
index 0000000..0ed99c3
--- /dev/null
+++ b/backend/templates/calibre-web/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Calibre-Web",
+ "description": "Web front-end for a Calibre ebook library, with OPDS and send-to-Kindle.",
+ "tags": [
+ "media",
+ "books"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/changedetection-io/.env.example b/backend/templates/changedetection-io/.env.example
new file mode 100644
index 0000000..78c675a
--- /dev/null
+++ b/backend/templates/changedetection-io/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/changedetection
+HTTP_PORT=5000
+BASE_URL=http://localhost:5000
diff --git a/backend/templates/changedetection-io/compose.yaml b/backend/templates/changedetection-io/compose.yaml
new file mode 100644
index 0000000..3ea38d3
--- /dev/null
+++ b/backend/templates/changedetection-io/compose.yaml
@@ -0,0 +1,25 @@
+services:
+ changedetection:
+ image: ghcr.io/dgtlmoon/changedetection.io:latest
+ container_name: changedetection
+ restart: unless-stopped
+ environment:
+ - BASE_URL=${BASE_URL:-http://localhost:5000}
+ - PLAYWRIGHT_DRIVER_URL=ws://browser:3000
+ ports:
+ - "${HTTP_PORT:-5000}:5000"
+ volumes:
+ - ${DATA_PATH:-/srv/changedetection}:/datastore
+ depends_on:
+ - browser
+
+ browser:
+ image: dgtlmoon/sockpuppetbrowser:latest
+ container_name: changedetection-browser
+ restart: unless-stopped
+ cap_add:
+ - SYS_ADMIN
+ environment:
+ - SCREEN_WIDTH=1920
+ - SCREEN_HEIGHT=1024
+ - MAX_CONCURRENT_CHROME_PROCESSES=10
diff --git a/backend/templates/changedetection-io/template.json b/backend/templates/changedetection-io/template.json
new file mode 100644
index 0000000..af296fe
--- /dev/null
+++ b/backend/templates/changedetection-io/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "changedetection.io",
+ "description": "Watches web pages for changes and notifies you — with a headless browser for JS-heavy sites.",
+ "tags": [
+ "monitoring",
+ "automation"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/code-server/.env.example b/backend/templates/code-server/.env.example
new file mode 100644
index 0000000..870f282
--- /dev/null
+++ b/backend/templates/code-server/.env.example
@@ -0,0 +1,9 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/code-server
+PROJECTS_PATH=/srv/projects
+HTTP_PORT=8443
+# Required — this is the web login password.
+PASSWORD=
+SUDO_PASSWORD=
diff --git a/backend/templates/code-server/compose.yaml b/backend/templates/code-server/compose.yaml
new file mode 100644
index 0000000..a6b6dfd
--- /dev/null
+++ b/backend/templates/code-server/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ code-server:
+ image: lscr.io/linuxserver/code-server:latest
+ container_name: code-server
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - PASSWORD=${PASSWORD:?password required}
+ - SUDO_PASSWORD=${SUDO_PASSWORD:-}
+ - DEFAULT_WORKSPACE=/config/workspace
+ ports:
+ - "${HTTP_PORT:-8443}:8443"
+ volumes:
+ - ${DATA_PATH:-/srv/code-server}:/config
+ - ${PROJECTS_PATH:-/srv/projects}:/config/workspace
diff --git a/backend/templates/code-server/template.json b/backend/templates/code-server/template.json
new file mode 100644
index 0000000..dcc15fe
--- /dev/null
+++ b/backend/templates/code-server/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "code-server",
+ "description": "VS Code in the browser, running on the server where your files already are.",
+ "tags": [
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/dashy/.env.example b/backend/templates/dashy/.env.example
new file mode 100644
index 0000000..630755f
--- /dev/null
+++ b/backend/templates/dashy/.env.example
@@ -0,0 +1,2 @@
+DATA_PATH=/srv/dashy
+HTTP_PORT=4000
diff --git a/backend/templates/dashy/compose.yaml b/backend/templates/dashy/compose.yaml
new file mode 100644
index 0000000..ad87352
--- /dev/null
+++ b/backend/templates/dashy/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ dashy:
+ image: lissy93/dashy:latest
+ container_name: dashy
+ restart: unless-stopped
+ environment:
+ - NODE_ENV=production
+ ports:
+ - "${HTTP_PORT:-4000}:8080"
+ volumes:
+ # Dashy writes conf.yml back into this directory when you edit in the UI.
+ - ${DATA_PATH:-/srv/dashy}:/app/user-data
diff --git a/backend/templates/dashy/template.json b/backend/templates/dashy/template.json
new file mode 100644
index 0000000..8ababb7
--- /dev/null
+++ b/backend/templates/dashy/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "Dashy",
+ "description": "Highly themeable dashboard with status checks, editable from the browser.",
+ "tags": [
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/docker-registry/.env.example b/backend/templates/docker-registry/.env.example
new file mode 100644
index 0000000..32ed92d
--- /dev/null
+++ b/backend/templates/docker-registry/.env.example
@@ -0,0 +1,4 @@
+DATA_PATH=/srv/registry
+HTTP_PORT=5050
+UI_PORT=5080
+REGISTRY_TITLE=Homelab Registry
diff --git a/backend/templates/docker-registry/compose.yaml b/backend/templates/docker-registry/compose.yaml
new file mode 100644
index 0000000..98e93d3
--- /dev/null
+++ b/backend/templates/docker-registry/compose.yaml
@@ -0,0 +1,29 @@
+services:
+ registry:
+ image: registry:2
+ container_name: registry
+ restart: unless-stopped
+ environment:
+ - REGISTRY_STORAGE_DELETE_ENABLED=true
+ ports:
+ - "${HTTP_PORT:-5050}:5000"
+ volumes:
+ - ${DATA_PATH:-/srv/registry}:/var/lib/registry
+ # Basic auth. Create it with:
+ # docker run --rm httpd:alpine htpasswd -Bbn user pass > htpasswd
+ # then uncomment the mount and the three REGISTRY_AUTH_* vars above.
+ # - ./htpasswd:/auth/htpasswd:ro
+
+ registry-ui:
+ image: joxit/docker-registry-ui:latest
+ container_name: registry-ui
+ restart: unless-stopped
+ depends_on:
+ - registry
+ environment:
+ - SINGLE_REGISTRY=true
+ - REGISTRY_TITLE=${REGISTRY_TITLE:-Homelab Registry}
+ - NGINX_PROXY_PASS_URL=http://registry:5000
+ - DELETE_IMAGES=true
+ ports:
+ - "${UI_PORT:-5080}:80"
diff --git a/backend/templates/docker-registry/template.json b/backend/templates/docker-registry/template.json
new file mode 100644
index 0000000..02e0c71
--- /dev/null
+++ b/backend/templates/docker-registry/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Docker Registry",
+ "description": "Private container registry with a browsable web UI. Add auth and TLS before exposing it.",
+ "tags": [
+ "docker",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/dockge/.env.example b/backend/templates/dockge/.env.example
new file mode 100644
index 0000000..55a0ad4
--- /dev/null
+++ b/backend/templates/dockge/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/dockge
+STACKS_DIR=/opt/stacks
+HTTP_PORT=5001
diff --git a/backend/templates/dockge/compose.yaml b/backend/templates/dockge/compose.yaml
new file mode 100644
index 0000000..979b0b4
--- /dev/null
+++ b/backend/templates/dockge/compose.yaml
@@ -0,0 +1,14 @@
+services:
+ dockge:
+ image: louislam/dockge:1
+ container_name: dockge
+ restart: unless-stopped
+ environment:
+ - DOCKGE_STACKS_DIR=${STACKS_DIR:-/opt/stacks}
+ ports:
+ - "${HTTP_PORT:-5001}:5001"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ${DATA_PATH:-/srv/dockge}:/app/data
+ # Host path and container path must match, or compose lookups break.
+ - ${STACKS_DIR:-/opt/stacks}:${STACKS_DIR:-/opt/stacks}
diff --git a/backend/templates/dockge/template.json b/backend/templates/dockge/template.json
new file mode 100644
index 0000000..8a755b3
--- /dev/null
+++ b/backend/templates/dockge/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Dockge",
+ "description": "Compose-stack manager with a live editor and terminal output.",
+ "tags": [
+ "docker",
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/dozzle/.env.example b/backend/templates/dozzle/.env.example
new file mode 100644
index 0000000..46bb51b
--- /dev/null
+++ b/backend/templates/dozzle/.env.example
@@ -0,0 +1 @@
+HTTP_PORT=8888
diff --git a/backend/templates/dozzle/compose.yaml b/backend/templates/dozzle/compose.yaml
new file mode 100644
index 0000000..b1b716f
--- /dev/null
+++ b/backend/templates/dozzle/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ dozzle:
+ image: amir20/dozzle:latest
+ container_name: dozzle
+ restart: unless-stopped
+ environment:
+ - DOZZLE_LEVEL=info
+ - DOZZLE_NO_ANALYTICS=true
+ ports:
+ - "${HTTP_PORT:-8888}:8080"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
diff --git a/backend/templates/dozzle/template.json b/backend/templates/dozzle/template.json
new file mode 100644
index 0000000..e5dd542
--- /dev/null
+++ b/backend/templates/dozzle/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Dozzle",
+ "description": "Live container log viewer in the browser — no log shipping, no storage.",
+ "tags": [
+ "monitoring",
+ "docker",
+ "logs"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/duplicati/.env.example b/backend/templates/duplicati/.env.example
new file mode 100644
index 0000000..9ef1670
--- /dev/null
+++ b/backend/templates/duplicati/.env.example
@@ -0,0 +1,10 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/duplicati
+BACKUPS_PATH=/srv/backups
+SOURCE_PATH=/srv
+HTTP_PORT=8201
+WEB_PASSWORD=change-me
+# Required. Generate with: openssl rand -hex 32
+SETTINGS_ENCRYPTION_KEY=
diff --git a/backend/templates/duplicati/compose.yaml b/backend/templates/duplicati/compose.yaml
new file mode 100644
index 0000000..451d004
--- /dev/null
+++ b/backend/templates/duplicati/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ duplicati:
+ image: lscr.io/linuxserver/duplicati:latest
+ container_name: duplicati
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - SETTINGS_ENCRYPTION_KEY=${SETTINGS_ENCRYPTION_KEY:?encryption key required}
+ - DUPLICATI__WEBSERVICE_PASSWORD=${WEB_PASSWORD:-change-me}
+ ports:
+ - "${HTTP_PORT:-8201}:8200"
+ volumes:
+ - ${DATA_PATH:-/srv/duplicati}:/config
+ - ${BACKUPS_PATH:-/srv/backups}:/backups
+ - ${SOURCE_PATH:-/srv}:/source:ro
diff --git a/backend/templates/duplicati/template.json b/backend/templates/duplicati/template.json
new file mode 100644
index 0000000..cf299cd
--- /dev/null
+++ b/backend/templates/duplicati/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Duplicati",
+ "description": "Scheduled, encrypted, deduplicated backups to local disks or any cloud target.",
+ "tags": [
+ "backup",
+ "files"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/emby/.env.example b/backend/templates/emby/.env.example
new file mode 100644
index 0000000..52e600c
--- /dev/null
+++ b/backend/templates/emby/.env.example
@@ -0,0 +1,6 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+CONFIG_PATH=/srv/emby/config
+MEDIA_PATH=/srv/media
+HTTP_PORT=8105
diff --git a/backend/templates/emby/compose.yaml b/backend/templates/emby/compose.yaml
new file mode 100644
index 0000000..176789a
--- /dev/null
+++ b/backend/templates/emby/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ emby:
+ image: lscr.io/linuxserver/emby:latest
+ container_name: emby
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8105}:8096"
+ volumes:
+ - ${CONFIG_PATH:-/srv/emby/config}:/config
+ - ${MEDIA_PATH:-/srv/media}:/media
+ # Uncomment for Intel/AMD hardware transcoding (Emby Premiere required):
+ # devices:
+ # - /dev/dri:/dev/dri
diff --git a/backend/templates/emby/template.json b/backend/templates/emby/template.json
new file mode 100644
index 0000000..635f3f4
--- /dev/null
+++ b/backend/templates/emby/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Emby",
+ "description": "Media server for movies, TV and music, with live TV and DVR support.",
+ "tags": [
+ "media",
+ "streaming"
+ ],
+ "gpu": "NVIDIA / Intel optional"
+}
diff --git a/backend/templates/esphome/.env.example b/backend/templates/esphome/.env.example
new file mode 100644
index 0000000..3632e96
--- /dev/null
+++ b/backend/templates/esphome/.env.example
@@ -0,0 +1,6 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/esphome
+# Optional basic auth for the dashboard.
+USERNAME=
+PASSWORD=
+# Runs on host networking — the UI is on port 6052 of the host itself.
diff --git a/backend/templates/esphome/compose.yaml b/backend/templates/esphome/compose.yaml
new file mode 100644
index 0000000..e22b288
--- /dev/null
+++ b/backend/templates/esphome/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ esphome:
+ image: ghcr.io/esphome/esphome:latest
+ container_name: esphome
+ restart: unless-stopped
+ # Host networking is needed for OTA discovery of your devices.
+ network_mode: host
+ privileged: true
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - USERNAME=${USERNAME:-}
+ - PASSWORD=${PASSWORD:-}
+ volumes:
+ - ${DATA_PATH:-/srv/esphome}:/config
+ - /etc/localtime:/etc/localtime:ro
diff --git a/backend/templates/esphome/template.json b/backend/templates/esphome/template.json
new file mode 100644
index 0000000..efd72c9
--- /dev/null
+++ b/backend/templates/esphome/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "ESPHome",
+ "description": "Build and flash firmware for ESP32/ESP8266 sensors straight from the browser.",
+ "tags": [
+ "home-automation",
+ "iot",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/filebrowser/.env.example b/backend/templates/filebrowser/.env.example
new file mode 100644
index 0000000..15864b1
--- /dev/null
+++ b/backend/templates/filebrowser/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/filebrowser
+FILES_PATH=/srv
+HTTP_PORT=8088
diff --git a/backend/templates/filebrowser/compose.yaml b/backend/templates/filebrowser/compose.yaml
new file mode 100644
index 0000000..9528b35
--- /dev/null
+++ b/backend/templates/filebrowser/compose.yaml
@@ -0,0 +1,16 @@
+services:
+ filebrowser:
+ # Pinned to the v2 line — v3 changes the on-disk layout.
+ image: filebrowser/filebrowser:v2
+ container_name: filebrowser
+ restart: unless-stopped
+ user: "${PUID:-1000}:${PGID:-1000}"
+ environment:
+ - FB_ROOT=/srv
+ - FB_DATABASE=/database/filebrowser.db
+ - FB_PORT=80
+ ports:
+ - "${HTTP_PORT:-8088}:80"
+ volumes:
+ - ${FILES_PATH:-/srv}:/srv
+ - ${DATA_PATH:-/srv/filebrowser}/database:/database
diff --git a/backend/templates/filebrowser/template.json b/backend/templates/filebrowser/template.json
new file mode 100644
index 0000000..e2597e6
--- /dev/null
+++ b/backend/templates/filebrowser/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "File Browser",
+ "description": "Web file manager for a directory tree. Default login admin/admin — change it immediately.",
+ "tags": [
+ "files"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/firefly-iii/.env.example b/backend/templates/firefly-iii/.env.example
new file mode 100644
index 0000000..676bd87
--- /dev/null
+++ b/backend/templates/firefly-iii/.env.example
@@ -0,0 +1,7 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/firefly-iii
+HTTP_PORT=8104
+APP_URL=http://localhost:8104
+# Required, exactly 32 characters. Generate with: openssl rand -hex 16
+APP_KEY=
+DB_PASSWORD=change-me
diff --git a/backend/templates/firefly-iii/compose.yaml b/backend/templates/firefly-iii/compose.yaml
new file mode 100644
index 0000000..8877fc9
--- /dev/null
+++ b/backend/templates/firefly-iii/compose.yaml
@@ -0,0 +1,37 @@
+services:
+ app:
+ image: fireflyiii/core:latest
+ container_name: firefly-iii
+ restart: unless-stopped
+ depends_on:
+ - db
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - APP_ENV=production
+ - APP_URL=${APP_URL:-http://localhost:8095}
+ # Required, exactly 32 characters. Generate with: openssl rand -hex 16
+ - APP_KEY=${APP_KEY:?app key required, exactly 32 characters}
+ - TRUSTED_PROXIES=**
+ - DB_CONNECTION=mysql
+ - DB_HOST=db
+ - DB_PORT=3306
+ - DB_DATABASE=firefly
+ - DB_USERNAME=firefly
+ - DB_PASSWORD=${DB_PASSWORD:-firefly}
+ ports:
+ - "${HTTP_PORT:-8104}:8080"
+ volumes:
+ - ${DATA_PATH:-/srv/firefly-iii}/upload:/var/www/html/storage/upload
+
+ db:
+ image: mariadb:11
+ container_name: firefly-iii-db
+ restart: unless-stopped
+ environment:
+ - MARIADB_AUTO_UPGRADE=1
+ - MYSQL_DATABASE=firefly
+ - MYSQL_USER=firefly
+ - MYSQL_PASSWORD=${DB_PASSWORD:-firefly}
+ - MYSQL_RANDOM_ROOT_PASSWORD=yes
+ volumes:
+ - ${DATA_PATH:-/srv/firefly-iii}/db:/var/lib/mysql
diff --git a/backend/templates/firefly-iii/template.json b/backend/templates/firefly-iii/template.json
new file mode 100644
index 0000000..4068ca5
--- /dev/null
+++ b/backend/templates/firefly-iii/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Firefly III",
+ "description": "Double-entry personal finance manager with budgets, rules and reports.",
+ "tags": [
+ "finance",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/forgejo/.env.example b/backend/templates/forgejo/.env.example
new file mode 100644
index 0000000..bffbaac
--- /dev/null
+++ b/backend/templates/forgejo/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/forgejo
+HTTP_PORT=3301
+SSH_PORT=2223
+ROOT_URL=http://localhost:3000/
+DOMAIN=localhost
diff --git a/backend/templates/forgejo/compose.yaml b/backend/templates/forgejo/compose.yaml
new file mode 100644
index 0000000..095d176
--- /dev/null
+++ b/backend/templates/forgejo/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ forgejo:
+ image: codeberg.org/forgejo/forgejo:16
+ container_name: forgejo
+ restart: unless-stopped
+ environment:
+ - USER_UID=${PUID:-1000}
+ - USER_GID=${PGID:-1000}
+ - FORGEJO__server__ROOT_URL=${ROOT_URL:-http://localhost:3000/}
+ - FORGEJO__server__DOMAIN=${DOMAIN:-localhost}
+ ports:
+ - "${HTTP_PORT:-3301}:3000"
+ - "${SSH_PORT:-2223}:22"
+ volumes:
+ - ${DATA_PATH:-/srv/forgejo}:/data
+ - /etc/timezone:/etc/timezone:ro
+ - /etc/localtime:/etc/localtime:ro
diff --git a/backend/templates/forgejo/template.json b/backend/templates/forgejo/template.json
new file mode 100644
index 0000000..d381d66
--- /dev/null
+++ b/backend/templates/forgejo/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Forgejo",
+ "description": "Community-run Git forge with CI (Forgejo Actions) — the Gitea fork.",
+ "tags": [
+ "git",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/freshrss/.env.example b/backend/templates/freshrss/.env.example
new file mode 100644
index 0000000..adffcd0
--- /dev/null
+++ b/backend/templates/freshrss/.env.example
@@ -0,0 +1,4 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/freshrss
+HTTP_PORT=8093
+CRON_MIN=*/20
diff --git a/backend/templates/freshrss/compose.yaml b/backend/templates/freshrss/compose.yaml
new file mode 100644
index 0000000..8e81018
--- /dev/null
+++ b/backend/templates/freshrss/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ freshrss:
+ image: freshrss/freshrss:latest
+ container_name: freshrss
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - CRON_MIN=${CRON_MIN:-*/20}
+ ports:
+ - "${HTTP_PORT:-8093}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/freshrss}/data:/var/www/FreshRSS/data
+ - ${DATA_PATH:-/srv/freshrss}/extensions:/var/www/FreshRSS/extensions
diff --git a/backend/templates/freshrss/template.json b/backend/templates/freshrss/template.json
new file mode 100644
index 0000000..d027205
--- /dev/null
+++ b/backend/templates/freshrss/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "FreshRSS",
+ "description": "Self-hosted RSS aggregator with an API for mobile readers, SQLite by default.",
+ "tags": [
+ "rss",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/frigate/.env.example b/backend/templates/frigate/.env.example
new file mode 100644
index 0000000..53e38a5
--- /dev/null
+++ b/backend/templates/frigate/.env.example
@@ -0,0 +1,8 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/frigate
+MEDIA_PATH=/srv/frigate/media
+HTTP_PORT=8971
+RTSP_PORT=8554
+WEBRTC_PORT=8555
+SHM_SIZE=256mb
+RTSP_PASSWORD=change-me
diff --git a/backend/templates/frigate/compose.yaml b/backend/templates/frigate/compose.yaml
new file mode 100644
index 0000000..6d88267
--- /dev/null
+++ b/backend/templates/frigate/compose.yaml
@@ -0,0 +1,32 @@
+services:
+ frigate:
+ image: ghcr.io/blakeblackshear/frigate:stable
+ container_name: frigate
+ restart: unless-stopped
+ privileged: true
+ shm_size: ${SHM_SIZE:-256mb}
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - FRIGATE_RTSP_PASSWORD=${RTSP_PASSWORD:-change-me}
+ ports:
+ - "${HTTP_PORT:-8971}:8971"
+ - "${RTSP_PORT:-8554}:8554"
+ - "${WEBRTC_PORT:-8555}:8555/tcp"
+ - "${WEBRTC_PORT:-8555}:8555/udp"
+ volumes:
+ # The config lives with the stack so you can edit it here; the directory
+ # underneath holds Frigate's database and model cache.
+ - ${DATA_PATH:-/srv/frigate}/config:/config
+ - ./config.yml:/config/config.yml
+ - ${MEDIA_PATH:-/srv/frigate/media}:/media/frigate
+ - /etc/localtime:/etc/localtime:ro
+ - type: tmpfs
+ target: /tmp/cache
+ tmpfs:
+ size: 1000000000
+ # Uncomment what your host actually has:
+ # devices:
+ # # Google Coral USB accelerator
+ # - /dev/bus/usb:/dev/bus/usb
+ # # Intel iGPU for hardware decoding
+ # - /dev/dri/renderD128:/dev/dri/renderD128
diff --git a/backend/templates/frigate/config.yml b/backend/templates/frigate/config.yml
new file mode 100644
index 0000000..0619e38
--- /dev/null
+++ b/backend/templates/frigate/config.yml
@@ -0,0 +1,38 @@
+mqtt:
+ enabled: false
+
+detectors:
+ cpu1:
+ type: cpu
+ # Swap for a Coral TPU once you have one:
+ # coral:
+ # type: edgetpu
+ # device: usb
+
+cameras:
+ front_door:
+ ffmpeg:
+ inputs:
+ - path: rtsp://user:{FRIGATE_RTSP_PASSWORD}@192.168.1.50:554/stream1
+ roles:
+ - detect
+ - record
+ detect:
+ width: 1280
+ height: 720
+ fps: 5
+ objects:
+ track:
+ - person
+ - car
+
+record:
+ enabled: true
+ retain:
+ days: 7
+ mode: motion
+
+snapshots:
+ enabled: true
+ retain:
+ default: 14
diff --git a/backend/templates/frigate/template.json b/backend/templates/frigate/template.json
new file mode 100644
index 0000000..cdff524
--- /dev/null
+++ b/backend/templates/frigate/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Frigate NVR",
+ "description": "Network video recorder with real-time object detection. Edit config.yml with your cameras before deploying.",
+ "tags": [
+ "home-automation",
+ "video",
+ "monitoring"
+ ],
+ "gpu": "Coral TPU / Intel / NVIDIA optional"
+}
diff --git a/backend/templates/gotify/.env.example b/backend/templates/gotify/.env.example
new file mode 100644
index 0000000..19ddcc6
--- /dev/null
+++ b/backend/templates/gotify/.env.example
@@ -0,0 +1,6 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/gotify
+HTTP_PORT=8098
+ADMIN_USER=admin
+# Required.
+ADMIN_PASSWORD=
diff --git a/backend/templates/gotify/compose.yaml b/backend/templates/gotify/compose.yaml
new file mode 100644
index 0000000..51189cb
--- /dev/null
+++ b/backend/templates/gotify/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ gotify:
+ image: gotify/server:latest
+ container_name: gotify
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - GOTIFY_DEFAULTUSER_NAME=${ADMIN_USER:-admin}
+ - GOTIFY_DEFAULTUSER_PASS=${ADMIN_PASSWORD:?admin password required}
+ ports:
+ - "${HTTP_PORT:-8098}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/gotify}:/app/data
diff --git a/backend/templates/gotify/template.json b/backend/templates/gotify/template.json
new file mode 100644
index 0000000..ec68ccb
--- /dev/null
+++ b/backend/templates/gotify/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Gotify",
+ "description": "Simple push notification server with per-application tokens and an Android app.",
+ "tags": [
+ "notifications",
+ "monitoring"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/grafana/.env.example b/backend/templates/grafana/.env.example
new file mode 100644
index 0000000..8e6b570
--- /dev/null
+++ b/backend/templates/grafana/.env.example
@@ -0,0 +1,8 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/grafana
+HTTP_PORT=3030
+ROOT_URL=http://localhost:3000
+ADMIN_USER=admin
+ADMIN_PASSWORD=change-me
+INSTALL_PLUGINS=
diff --git a/backend/templates/grafana/compose.yaml b/backend/templates/grafana/compose.yaml
new file mode 100644
index 0000000..f782cfd
--- /dev/null
+++ b/backend/templates/grafana/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ grafana:
+ image: grafana/grafana:latest
+ container_name: grafana
+ restart: unless-stopped
+ user: "${PUID:-1000}:${PGID:-1000}"
+ environment:
+ - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin}
+ - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-change-me}
+ - GF_SERVER_ROOT_URL=${ROOT_URL:-http://localhost:3000}
+ - GF_INSTALL_PLUGINS=${INSTALL_PLUGINS:-}
+ ports:
+ - "${HTTP_PORT:-3030}:3000"
+ volumes:
+ - ${DATA_PATH:-/srv/grafana}:/var/lib/grafana
diff --git a/backend/templates/grafana/template.json b/backend/templates/grafana/template.json
new file mode 100644
index 0000000..c330a4a
--- /dev/null
+++ b/backend/templates/grafana/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Grafana",
+ "description": "Dashboards and alerting on top of Prometheus, InfluxDB, Loki and friends.",
+ "tags": [
+ "monitoring",
+ "metrics",
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/grocy/.env.example b/backend/templates/grocy/.env.example
new file mode 100644
index 0000000..12f248d
--- /dev/null
+++ b/backend/templates/grocy/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/grocy
+HTTP_PORT=9283
diff --git a/backend/templates/grocy/compose.yaml b/backend/templates/grocy/compose.yaml
new file mode 100644
index 0000000..5d86f97
--- /dev/null
+++ b/backend/templates/grocy/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ grocy:
+ image: lscr.io/linuxserver/grocy:latest
+ container_name: grocy
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-9283}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/grocy}:/config
diff --git a/backend/templates/grocy/template.json b/backend/templates/grocy/template.json
new file mode 100644
index 0000000..00a1e7e
--- /dev/null
+++ b/backend/templates/grocy/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Grocy",
+ "description": "Groceries, stock, chores and household ERP with barcode scanning. First login: admin / admin.",
+ "tags": [
+ "productivity",
+ "recipes"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/heimdall/.env.example b/backend/templates/heimdall/.env.example
new file mode 100644
index 0000000..331718f
--- /dev/null
+++ b/backend/templates/heimdall/.env.example
@@ -0,0 +1,6 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/heimdall
+HTTP_PORT=8102
+HTTPS_PORT=8103
diff --git a/backend/templates/heimdall/compose.yaml b/backend/templates/heimdall/compose.yaml
new file mode 100644
index 0000000..24cfbcc
--- /dev/null
+++ b/backend/templates/heimdall/compose.yaml
@@ -0,0 +1,14 @@
+services:
+ heimdall:
+ image: lscr.io/linuxserver/heimdall:latest
+ container_name: heimdall
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8102}:80"
+ - "${HTTPS_PORT:-8103}:443"
+ volumes:
+ - ${DATA_PATH:-/srv/heimdall}:/config
diff --git a/backend/templates/heimdall/template.json b/backend/templates/heimdall/template.json
new file mode 100644
index 0000000..77635ed
--- /dev/null
+++ b/backend/templates/heimdall/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "Heimdall",
+ "description": "Simple application dashboard with enhanced tiles for common services.",
+ "tags": [
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/homarr/.env.example b/backend/templates/homarr/.env.example
new file mode 100644
index 0000000..4155c08
--- /dev/null
+++ b/backend/templates/homarr/.env.example
@@ -0,0 +1,4 @@
+DATA_PATH=/srv/homarr
+HTTP_PORT=7575
+# Required. Generate with: openssl rand -hex 32
+SECRET_ENCRYPTION_KEY=
diff --git a/backend/templates/homarr/compose.yaml b/backend/templates/homarr/compose.yaml
new file mode 100644
index 0000000..28b3e8c
--- /dev/null
+++ b/backend/templates/homarr/compose.yaml
@@ -0,0 +1,14 @@
+services:
+ homarr:
+ image: ghcr.io/homarr-labs/homarr:latest
+ container_name: homarr
+ restart: unless-stopped
+ environment:
+ # Required. Generate with: openssl rand -hex 32
+ - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY:?encryption key required}
+ ports:
+ - "${HTTP_PORT:-7575}:7575"
+ volumes:
+ - ${DATA_PATH:-/srv/homarr}:/appdata
+ # Optional: lets Homarr show container status
+ - /var/run/docker.sock:/var/run/docker.sock:ro
diff --git a/backend/templates/homarr/template.json b/backend/templates/homarr/template.json
new file mode 100644
index 0000000..2865324
--- /dev/null
+++ b/backend/templates/homarr/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "Homarr",
+ "description": "Drag-and-drop dashboard with integrations for the *arr apps, Docker and more.",
+ "tags": [
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/home-assistant/.env.example b/backend/templates/home-assistant/.env.example
new file mode 100644
index 0000000..a621ab9
--- /dev/null
+++ b/backend/templates/home-assistant/.env.example
@@ -0,0 +1,3 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/homeassistant
+# Runs on host networking — the UI is on port 8123 of the host itself.
diff --git a/backend/templates/home-assistant/compose.yaml b/backend/templates/home-assistant/compose.yaml
new file mode 100644
index 0000000..95d10f9
--- /dev/null
+++ b/backend/templates/home-assistant/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ homeassistant:
+ image: ghcr.io/home-assistant/home-assistant:stable
+ container_name: homeassistant
+ restart: unless-stopped
+ # Host networking lets HA discover devices via mDNS, SSDP and DHCP.
+ # Swap it for a ports mapping if you only use cloud/MQTT integrations.
+ network_mode: host
+ privileged: true
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ volumes:
+ - ${DATA_PATH:-/srv/homeassistant}:/config
+ - /run/dbus:/run/dbus:ro
+ # Uncomment to pass through a Zigbee/Z-Wave USB stick:
+ # devices:
+ # - /dev/ttyUSB0:/dev/ttyUSB0
diff --git a/backend/templates/home-assistant/template.json b/backend/templates/home-assistant/template.json
new file mode 100644
index 0000000..cec95d0
--- /dev/null
+++ b/backend/templates/home-assistant/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Home Assistant",
+ "description": "Home automation hub with thousands of integrations. Runs on host networking for device discovery.",
+ "tags": [
+ "home-automation",
+ "iot"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/homepage/.env.example b/backend/templates/homepage/.env.example
new file mode 100644
index 0000000..afb3da6
--- /dev/null
+++ b/backend/templates/homepage/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/homepage
+HTTP_PORT=3005
+ALLOWED_HOSTS=*
diff --git a/backend/templates/homepage/compose.yaml b/backend/templates/homepage/compose.yaml
new file mode 100644
index 0000000..e9fb7e5
--- /dev/null
+++ b/backend/templates/homepage/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ homepage:
+ image: ghcr.io/gethomepage/homepage:latest
+ container_name: homepage
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ # Comma-separated hostnames the UI may be reached under, or "*" to allow all.
+ - HOMEPAGE_ALLOWED_HOSTS=${ALLOWED_HOSTS:-*}
+ ports:
+ - "${HTTP_PORT:-3005}:3000"
+ volumes:
+ - ${DATA_PATH:-/srv/homepage}:/app/config
+ - /var/run/docker.sock:/var/run/docker.sock:ro
diff --git a/backend/templates/homepage/template.json b/backend/templates/homepage/template.json
new file mode 100644
index 0000000..7023a9c
--- /dev/null
+++ b/backend/templates/homepage/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "Homepage",
+ "description": "YAML-configured start page with live widgets for dozens of self-hosted services.",
+ "tags": [
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/immich/.env.example b/backend/templates/immich/.env.example
new file mode 100644
index 0000000..5e49a8f
--- /dev/null
+++ b/backend/templates/immich/.env.example
@@ -0,0 +1,11 @@
+TZ=Europe/Berlin
+IMMICH_VERSION=release
+DATA_PATH=/srv/immich
+# Where uploaded photos and videos live. Network shares are fine here.
+UPLOAD_LOCATION=/srv/immich/library
+# The database must sit on local storage — never on NFS/SMB.
+DB_DATA_LOCATION=/srv/immich/postgres
+DB_USERNAME=postgres
+DB_PASSWORD=change-me
+DB_DATABASE_NAME=immich
+HTTP_PORT=2283
diff --git a/backend/templates/immich/compose.yaml b/backend/templates/immich/compose.yaml
new file mode 100644
index 0000000..8f7c530
--- /dev/null
+++ b/backend/templates/immich/compose.yaml
@@ -0,0 +1,51 @@
+services:
+ immich-server:
+ image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
+ container_name: immich-server
+ restart: unless-stopped
+ depends_on:
+ - redis
+ - database
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - DB_HOSTNAME=database
+ - DB_USERNAME=${DB_USERNAME:-postgres}
+ - DB_PASSWORD=${DB_PASSWORD:-postgres}
+ - DB_DATABASE_NAME=${DB_DATABASE_NAME:-immich}
+ - REDIS_HOSTNAME=redis
+ ports:
+ - "${HTTP_PORT:-2283}:2283"
+ volumes:
+ - ${UPLOAD_LOCATION:-/srv/immich/library}:/data
+ - /etc/localtime:/etc/localtime:ro
+ # Uncomment for Intel/AMD hardware transcoding:
+ # devices:
+ # - /dev/dri:/dev/dri
+
+ immich-machine-learning:
+ # Add -cuda / -openvino / -rocm to the tag for accelerated inference.
+ image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
+ container_name: immich-machine-learning
+ restart: unless-stopped
+ volumes:
+ - ${DATA_PATH:-/srv/immich}/model-cache:/cache
+
+ redis:
+ image: valkey/valkey:8-alpine
+ container_name: immich-redis
+ restart: unless-stopped
+ healthcheck:
+ test: redis-cli ping | grep -q PONG || exit 1
+
+ database:
+ image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
+ container_name: immich-postgres
+ restart: unless-stopped
+ shm_size: 128mb
+ environment:
+ - POSTGRES_USER=${DB_USERNAME:-postgres}
+ - POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
+ - POSTGRES_DB=${DB_DATABASE_NAME:-immich}
+ - POSTGRES_INITDB_ARGS=--data-checksums
+ volumes:
+ - ${DB_DATA_LOCATION:-/srv/immich/postgres}:/var/lib/postgresql/data
diff --git a/backend/templates/immich/template.json b/backend/templates/immich/template.json
new file mode 100644
index 0000000..451ead7
--- /dev/null
+++ b/backend/templates/immich/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Immich",
+ "description": "Self-hosted photo and video backup with mobile apps and AI search.",
+ "tags": [
+ "media",
+ "photos",
+ "backup"
+ ],
+ "gpu": "NVIDIA / Intel optional"
+}
diff --git a/backend/templates/it-tools/.env.example b/backend/templates/it-tools/.env.example
new file mode 100644
index 0000000..fbe97ce
--- /dev/null
+++ b/backend/templates/it-tools/.env.example
@@ -0,0 +1 @@
+HTTP_PORT=8100
diff --git a/backend/templates/it-tools/compose.yaml b/backend/templates/it-tools/compose.yaml
new file mode 100644
index 0000000..2144b27
--- /dev/null
+++ b/backend/templates/it-tools/compose.yaml
@@ -0,0 +1,7 @@
+services:
+ it-tools:
+ image: corentinth/it-tools:latest
+ container_name: it-tools
+ restart: unless-stopped
+ ports:
+ - "${HTTP_PORT:-8100}:80"
diff --git a/backend/templates/it-tools/template.json b/backend/templates/it-tools/template.json
new file mode 100644
index 0000000..32da26d
--- /dev/null
+++ b/backend/templates/it-tools/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "IT Tools",
+ "description": "Offline collection of developer utilities — hashes, JWTs, cron parsing, converters and more.",
+ "tags": [
+ "utilities",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/jellyseerr/.env.example b/backend/templates/jellyseerr/.env.example
new file mode 100644
index 0000000..5b099d4
--- /dev/null
+++ b/backend/templates/jellyseerr/.env.example
@@ -0,0 +1,3 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/jellyseerr
+HTTP_PORT=5055
diff --git a/backend/templates/jellyseerr/compose.yaml b/backend/templates/jellyseerr/compose.yaml
new file mode 100644
index 0000000..51b681a
--- /dev/null
+++ b/backend/templates/jellyseerr/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ jellyseerr:
+ image: fallenbagel/jellyseerr:latest
+ container_name: jellyseerr
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - LOG_LEVEL=info
+ ports:
+ - "${HTTP_PORT:-5055}:5055"
+ volumes:
+ - ${DATA_PATH:-/srv/jellyseerr}:/app/config
diff --git a/backend/templates/jellyseerr/template.json b/backend/templates/jellyseerr/template.json
new file mode 100644
index 0000000..100d196
--- /dev/null
+++ b/backend/templates/jellyseerr/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Jellyseerr",
+ "description": "Request portal for Jellyfin, Emby and Plex that feeds Sonarr and Radarr.",
+ "tags": [
+ "media",
+ "automation",
+ "arr"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/kavita/.env.example b/backend/templates/kavita/.env.example
new file mode 100644
index 0000000..337ee76
--- /dev/null
+++ b/backend/templates/kavita/.env.example
@@ -0,0 +1,4 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/kavita
+LIBRARY_PATH=/srv/media/books
+HTTP_PORT=5010
diff --git a/backend/templates/kavita/compose.yaml b/backend/templates/kavita/compose.yaml
new file mode 100644
index 0000000..c53ba62
--- /dev/null
+++ b/backend/templates/kavita/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ kavita:
+ image: jvmilazz0/kavita:latest
+ container_name: kavita
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-5010}:5000"
+ volumes:
+ - ${DATA_PATH:-/srv/kavita}:/kavita/config
+ - ${LIBRARY_PATH:-/srv/media/books}:/books
diff --git a/backend/templates/kavita/template.json b/backend/templates/kavita/template.json
new file mode 100644
index 0000000..fa1a63b
--- /dev/null
+++ b/backend/templates/kavita/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Kavita",
+ "description": "Fast reading server for ebooks, manga and comics.",
+ "tags": [
+ "media",
+ "books"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/komga/.env.example b/backend/templates/komga/.env.example
new file mode 100644
index 0000000..ea2c2b4
--- /dev/null
+++ b/backend/templates/komga/.env.example
@@ -0,0 +1,6 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/komga
+COMICS_PATH=/srv/media/comics
+HTTP_PORT=25600
diff --git a/backend/templates/komga/compose.yaml b/backend/templates/komga/compose.yaml
new file mode 100644
index 0000000..2600598
--- /dev/null
+++ b/backend/templates/komga/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ komga:
+ image: gotson/komga:latest
+ container_name: komga
+ restart: unless-stopped
+ user: "${PUID:-1000}:${PGID:-1000}"
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-25600}:25600"
+ volumes:
+ - ${DATA_PATH:-/srv/komga}:/config
+ - ${COMICS_PATH:-/srv/media/comics}:/data
diff --git a/backend/templates/komga/template.json b/backend/templates/komga/template.json
new file mode 100644
index 0000000..2925989
--- /dev/null
+++ b/backend/templates/komga/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Komga",
+ "description": "Comic, manga and BD server with a web reader and OPDS feed.",
+ "tags": [
+ "media",
+ "books"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/lidarr/.env.example b/backend/templates/lidarr/.env.example
new file mode 100644
index 0000000..6b46ef2
--- /dev/null
+++ b/backend/templates/lidarr/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/lidarr
+MEDIA_PATH=/srv/media
+DOWNLOADS_PATH=/srv/downloads
+HTTP_PORT=8686
diff --git a/backend/templates/lidarr/compose.yaml b/backend/templates/lidarr/compose.yaml
new file mode 100644
index 0000000..8f4551a
--- /dev/null
+++ b/backend/templates/lidarr/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ lidarr:
+ image: lscr.io/linuxserver/lidarr:latest
+ container_name: lidarr
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8686}:8686"
+ volumes:
+ - ${DATA_PATH:-/srv/lidarr}:/config
+ - ${MEDIA_PATH:-/srv/media}:/media
+ - ${DOWNLOADS_PATH:-/srv/downloads}:/downloads
diff --git a/backend/templates/lidarr/template.json b/backend/templates/lidarr/template.json
new file mode 100644
index 0000000..7813a96
--- /dev/null
+++ b/backend/templates/lidarr/template.json
@@ -0,0 +1,11 @@
+{
+ "name": "Lidarr",
+ "description": "Automates finding, downloading and organising music.",
+ "tags": [
+ "media",
+ "music",
+ "automation",
+ "arr"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/linkwarden/.env.example b/backend/templates/linkwarden/.env.example
new file mode 100644
index 0000000..8e13a49
--- /dev/null
+++ b/backend/templates/linkwarden/.env.example
@@ -0,0 +1,7 @@
+DATA_PATH=/srv/linkwarden
+HTTP_PORT=3010
+NEXTAUTH_URL=http://localhost:3010
+POSTGRES_PASSWORD=change-me
+# Both required. Generate each with: openssl rand -hex 32
+NEXTAUTH_SECRET=
+MEILI_MASTER_KEY=
diff --git a/backend/templates/linkwarden/compose.yaml b/backend/templates/linkwarden/compose.yaml
new file mode 100644
index 0000000..6adecf2
--- /dev/null
+++ b/backend/templates/linkwarden/compose.yaml
@@ -0,0 +1,38 @@
+services:
+ linkwarden:
+ image: ghcr.io/linkwarden/linkwarden:latest
+ container_name: linkwarden
+ restart: unless-stopped
+ depends_on:
+ - postgres
+ - meilisearch
+ environment:
+ - DATABASE_URL=postgresql://linkwarden:${POSTGRES_PASSWORD:-linkwarden}@postgres:5432/linkwarden
+ - NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3010}/api/v1/auth
+ - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?auth secret required}
+ - MEILI_HOST=http://meilisearch:7700
+ - MEILI_MASTER_KEY=${MEILI_MASTER_KEY:?meilisearch key required}
+ ports:
+ - "${HTTP_PORT:-3010}:3000"
+ volumes:
+ - ${DATA_PATH:-/srv/linkwarden}/data:/data/data
+
+ postgres:
+ image: postgres:16-alpine
+ container_name: linkwarden-db
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=linkwarden
+ - POSTGRES_USER=linkwarden
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-linkwarden}
+ volumes:
+ - ${DATA_PATH:-/srv/linkwarden}/pgdata:/var/lib/postgresql/data
+
+ meilisearch:
+ image: getmeili/meilisearch:v1.13
+ container_name: linkwarden-meili
+ restart: unless-stopped
+ environment:
+ - MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
+ volumes:
+ - ${DATA_PATH:-/srv/linkwarden}/meili:/meili_data
diff --git a/backend/templates/linkwarden/template.json b/backend/templates/linkwarden/template.json
new file mode 100644
index 0000000..ac805ef
--- /dev/null
+++ b/backend/templates/linkwarden/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Linkwarden",
+ "description": "Bookmark manager that archives pages as screenshots and PDFs, with full-text search.",
+ "tags": [
+ "productivity",
+ "bookmarks"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/mariadb/.env.example b/backend/templates/mariadb/.env.example
new file mode 100644
index 0000000..9a31cae
--- /dev/null
+++ b/backend/templates/mariadb/.env.example
@@ -0,0 +1,9 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/mariadb
+DB_PORT=3306
+ADMINER_PORT=8091
+MYSQL_DATABASE=app
+MYSQL_USER=app
+# Both required.
+MYSQL_PASSWORD=
+MYSQL_ROOT_PASSWORD=
diff --git a/backend/templates/mariadb/compose.yaml b/backend/templates/mariadb/compose.yaml
new file mode 100644
index 0000000..09e890b
--- /dev/null
+++ b/backend/templates/mariadb/compose.yaml
@@ -0,0 +1,28 @@
+services:
+ mariadb:
+ image: mariadb:11
+ container_name: mariadb
+ restart: unless-stopped
+ command: --transaction-isolation=READ-COMMITTED
+ environment:
+ - MARIADB_AUTO_UPGRADE=1
+ - MARIADB_DATABASE=${MYSQL_DATABASE:-app}
+ - MARIADB_USER=${MYSQL_USER:-app}
+ - MARIADB_PASSWORD=${MYSQL_PASSWORD:?database password required}
+ - MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?root password required}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${DB_PORT:-3306}:3306"
+ volumes:
+ - ${DATA_PATH:-/srv/mariadb}:/var/lib/mysql
+
+ adminer:
+ image: adminer:latest
+ container_name: mariadb-adminer
+ restart: unless-stopped
+ depends_on:
+ - mariadb
+ environment:
+ - ADMINER_DEFAULT_SERVER=mariadb
+ ports:
+ - "${ADMINER_PORT:-8091}:8080"
diff --git a/backend/templates/mariadb/template.json b/backend/templates/mariadb/template.json
new file mode 100644
index 0000000..38fcef9
--- /dev/null
+++ b/backend/templates/mariadb/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "MariaDB + Adminer",
+ "description": "Standalone MySQL-compatible database for your own apps, with Adminer for browsing it.",
+ "tags": [
+ "database",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/mealie/.env.example b/backend/templates/mealie/.env.example
new file mode 100644
index 0000000..43054d6
--- /dev/null
+++ b/backend/templates/mealie/.env.example
@@ -0,0 +1,6 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/mealie
+HTTP_PORT=9925
+BASE_URL=http://localhost:9925
diff --git a/backend/templates/mealie/compose.yaml b/backend/templates/mealie/compose.yaml
new file mode 100644
index 0000000..ce908d3
--- /dev/null
+++ b/backend/templates/mealie/compose.yaml
@@ -0,0 +1,19 @@
+services:
+ mealie:
+ image: ghcr.io/mealie-recipes/mealie:latest
+ container_name: mealie
+ restart: unless-stopped
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - BASE_URL=${BASE_URL:-http://localhost:9925}
+ - ALLOW_SIGNUP=false
+ ports:
+ - "${HTTP_PORT:-9925}:9000"
+ volumes:
+ - ${DATA_PATH:-/srv/mealie}:/app/data
diff --git a/backend/templates/mealie/template.json b/backend/templates/mealie/template.json
new file mode 100644
index 0000000..fd4ea71
--- /dev/null
+++ b/backend/templates/mealie/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Mealie",
+ "description": "Recipe manager and meal planner that imports recipes from any URL. First login: changeme@example.com / MyPassword.",
+ "tags": [
+ "recipes",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/memos/.env.example b/backend/templates/memos/.env.example
new file mode 100644
index 0000000..80cb2db
--- /dev/null
+++ b/backend/templates/memos/.env.example
@@ -0,0 +1,3 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/memos
+HTTP_PORT=5230
diff --git a/backend/templates/memos/compose.yaml b/backend/templates/memos/compose.yaml
new file mode 100644
index 0000000..aaf8187
--- /dev/null
+++ b/backend/templates/memos/compose.yaml
@@ -0,0 +1,11 @@
+services:
+ memos:
+ image: neosmemo/memos:stable
+ container_name: memos
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-5230}:5230"
+ volumes:
+ - ${DATA_PATH:-/srv/memos}:/var/opt/memos
diff --git a/backend/templates/memos/template.json b/backend/templates/memos/template.json
new file mode 100644
index 0000000..e9c63bf
--- /dev/null
+++ b/backend/templates/memos/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Memos",
+ "description": "Lightweight, Markdown-first note taking — a self-hosted take on microblogging your thoughts.",
+ "tags": [
+ "notes",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/minecraft-server/.env.example b/backend/templates/minecraft-server/.env.example
new file mode 100644
index 0000000..1319733
--- /dev/null
+++ b/backend/templates/minecraft-server/.env.example
@@ -0,0 +1,11 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/minecraft
+GAME_PORT=25565
+# VANILLA, PAPER, FABRIC, FORGE, ...
+TYPE=PAPER
+VERSION=LATEST
+MEMORY=4G
+DIFFICULTY=normal
+MOTD=Homelab Minecraft
+# Comma-separated player names granted operator rights.
+OPS=
diff --git a/backend/templates/minecraft-server/compose.yaml b/backend/templates/minecraft-server/compose.yaml
new file mode 100644
index 0000000..50217d4
--- /dev/null
+++ b/backend/templates/minecraft-server/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ minecraft:
+ image: itzg/minecraft-server:latest
+ container_name: minecraft
+ restart: unless-stopped
+ stdin_open: true
+ tty: true
+ environment:
+ - EULA=TRUE
+ - TZ=${TZ:-Europe/Berlin}
+ - TYPE=${TYPE:-PAPER}
+ - VERSION=${VERSION:-LATEST}
+ - MEMORY=${MEMORY:-4G}
+ - DIFFICULTY=${DIFFICULTY:-normal}
+ - MOTD=${MOTD:-Homelab Minecraft}
+ - OPS=${OPS:-}
+ ports:
+ - "${GAME_PORT:-25565}:25565"
+ volumes:
+ - ${DATA_PATH:-/srv/minecraft}:/data
diff --git a/backend/templates/minecraft-server/template.json b/backend/templates/minecraft-server/template.json
new file mode 100644
index 0000000..45cfb4d
--- /dev/null
+++ b/backend/templates/minecraft-server/template.json
@@ -0,0 +1,8 @@
+{
+ "name": "Minecraft Server",
+ "description": "Java Edition server (Paper, Fabric, Forge or vanilla) with automatic version downloads.",
+ "tags": [
+ "games"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/miniflux/.env.example b/backend/templates/miniflux/.env.example
new file mode 100644
index 0000000..d96a52a
--- /dev/null
+++ b/backend/templates/miniflux/.env.example
@@ -0,0 +1,6 @@
+DATA_PATH=/srv/miniflux
+HTTP_PORT=8094
+DB_PASSWORD=change-me
+ADMIN_USER=admin
+# Required, at least 6 characters.
+ADMIN_PASSWORD=
diff --git a/backend/templates/miniflux/compose.yaml b/backend/templates/miniflux/compose.yaml
new file mode 100644
index 0000000..307b938
--- /dev/null
+++ b/backend/templates/miniflux/compose.yaml
@@ -0,0 +1,32 @@
+services:
+ miniflux:
+ image: miniflux/miniflux:latest
+ container_name: miniflux
+ restart: unless-stopped
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ - DATABASE_URL=postgres://miniflux:${DB_PASSWORD:-miniflux}@db/miniflux?sslmode=disable
+ - RUN_MIGRATIONS=1
+ - CREATE_ADMIN=1
+ - ADMIN_USERNAME=${ADMIN_USER:-admin}
+ - ADMIN_PASSWORD=${ADMIN_PASSWORD:?admin password required, min 6 chars}
+ ports:
+ - "${HTTP_PORT:-8094}:8080"
+
+ db:
+ image: postgres:16-alpine
+ container_name: miniflux-db
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=miniflux
+ - POSTGRES_USER=miniflux
+ - POSTGRES_PASSWORD=${DB_PASSWORD:-miniflux}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U miniflux"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ volumes:
+ - ${DATA_PATH:-/srv/miniflux}/db:/var/lib/postgresql/data
diff --git a/backend/templates/miniflux/template.json b/backend/templates/miniflux/template.json
new file mode 100644
index 0000000..720bb99
--- /dev/null
+++ b/backend/templates/miniflux/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Miniflux",
+ "description": "Minimalist, very fast RSS reader with a Google Reader compatible API.",
+ "tags": [
+ "rss",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/minio/.env.example b/backend/templates/minio/.env.example
new file mode 100644
index 0000000..3686b2f
--- /dev/null
+++ b/backend/templates/minio/.env.example
@@ -0,0 +1,7 @@
+DATA_PATH=/srv/minio
+API_PORT=9010
+CONSOLE_PORT=9011
+CONSOLE_URL=http://localhost:9011
+ROOT_USER=minioadmin
+# Required, at least 8 characters.
+ROOT_PASSWORD=
diff --git a/backend/templates/minio/compose.yaml b/backend/templates/minio/compose.yaml
new file mode 100644
index 0000000..0d4abae
--- /dev/null
+++ b/backend/templates/minio/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ minio:
+ image: minio/minio:latest
+ container_name: minio
+ restart: unless-stopped
+ command: server /data --console-address ":9001"
+ environment:
+ - MINIO_ROOT_USER=${ROOT_USER:-minioadmin}
+ - MINIO_ROOT_PASSWORD=${ROOT_PASSWORD:?root password required}
+ - MINIO_BROWSER_REDIRECT_URL=${CONSOLE_URL:-http://localhost:9011}
+ ports:
+ - "${API_PORT:-9010}:9000"
+ - "${CONSOLE_PORT:-9011}:9001"
+ volumes:
+ - ${DATA_PATH:-/srv/minio}:/data
diff --git a/backend/templates/minio/template.json b/backend/templates/minio/template.json
new file mode 100644
index 0000000..8a1caf3
--- /dev/null
+++ b/backend/templates/minio/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "MinIO",
+ "description": "S3-compatible object storage — a handy backup target for Restic, Duplicati and friends.",
+ "tags": [
+ "storage",
+ "backup",
+ "files"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/mosquitto/.env.example b/backend/templates/mosquitto/.env.example
new file mode 100644
index 0000000..712db9f
--- /dev/null
+++ b/backend/templates/mosquitto/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/mosquitto
+MQTT_PORT=1883
+WS_PORT=9001
diff --git a/backend/templates/mosquitto/compose.yaml b/backend/templates/mosquitto/compose.yaml
new file mode 100644
index 0000000..c2b97d7
--- /dev/null
+++ b/backend/templates/mosquitto/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ mosquitto:
+ image: eclipse-mosquitto:2
+ container_name: mosquitto
+ restart: unless-stopped
+ ports:
+ - "${MQTT_PORT:-1883}:1883"
+ - "${WS_PORT:-9001}:9001"
+ volumes:
+ - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
+ - ${DATA_PATH:-/srv/mosquitto}/data:/mosquitto/data
+ - ${DATA_PATH:-/srv/mosquitto}/log:/mosquitto/log
diff --git a/backend/templates/mosquitto/mosquitto.conf b/backend/templates/mosquitto/mosquitto.conf
new file mode 100644
index 0000000..ac66092
--- /dev/null
+++ b/backend/templates/mosquitto/mosquitto.conf
@@ -0,0 +1,13 @@
+persistence true
+persistence_location /mosquitto/data/
+log_dest stdout
+
+listener 1883
+listener 9001
+protocol websockets
+
+# Anonymous access is fine on a trusted LAN. To require credentials:
+# 1. set allow_anonymous false
+# 2. add "password_file /mosquitto/config/passwd"
+# 3. docker compose exec mosquitto mosquitto_passwd -c /mosquitto/config/passwd
+allow_anonymous true
diff --git a/backend/templates/mosquitto/template.json b/backend/templates/mosquitto/template.json
new file mode 100644
index 0000000..0d11966
--- /dev/null
+++ b/backend/templates/mosquitto/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Mosquitto",
+ "description": "MQTT broker — the message bus behind Zigbee2MQTT, ESPHome and Home Assistant.",
+ "tags": [
+ "home-automation",
+ "iot",
+ "mqtt"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/n8n/.env.example b/backend/templates/n8n/.env.example
new file mode 100644
index 0000000..6b3561f
--- /dev/null
+++ b/backend/templates/n8n/.env.example
@@ -0,0 +1,8 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/n8n
+HTTP_PORT=5678
+N8N_HOST=localhost
+N8N_PROTOCOL=http
+WEBHOOK_URL=http://localhost:5678/
+# Required. Generate with: openssl rand -hex 32
+N8N_ENCRYPTION_KEY=
diff --git a/backend/templates/n8n/compose.yaml b/backend/templates/n8n/compose.yaml
new file mode 100644
index 0000000..bd2df6c
--- /dev/null
+++ b/backend/templates/n8n/compose.yaml
@@ -0,0 +1,19 @@
+services:
+ n8n:
+ image: n8nio/n8n:latest
+ container_name: n8n
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - GENERIC_TIMEZONE=${TZ:-Europe/Berlin}
+ - N8N_HOST=${N8N_HOST:-localhost}
+ - N8N_PORT=5678
+ - N8N_PROTOCOL=${N8N_PROTOCOL:-http}
+ - WEBHOOK_URL=${WEBHOOK_URL:-http://localhost:5678/}
+ # Required. Generate with: openssl rand -hex 32
+ - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY:?encryption key required}
+ - N8N_RUNNERS_ENABLED=true
+ ports:
+ - "${HTTP_PORT:-5678}:5678"
+ volumes:
+ - ${DATA_PATH:-/srv/n8n}:/home/node/.n8n
diff --git a/backend/templates/n8n/template.json b/backend/templates/n8n/template.json
new file mode 100644
index 0000000..b382854
--- /dev/null
+++ b/backend/templates/n8n/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "n8n",
+ "description": "Workflow automation with 400+ integrations — the self-hosted answer to Zapier.",
+ "tags": [
+ "automation",
+ "dev",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/navidrome/.env.example b/backend/templates/navidrome/.env.example
new file mode 100644
index 0000000..1aee1dd
--- /dev/null
+++ b/backend/templates/navidrome/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/navidrome
+MUSIC_PATH=/srv/media/music
+HTTP_PORT=4533
diff --git a/backend/templates/navidrome/compose.yaml b/backend/templates/navidrome/compose.yaml
new file mode 100644
index 0000000..39e8126
--- /dev/null
+++ b/backend/templates/navidrome/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ navidrome:
+ image: deluan/navidrome:latest
+ container_name: navidrome
+ restart: unless-stopped
+ user: "${PUID:-1000}:${PGID:-1000}"
+ environment:
+ - ND_LOGLEVEL=info
+ - ND_SCANSCHEDULE=1h
+ - ND_SESSIONTIMEOUT=24h
+ ports:
+ - "${HTTP_PORT:-4533}:4533"
+ volumes:
+ - ${DATA_PATH:-/srv/navidrome}:/data
+ - ${MUSIC_PATH:-/srv/media/music}:/music:ro
diff --git a/backend/templates/navidrome/template.json b/backend/templates/navidrome/template.json
new file mode 100644
index 0000000..e0b2a7d
--- /dev/null
+++ b/backend/templates/navidrome/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Navidrome",
+ "description": "Music streaming server compatible with any Subsonic client.",
+ "tags": [
+ "media",
+ "music",
+ "streaming"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/netdata/.env.example b/backend/templates/netdata/.env.example
new file mode 100644
index 0000000..165b3a6
--- /dev/null
+++ b/backend/templates/netdata/.env.example
@@ -0,0 +1,3 @@
+NETDATA_HOSTNAME=homelab
+DATA_PATH=/srv/netdata
+HTTP_PORT=19999
diff --git a/backend/templates/netdata/compose.yaml b/backend/templates/netdata/compose.yaml
new file mode 100644
index 0000000..8c38f90
--- /dev/null
+++ b/backend/templates/netdata/compose.yaml
@@ -0,0 +1,26 @@
+services:
+ netdata:
+ image: netdata/netdata:latest
+ container_name: netdata
+ restart: unless-stopped
+ hostname: ${NETDATA_HOSTNAME:-homelab}
+ pid: host
+ cap_add:
+ - SYS_PTRACE
+ - SYS_ADMIN
+ security_opt:
+ - apparmor:unconfined
+ ports:
+ - "${HTTP_PORT:-19999}:19999"
+ volumes:
+ - ${DATA_PATH:-/srv/netdata}/config:/etc/netdata
+ - ${DATA_PATH:-/srv/netdata}/lib:/var/lib/netdata
+ - ${DATA_PATH:-/srv/netdata}/cache:/var/cache/netdata
+ - /etc/passwd:/host/etc/passwd:ro
+ - /etc/group:/host/etc/group:ro
+ - /etc/localtime:/etc/localtime:ro
+ - /proc:/host/proc:ro
+ - /sys:/host/sys:ro
+ - /etc/os-release:/host/etc/os-release:ro
+ - /var/log:/host/var/log:ro
+ - /var/run/docker.sock:/var/run/docker.sock:ro
diff --git a/backend/templates/netdata/template.json b/backend/templates/netdata/template.json
new file mode 100644
index 0000000..19d735b
--- /dev/null
+++ b/backend/templates/netdata/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Netdata",
+ "description": "Per-second metrics for the host and every container, with zero configuration.",
+ "tags": [
+ "monitoring",
+ "metrics"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/nextcloud/.env.example b/backend/templates/nextcloud/.env.example
new file mode 100644
index 0000000..380a012
--- /dev/null
+++ b/backend/templates/nextcloud/.env.example
@@ -0,0 +1,9 @@
+DATA_PATH=/srv/nextcloud
+FILES_PATH=/srv/nextcloud/data
+HTTP_PORT=8087
+TRUSTED_DOMAINS=localhost
+UPLOAD_LIMIT=16G
+ADMIN_USER=admin
+ADMIN_PASSWORD=change-me
+DB_PASSWORD=change-me
+DB_ROOT_PASSWORD=change-me
diff --git a/backend/templates/nextcloud/compose.yaml b/backend/templates/nextcloud/compose.yaml
new file mode 100644
index 0000000..a250b94
--- /dev/null
+++ b/backend/templates/nextcloud/compose.yaml
@@ -0,0 +1,42 @@
+services:
+ nextcloud:
+ image: nextcloud:latest
+ container_name: nextcloud
+ restart: unless-stopped
+ depends_on:
+ - db
+ - redis
+ environment:
+ - MYSQL_HOST=db
+ - MYSQL_DATABASE=nextcloud
+ - MYSQL_USER=nextcloud
+ - MYSQL_PASSWORD=${DB_PASSWORD:-nextcloud}
+ - REDIS_HOST=redis
+ - NEXTCLOUD_ADMIN_USER=${ADMIN_USER:-admin}
+ - NEXTCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD:-change-me}
+ - NEXTCLOUD_TRUSTED_DOMAINS=${TRUSTED_DOMAINS:-localhost}
+ - PHP_UPLOAD_LIMIT=${UPLOAD_LIMIT:-16G}
+ ports:
+ - "${HTTP_PORT:-8087}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/nextcloud}/html:/var/www/html
+ - ${FILES_PATH:-/srv/nextcloud/data}:/var/www/html/data
+
+ db:
+ image: mariadb:11
+ container_name: nextcloud-db
+ restart: unless-stopped
+ command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
+ environment:
+ - MARIADB_AUTO_UPGRADE=1
+ - MYSQL_DATABASE=nextcloud
+ - MYSQL_USER=nextcloud
+ - MYSQL_PASSWORD=${DB_PASSWORD:-nextcloud}
+ - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-nextcloud}
+ volumes:
+ - ${DATA_PATH:-/srv/nextcloud}/db:/var/lib/mysql
+
+ redis:
+ image: redis:7-alpine
+ container_name: nextcloud-redis
+ restart: unless-stopped
diff --git a/backend/templates/nextcloud/template.json b/backend/templates/nextcloud/template.json
new file mode 100644
index 0000000..f8726d2
--- /dev/null
+++ b/backend/templates/nextcloud/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Nextcloud",
+ "description": "File sync and share with calendar, contacts and an app store, backed by MariaDB and Redis.",
+ "tags": [
+ "files",
+ "sync",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/nginx-proxy-manager/.env.example b/backend/templates/nginx-proxy-manager/.env.example
new file mode 100644
index 0000000..57cc37c
--- /dev/null
+++ b/backend/templates/nginx-proxy-manager/.env.example
@@ -0,0 +1,4 @@
+DATA_PATH=/srv/nginx-proxy-manager
+HTTP_PORT=80
+HTTPS_PORT=443
+ADMIN_PORT=81
diff --git a/backend/templates/nginx-proxy-manager/compose.yaml b/backend/templates/nginx-proxy-manager/compose.yaml
new file mode 100644
index 0000000..4618f4d
--- /dev/null
+++ b/backend/templates/nginx-proxy-manager/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ npm:
+ image: jc21/nginx-proxy-manager:latest
+ container_name: nginx-proxy-manager
+ restart: unless-stopped
+ ports:
+ - "${HTTP_PORT:-80}:80"
+ - "${HTTPS_PORT:-443}:443"
+ - "${ADMIN_PORT:-81}:81"
+ volumes:
+ - ${DATA_PATH:-/srv/nginx-proxy-manager}/data:/data
+ - ${DATA_PATH:-/srv/nginx-proxy-manager}/letsencrypt:/etc/letsencrypt
diff --git a/backend/templates/nginx-proxy-manager/template.json b/backend/templates/nginx-proxy-manager/template.json
new file mode 100644
index 0000000..2273659
--- /dev/null
+++ b/backend/templates/nginx-proxy-manager/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Nginx Proxy Manager",
+ "description": "Reverse proxy with a click-through UI for hosts and Let's Encrypt certs. First login: admin@example.com / changeme.",
+ "tags": [
+ "network",
+ "proxy"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/node-red/.env.example b/backend/templates/node-red/.env.example
new file mode 100644
index 0000000..43f70e9
--- /dev/null
+++ b/backend/templates/node-red/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/node-red
+HTTP_PORT=1880
diff --git a/backend/templates/node-red/compose.yaml b/backend/templates/node-red/compose.yaml
new file mode 100644
index 0000000..ebaf5ce
--- /dev/null
+++ b/backend/templates/node-red/compose.yaml
@@ -0,0 +1,12 @@
+services:
+ node-red:
+ image: nodered/node-red:latest
+ container_name: node-red
+ restart: unless-stopped
+ user: "${PUID:-1000}:${PGID:-1000}"
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-1880}:1880"
+ volumes:
+ - ${DATA_PATH:-/srv/node-red}:/data
diff --git a/backend/templates/node-red/template.json b/backend/templates/node-red/template.json
new file mode 100644
index 0000000..316a6e8
--- /dev/null
+++ b/backend/templates/node-red/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Node-RED",
+ "description": "Flow-based automation wiring together MQTT, HTTP, Home Assistant and custom logic.",
+ "tags": [
+ "home-automation",
+ "automation",
+ "iot"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/ntfy/.env.example b/backend/templates/ntfy/.env.example
new file mode 100644
index 0000000..e796863
--- /dev/null
+++ b/backend/templates/ntfy/.env.example
@@ -0,0 +1,6 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/ntfy
+HTTP_PORT=8097
+BASE_URL=http://localhost:8097
+DEFAULT_ACCESS=deny-all
+BEHIND_PROXY=false
diff --git a/backend/templates/ntfy/compose.yaml b/backend/templates/ntfy/compose.yaml
new file mode 100644
index 0000000..ae15b5d
--- /dev/null
+++ b/backend/templates/ntfy/compose.yaml
@@ -0,0 +1,21 @@
+services:
+ ntfy:
+ image: binwiederhier/ntfy:latest
+ container_name: ntfy
+ restart: unless-stopped
+ command: serve
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - NTFY_BASE_URL=${BASE_URL:-http://localhost:8097}
+ # Set to true only when a reverse proxy sits in front (fixes client IPs).
+ - NTFY_BEHIND_PROXY=${BEHIND_PROXY:-false}
+ - NTFY_CACHE_FILE=/var/cache/ntfy/cache.db
+ - NTFY_AUTH_FILE=/var/lib/ntfy/user.db
+ # "read-write" is an open server; use "deny-all" plus `ntfy user add` for a private one.
+ - NTFY_AUTH_DEFAULT_ACCESS=${DEFAULT_ACCESS:-deny-all}
+ - NTFY_ENABLE_LOGIN=true
+ ports:
+ - "${HTTP_PORT:-8097}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/ntfy}/cache:/var/cache/ntfy
+ - ${DATA_PATH:-/srv/ntfy}/lib:/var/lib/ntfy
diff --git a/backend/templates/ntfy/template.json b/backend/templates/ntfy/template.json
new file mode 100644
index 0000000..c89813f
--- /dev/null
+++ b/backend/templates/ntfy/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "ntfy",
+ "description": "Push notifications to your phone from any script with a plain HTTP POST. Create users with: docker compose exec ntfy ntfy user add --role=admin ",
+ "tags": [
+ "notifications",
+ "monitoring"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/ollama-open-webui/.env.example b/backend/templates/ollama-open-webui/.env.example
new file mode 100644
index 0000000..5851360
--- /dev/null
+++ b/backend/templates/ollama-open-webui/.env.example
@@ -0,0 +1,5 @@
+DATA_PATH=/srv/ollama
+HTTP_PORT=3020
+OLLAMA_PORT=11434
+# Required. Generate with: openssl rand -hex 32
+WEBUI_SECRET_KEY=
diff --git a/backend/templates/ollama-open-webui/compose.yaml b/backend/templates/ollama-open-webui/compose.yaml
new file mode 100644
index 0000000..f14ff35
--- /dev/null
+++ b/backend/templates/ollama-open-webui/compose.yaml
@@ -0,0 +1,31 @@
+services:
+ ollama:
+ image: ollama/ollama:latest
+ container_name: ollama
+ restart: unless-stopped
+ ports:
+ - "${OLLAMA_PORT:-11434}:11434"
+ volumes:
+ - ${DATA_PATH:-/srv/ollama}/models:/root/.ollama
+ # Uncomment for NVIDIA GPUs (needs the NVIDIA Container Toolkit on the host):
+ # deploy:
+ # resources:
+ # reservations:
+ # devices:
+ # - driver: nvidia
+ # count: all
+ # capabilities: [gpu]
+
+ open-webui:
+ image: ghcr.io/open-webui/open-webui:main
+ container_name: open-webui
+ restart: unless-stopped
+ depends_on:
+ - ollama
+ environment:
+ - OLLAMA_BASE_URL=http://ollama:11434
+ - WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY:?secret key required}
+ ports:
+ - "${HTTP_PORT:-3020}:8080"
+ volumes:
+ - ${DATA_PATH:-/srv/ollama}/open-webui:/app/backend/data
diff --git a/backend/templates/ollama-open-webui/template.json b/backend/templates/ollama-open-webui/template.json
new file mode 100644
index 0000000..7f364fa
--- /dev/null
+++ b/backend/templates/ollama-open-webui/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Ollama + Open WebUI",
+ "description": "Local LLM runtime with a ChatGPT-style web front-end. Runs on CPU; far faster with a GPU.",
+ "tags": [
+ "ai",
+ "llm"
+ ],
+ "gpu": "NVIDIA optional"
+}
diff --git a/backend/templates/photoprism/.env.example b/backend/templates/photoprism/.env.example
new file mode 100644
index 0000000..8009645
--- /dev/null
+++ b/backend/templates/photoprism/.env.example
@@ -0,0 +1,10 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/photoprism
+ORIGINALS_PATH=/srv/media/photos
+HTTP_PORT=2342
+SITE_URL=http://localhost:2342/
+ADMIN_USER=admin
+ADMIN_PASSWORD=change-me
+DB_PASSWORD=change-me
+DB_ROOT_PASSWORD=change-me
diff --git a/backend/templates/photoprism/compose.yaml b/backend/templates/photoprism/compose.yaml
new file mode 100644
index 0000000..e841cb9
--- /dev/null
+++ b/backend/templates/photoprism/compose.yaml
@@ -0,0 +1,41 @@
+services:
+ photoprism:
+ image: photoprism/photoprism:latest
+ container_name: photoprism
+ restart: unless-stopped
+ depends_on:
+ - mariadb
+ security_opt:
+ - seccomp:unconfined
+ - apparmor:unconfined
+ environment:
+ - PHOTOPRISM_ADMIN_USER=${ADMIN_USER:-admin}
+ - PHOTOPRISM_ADMIN_PASSWORD=${ADMIN_PASSWORD:-change-me}
+ - PHOTOPRISM_SITE_URL=${SITE_URL:-http://localhost:2342/}
+ - PHOTOPRISM_ORIGINALS_LIMIT=5000
+ - PHOTOPRISM_DATABASE_DRIVER=mysql
+ - PHOTOPRISM_DATABASE_SERVER=mariadb:3306
+ - PHOTOPRISM_DATABASE_NAME=photoprism
+ - PHOTOPRISM_DATABASE_USER=photoprism
+ - PHOTOPRISM_DATABASE_PASSWORD=${DB_PASSWORD:-photoprism}
+ - PHOTOPRISM_UID=${PUID:-1000}
+ - PHOTOPRISM_GID=${PGID:-1000}
+ ports:
+ - "${HTTP_PORT:-2342}:2342"
+ volumes:
+ - ${ORIGINALS_PATH:-/srv/media/photos}:/photoprism/originals
+ - ${DATA_PATH:-/srv/photoprism}/storage:/photoprism/storage
+
+ mariadb:
+ image: mariadb:11
+ container_name: photoprism-db
+ restart: unless-stopped
+ command: --innodb-buffer-pool-size=512M --transaction-isolation=READ-COMMITTED
+ environment:
+ - MARIADB_AUTO_UPGRADE=1
+ - MARIADB_DATABASE=photoprism
+ - MARIADB_USER=photoprism
+ - MARIADB_PASSWORD=${DB_PASSWORD:-photoprism}
+ - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-photoprism}
+ volumes:
+ - ${DATA_PATH:-/srv/photoprism}/mariadb:/var/lib/mysql
diff --git a/backend/templates/photoprism/template.json b/backend/templates/photoprism/template.json
new file mode 100644
index 0000000..2b47048
--- /dev/null
+++ b/backend/templates/photoprism/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "PhotoPrism",
+ "description": "AI-powered photo library that tags and indexes your originals.",
+ "tags": [
+ "media",
+ "photos"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/pihole/.env.example b/backend/templates/pihole/.env.example
new file mode 100644
index 0000000..2b9c441
--- /dev/null
+++ b/backend/templates/pihole/.env.example
@@ -0,0 +1,5 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/pihole
+DNS_PORT=53
+HTTP_PORT=8081
+WEB_PASSWORD=change-me
diff --git a/backend/templates/pihole/compose.yaml b/backend/templates/pihole/compose.yaml
new file mode 100644
index 0000000..f983d76
--- /dev/null
+++ b/backend/templates/pihole/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ pihole:
+ image: pihole/pihole:latest
+ container_name: pihole
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - FTLCONF_webserver_api_password=${WEB_PASSWORD:-change-me}
+ - FTLCONF_dns_listeningMode=all
+ ports:
+ - "${DNS_PORT:-53}:53/tcp"
+ - "${DNS_PORT:-53}:53/udp"
+ - "${HTTP_PORT:-8081}:80/tcp"
+ volumes:
+ - ${DATA_PATH:-/srv/pihole}/etc-pihole:/etc/pihole
+ cap_add:
+ # Only required when Pi-hole also serves DHCP
+ - NET_ADMIN
+ - SYS_TIME
+ - SYS_NICE
diff --git a/backend/templates/pihole/template.json b/backend/templates/pihole/template.json
new file mode 100644
index 0000000..747d2c5
--- /dev/null
+++ b/backend/templates/pihole/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Pi-hole",
+ "description": "Network-wide DNS ad blocker. Port 53 must be free — disable systemd-resolved's stub listener first.",
+ "tags": [
+ "network",
+ "dns",
+ "ad-blocking"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/plex/.env.example b/backend/templates/plex/.env.example
new file mode 100644
index 0000000..b43a153
--- /dev/null
+++ b/backend/templates/plex/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+CONFIG_PATH=/srv/plex/config
+MEDIA_PATH=/srv/media
+HTTP_PORT=32400
+PLEX_CLAIM=
diff --git a/backend/templates/plex/compose.yaml b/backend/templates/plex/compose.yaml
new file mode 100644
index 0000000..524ab4a
--- /dev/null
+++ b/backend/templates/plex/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ plex:
+ image: lscr.io/linuxserver/plex:latest
+ container_name: plex
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - VERSION=docker
+ # Grab a claim token at https://plex.tv/claim (valid 4 minutes)
+ - PLEX_CLAIM=${PLEX_CLAIM:-}
+ ports:
+ - "${HTTP_PORT:-32400}:32400"
+ volumes:
+ - ${CONFIG_PATH:-/srv/plex/config}:/config
+ - ${MEDIA_PATH:-/srv/media}:/media
+ # Uncomment for Intel/AMD hardware transcoding (Plex Pass required):
+ # devices:
+ # - /dev/dri:/dev/dri
diff --git a/backend/templates/plex/template.json b/backend/templates/plex/template.json
new file mode 100644
index 0000000..e19c72d
--- /dev/null
+++ b/backend/templates/plex/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Plex",
+ "description": "Media server for movies, TV and music with polished client apps.",
+ "tags": [
+ "media",
+ "streaming"
+ ],
+ "gpu": "NVIDIA / Intel optional"
+}
diff --git a/backend/templates/portainer/.env.example b/backend/templates/portainer/.env.example
new file mode 100644
index 0000000..a9e2d4f
--- /dev/null
+++ b/backend/templates/portainer/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/portainer
+HTTP_PORT=9000
+HTTPS_PORT=9443
diff --git a/backend/templates/portainer/compose.yaml b/backend/templates/portainer/compose.yaml
new file mode 100644
index 0000000..4f4eae9
--- /dev/null
+++ b/backend/templates/portainer/compose.yaml
@@ -0,0 +1,11 @@
+services:
+ portainer:
+ image: portainer/portainer-ce:latest
+ container_name: portainer
+ restart: unless-stopped
+ ports:
+ - "${HTTPS_PORT:-9443}:9443"
+ - "${HTTP_PORT:-9000}:9000"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ${DATA_PATH:-/srv/portainer}:/data
diff --git a/backend/templates/portainer/template.json b/backend/templates/portainer/template.json
new file mode 100644
index 0000000..9502e72
--- /dev/null
+++ b/backend/templates/portainer/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Portainer CE",
+ "description": "Docker management UI. The admin account must be created within a few minutes of first start.",
+ "tags": [
+ "docker",
+ "dashboard"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/postgres/.env.example b/backend/templates/postgres/.env.example
new file mode 100644
index 0000000..9914925
--- /dev/null
+++ b/backend/templates/postgres/.env.example
@@ -0,0 +1,8 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/postgres
+DB_PORT=5432
+ADMINER_PORT=8089
+POSTGRES_DB=app
+POSTGRES_USER=app
+# Required.
+POSTGRES_PASSWORD=
diff --git a/backend/templates/postgres/compose.yaml b/backend/templates/postgres/compose.yaml
new file mode 100644
index 0000000..d45354a
--- /dev/null
+++ b/backend/templates/postgres/compose.yaml
@@ -0,0 +1,30 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: postgres
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=${POSTGRES_DB:-app}
+ - POSTGRES_USER=${POSTGRES_USER:-app}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?database password required}
+ - TZ=${TZ:-Europe/Berlin}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ ports:
+ - "${DB_PORT:-5432}:5432"
+ volumes:
+ - ${DATA_PATH:-/srv/postgres}:/var/lib/postgresql/data
+
+ adminer:
+ image: adminer:latest
+ container_name: postgres-adminer
+ restart: unless-stopped
+ depends_on:
+ - postgres
+ environment:
+ - ADMINER_DEFAULT_SERVER=postgres
+ ports:
+ - "${ADMINER_PORT:-8089}:8080"
diff --git a/backend/templates/postgres/template.json b/backend/templates/postgres/template.json
new file mode 100644
index 0000000..63f78b6
--- /dev/null
+++ b/backend/templates/postgres/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "PostgreSQL + Adminer",
+ "description": "Standalone Postgres for your own apps, with Adminer for browsing it.",
+ "tags": [
+ "database",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/prometheus/.env.example b/backend/templates/prometheus/.env.example
new file mode 100644
index 0000000..bf80799
--- /dev/null
+++ b/backend/templates/prometheus/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+DATA_PATH=/srv/prometheus
+HTTP_PORT=9090
+RETENTION=30d
diff --git a/backend/templates/prometheus/compose.yaml b/backend/templates/prometheus/compose.yaml
new file mode 100644
index 0000000..626a1ae
--- /dev/null
+++ b/backend/templates/prometheus/compose.yaml
@@ -0,0 +1,39 @@
+services:
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: prometheus
+ restart: unless-stopped
+ user: "${PUID:-1000}:${PGID:-1000}"
+ command:
+ - --config.file=/etc/prometheus/prometheus.yml
+ - --storage.tsdb.path=/prometheus
+ - --storage.tsdb.retention.time=${RETENTION:-30d}
+ - --web.enable-lifecycle
+ ports:
+ - "${HTTP_PORT:-9090}:9090"
+ volumes:
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - ${DATA_PATH:-/srv/prometheus}:/prometheus
+
+ node-exporter:
+ image: prom/node-exporter:latest
+ container_name: node-exporter
+ restart: unless-stopped
+ pid: host
+ command:
+ - --path.rootfs=/host
+ volumes:
+ - /:/host:ro,rslave
+
+ cadvisor:
+ image: gcr.io/cadvisor/cadvisor:latest
+ container_name: cadvisor
+ restart: unless-stopped
+ privileged: true
+ devices:
+ - /dev/kmsg
+ volumes:
+ - /:/rootfs:ro
+ - /var/run:/var/run:ro
+ - /sys:/sys:ro
+ - /var/lib/docker/:/var/lib/docker:ro
diff --git a/backend/templates/prometheus/prometheus.yml b/backend/templates/prometheus/prometheus.yml
new file mode 100644
index 0000000..415d585
--- /dev/null
+++ b/backend/templates/prometheus/prometheus.yml
@@ -0,0 +1,16 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: prometheus
+ static_configs:
+ - targets: ["localhost:9090"]
+
+ - job_name: node
+ static_configs:
+ - targets: ["node-exporter:9100"]
+
+ - job_name: cadvisor
+ static_configs:
+ - targets: ["cadvisor:8080"]
diff --git a/backend/templates/prometheus/template.json b/backend/templates/prometheus/template.json
new file mode 100644
index 0000000..0ba49a5
--- /dev/null
+++ b/backend/templates/prometheus/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Prometheus",
+ "description": "Metrics database with node-exporter and cAdvisor already wired up for host and container stats.",
+ "tags": [
+ "monitoring",
+ "metrics"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/prowlarr/.env.example b/backend/templates/prowlarr/.env.example
new file mode 100644
index 0000000..68e6d16
--- /dev/null
+++ b/backend/templates/prowlarr/.env.example
@@ -0,0 +1,5 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/prowlarr
+HTTP_PORT=9696
diff --git a/backend/templates/prowlarr/compose.yaml b/backend/templates/prowlarr/compose.yaml
new file mode 100644
index 0000000..aa6772e
--- /dev/null
+++ b/backend/templates/prowlarr/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ prowlarr:
+ image: lscr.io/linuxserver/prowlarr:latest
+ container_name: prowlarr
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-9696}:9696"
+ volumes:
+ - ${DATA_PATH:-/srv/prowlarr}:/config
diff --git a/backend/templates/prowlarr/template.json b/backend/templates/prowlarr/template.json
new file mode 100644
index 0000000..9ad645f
--- /dev/null
+++ b/backend/templates/prowlarr/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Prowlarr",
+ "description": "Indexer manager that feeds trackers and usenet indexers to the *arr apps.",
+ "tags": [
+ "media",
+ "automation",
+ "arr"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/qbittorrent/.env.example b/backend/templates/qbittorrent/.env.example
new file mode 100644
index 0000000..0323a26
--- /dev/null
+++ b/backend/templates/qbittorrent/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/qbittorrent
+DOWNLOADS_PATH=/srv/downloads
+HTTP_PORT=8080
+TORRENT_PORT=6881
diff --git a/backend/templates/qbittorrent/compose.yaml b/backend/templates/qbittorrent/compose.yaml
new file mode 100644
index 0000000..030c299
--- /dev/null
+++ b/backend/templates/qbittorrent/compose.yaml
@@ -0,0 +1,18 @@
+services:
+ qbittorrent:
+ image: lscr.io/linuxserver/qbittorrent:latest
+ container_name: qbittorrent
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ - WEBUI_PORT=${HTTP_PORT:-8080}
+ - TORRENTING_PORT=${TORRENT_PORT:-6881}
+ ports:
+ - "${HTTP_PORT:-8080}:${HTTP_PORT:-8080}"
+ - "${TORRENT_PORT:-6881}:${TORRENT_PORT:-6881}"
+ - "${TORRENT_PORT:-6881}:${TORRENT_PORT:-6881}/udp"
+ volumes:
+ - ${DATA_PATH:-/srv/qbittorrent}:/config
+ - ${DOWNLOADS_PATH:-/srv/downloads}:/downloads
diff --git a/backend/templates/qbittorrent/template.json b/backend/templates/qbittorrent/template.json
new file mode 100644
index 0000000..8b52309
--- /dev/null
+++ b/backend/templates/qbittorrent/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "qBittorrent",
+ "description": "BitTorrent client with a web UI. The temporary admin password is printed to the container log on first start.",
+ "tags": [
+ "downloads",
+ "media"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/radarr/.env.example b/backend/templates/radarr/.env.example
new file mode 100644
index 0000000..0845023
--- /dev/null
+++ b/backend/templates/radarr/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/radarr
+MEDIA_PATH=/srv/media
+DOWNLOADS_PATH=/srv/downloads
+HTTP_PORT=7878
diff --git a/backend/templates/radarr/compose.yaml b/backend/templates/radarr/compose.yaml
new file mode 100644
index 0000000..ad266a3
--- /dev/null
+++ b/backend/templates/radarr/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ radarr:
+ image: lscr.io/linuxserver/radarr:latest
+ container_name: radarr
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-7878}:7878"
+ volumes:
+ - ${DATA_PATH:-/srv/radarr}:/config
+ - ${MEDIA_PATH:-/srv/media}:/media
+ - ${DOWNLOADS_PATH:-/srv/downloads}:/downloads
diff --git a/backend/templates/radarr/template.json b/backend/templates/radarr/template.json
new file mode 100644
index 0000000..bd52e4b
--- /dev/null
+++ b/backend/templates/radarr/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Radarr",
+ "description": "Automates finding, downloading and organising movies.",
+ "tags": [
+ "media",
+ "automation",
+ "arr"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/redis/.env.example b/backend/templates/redis/.env.example
new file mode 100644
index 0000000..e0a5254
--- /dev/null
+++ b/backend/templates/redis/.env.example
@@ -0,0 +1,4 @@
+DATA_PATH=/srv/redis
+REDIS_PORT=6379
+# Required.
+REDIS_PASSWORD=
diff --git a/backend/templates/redis/compose.yaml b/backend/templates/redis/compose.yaml
new file mode 100644
index 0000000..ac9aa99
--- /dev/null
+++ b/backend/templates/redis/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ redis:
+ image: redis:7-alpine
+ container_name: redis
+ restart: unless-stopped
+ environment:
+ - REDIS_PASSWORD=${REDIS_PASSWORD:?redis password required}
+ command: >
+ redis-server
+ --appendonly yes
+ --requirepass ${REDIS_PASSWORD}
+ healthcheck:
+ test: ["CMD-SHELL", "redis-cli --no-auth-warning -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ ports:
+ - "${REDIS_PORT:-6379}:6379"
+ volumes:
+ - ${DATA_PATH:-/srv/redis}:/data
diff --git a/backend/templates/redis/template.json b/backend/templates/redis/template.json
new file mode 100644
index 0000000..7620d11
--- /dev/null
+++ b/backend/templates/redis/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Redis",
+ "description": "In-memory cache and queue with append-only persistence, for apps that need one.",
+ "tags": [
+ "database",
+ "dev"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/sabnzbd/.env.example b/backend/templates/sabnzbd/.env.example
new file mode 100644
index 0000000..e729be9
--- /dev/null
+++ b/backend/templates/sabnzbd/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/sabnzbd
+DOWNLOADS_PATH=/srv/downloads
+INCOMPLETE_PATH=/srv/downloads/incomplete
+HTTP_PORT=8880
diff --git a/backend/templates/sabnzbd/compose.yaml b/backend/templates/sabnzbd/compose.yaml
new file mode 100644
index 0000000..bfd55fb
--- /dev/null
+++ b/backend/templates/sabnzbd/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ sabnzbd:
+ image: lscr.io/linuxserver/sabnzbd:latest
+ container_name: sabnzbd
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8880}:8080"
+ volumes:
+ - ${DATA_PATH:-/srv/sabnzbd}:/config
+ - ${DOWNLOADS_PATH:-/srv/downloads}:/downloads
+ - ${INCOMPLETE_PATH:-/srv/downloads/incomplete}:/incomplete-downloads
diff --git a/backend/templates/sabnzbd/template.json b/backend/templates/sabnzbd/template.json
new file mode 100644
index 0000000..33042a2
--- /dev/null
+++ b/backend/templates/sabnzbd/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "SABnzbd",
+ "description": "Usenet binary downloader that hands finished jobs to the *arr apps.",
+ "tags": [
+ "downloads",
+ "media"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/scrutiny/.env.example b/backend/templates/scrutiny/.env.example
new file mode 100644
index 0000000..76f4b1e
--- /dev/null
+++ b/backend/templates/scrutiny/.env.example
@@ -0,0 +1,2 @@
+DATA_PATH=/srv/scrutiny
+HTTP_PORT=8085
diff --git a/backend/templates/scrutiny/compose.yaml b/backend/templates/scrutiny/compose.yaml
new file mode 100644
index 0000000..8243283
--- /dev/null
+++ b/backend/templates/scrutiny/compose.yaml
@@ -0,0 +1,19 @@
+services:
+ scrutiny:
+ image: ghcr.io/analogj/scrutiny:master-omnibus
+ container_name: scrutiny
+ restart: unless-stopped
+ cap_add:
+ - SYS_RAWIO
+ ports:
+ - "${HTTP_PORT:-8085}:8080"
+ volumes:
+ - /run/udev:/run/udev:ro
+ - ${DATA_PATH:-/srv/scrutiny}/config:/opt/scrutiny/config
+ - ${DATA_PATH:-/srv/scrutiny}/influxdb:/opt/scrutiny/influxdb
+ devices:
+ # List every disk you want monitored.
+ - /dev/sda:/dev/sda
+ # - /dev/sdb:/dev/sdb
+ # NVMe drives additionally need: cap_add: [SYS_ADMIN]
+ # - /dev/nvme0:/dev/nvme0
diff --git a/backend/templates/scrutiny/template.json b/backend/templates/scrutiny/template.json
new file mode 100644
index 0000000..13f0e6a
--- /dev/null
+++ b/backend/templates/scrutiny/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Scrutiny",
+ "description": "S.M.A.R.T. disk health dashboard with failure-rate context. Edit the devices list before deploying.",
+ "tags": [
+ "monitoring",
+ "storage"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/searxng/.env.example b/backend/templates/searxng/.env.example
new file mode 100644
index 0000000..7b19185
--- /dev/null
+++ b/backend/templates/searxng/.env.example
@@ -0,0 +1,5 @@
+DATA_PATH=/srv/searxng
+HTTP_PORT=8099
+BASE_URL=http://localhost:8099
+# Required. Generate with: openssl rand -hex 32
+SEARXNG_SECRET=
diff --git a/backend/templates/searxng/compose.yaml b/backend/templates/searxng/compose.yaml
new file mode 100644
index 0000000..9109db5
--- /dev/null
+++ b/backend/templates/searxng/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ searxng:
+ image: searxng/searxng:latest
+ container_name: searxng
+ restart: unless-stopped
+ environment:
+ - SEARXNG_BASE_URL=${BASE_URL:-http://localhost:8099}/
+ - SEARXNG_SECRET=${SEARXNG_SECRET:?secret required}
+ - UWSGI_WORKERS=4
+ - UWSGI_THREADS=4
+ ports:
+ - "${HTTP_PORT:-8099}:8080"
+ volumes:
+ - ${DATA_PATH:-/srv/searxng}:/etc/searxng
+ cap_drop:
+ - ALL
+ cap_add:
+ - CHOWN
+ - SETGID
+ - SETUID
diff --git a/backend/templates/searxng/template.json b/backend/templates/searxng/template.json
new file mode 100644
index 0000000..20d3f26
--- /dev/null
+++ b/backend/templates/searxng/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "SearXNG",
+ "description": "Private meta search engine that queries dozens of engines without tracking you.",
+ "tags": [
+ "search",
+ "privacy"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/sonarr/.env.example b/backend/templates/sonarr/.env.example
new file mode 100644
index 0000000..1641fc7
--- /dev/null
+++ b/backend/templates/sonarr/.env.example
@@ -0,0 +1,7 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/sonarr
+MEDIA_PATH=/srv/media
+DOWNLOADS_PATH=/srv/downloads
+HTTP_PORT=8989
diff --git a/backend/templates/sonarr/compose.yaml b/backend/templates/sonarr/compose.yaml
new file mode 100644
index 0000000..2d2c9e6
--- /dev/null
+++ b/backend/templates/sonarr/compose.yaml
@@ -0,0 +1,15 @@
+services:
+ sonarr:
+ image: lscr.io/linuxserver/sonarr:latest
+ container_name: sonarr
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8989}:8989"
+ volumes:
+ - ${DATA_PATH:-/srv/sonarr}:/config
+ - ${MEDIA_PATH:-/srv/media}:/media
+ - ${DOWNLOADS_PATH:-/srv/downloads}:/downloads
diff --git a/backend/templates/sonarr/template.json b/backend/templates/sonarr/template.json
new file mode 100644
index 0000000..7754695
--- /dev/null
+++ b/backend/templates/sonarr/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Sonarr",
+ "description": "Automates finding, downloading and organising TV series.",
+ "tags": [
+ "media",
+ "automation",
+ "arr"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/speedtest-tracker/.env.example b/backend/templates/speedtest-tracker/.env.example
new file mode 100644
index 0000000..f53c1a7
--- /dev/null
+++ b/backend/templates/speedtest-tracker/.env.example
@@ -0,0 +1,10 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+DATA_PATH=/srv/speedtest-tracker
+HTTP_PORT=8086
+APP_URL=http://localhost:8086
+# Required. Generate with: openssl rand -base64 32
+APP_KEY=
+SPEEDTEST_SCHEDULE=0 */6 * * *
+PRUNE_DAYS=90
diff --git a/backend/templates/speedtest-tracker/compose.yaml b/backend/templates/speedtest-tracker/compose.yaml
new file mode 100644
index 0000000..af49460
--- /dev/null
+++ b/backend/templates/speedtest-tracker/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ speedtest-tracker:
+ image: lscr.io/linuxserver/speedtest-tracker:latest
+ container_name: speedtest-tracker
+ restart: unless-stopped
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ # Generate with: openssl rand -base64 32
+ - APP_KEY=${APP_KEY:?app key required}
+ - APP_URL=${APP_URL:-http://localhost:8086}
+ - DB_CONNECTION=sqlite
+ - SPEEDTEST_SCHEDULE=${SPEEDTEST_SCHEDULE:-0 */6 * * *}
+ - DISPLAY_TIMEZONE=${TZ:-Europe/Berlin}
+ - PRUNE_RESULTS_OLDER_THAN=${PRUNE_DAYS:-90}
+ ports:
+ - "${HTTP_PORT:-8086}:80"
+ volumes:
+ - ${DATA_PATH:-/srv/speedtest-tracker}:/config
diff --git a/backend/templates/speedtest-tracker/template.json b/backend/templates/speedtest-tracker/template.json
new file mode 100644
index 0000000..9caa551
--- /dev/null
+++ b/backend/templates/speedtest-tracker/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Speedtest Tracker",
+ "description": "Runs scheduled internet speed tests and charts the history. First login: admin@example.com / password.",
+ "tags": [
+ "monitoring",
+ "network"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/stirling-pdf/.env.example b/backend/templates/stirling-pdf/.env.example
new file mode 100644
index 0000000..920a06e
--- /dev/null
+++ b/backend/templates/stirling-pdf/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/stirling-pdf
+HTTP_PORT=8092
+LANGS=en_GB,de_DE
diff --git a/backend/templates/stirling-pdf/compose.yaml b/backend/templates/stirling-pdf/compose.yaml
new file mode 100644
index 0000000..b90f442
--- /dev/null
+++ b/backend/templates/stirling-pdf/compose.yaml
@@ -0,0 +1,13 @@
+services:
+ stirling-pdf:
+ image: ghcr.io/stirling-tools/stirling-pdf:latest
+ container_name: stirling-pdf
+ restart: unless-stopped
+ environment:
+ - DOCKER_ENABLE_SECURITY=false
+ - LANGS=${LANGS:-en_GB,de_DE}
+ ports:
+ - "${HTTP_PORT:-8092}:8080"
+ volumes:
+ - ${DATA_PATH:-/srv/stirling-pdf}/configs:/configs
+ - ${DATA_PATH:-/srv/stirling-pdf}/tessdata:/usr/share/tessdata
diff --git a/backend/templates/stirling-pdf/template.json b/backend/templates/stirling-pdf/template.json
new file mode 100644
index 0000000..b3fcca2
--- /dev/null
+++ b/backend/templates/stirling-pdf/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Stirling PDF",
+ "description": "Split, merge, OCR, sign and convert PDFs entirely in your own browser session.",
+ "tags": [
+ "documents",
+ "utilities"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/syncthing/.env.example b/backend/templates/syncthing/.env.example
new file mode 100644
index 0000000..0272e3a
--- /dev/null
+++ b/backend/templates/syncthing/.env.example
@@ -0,0 +1,9 @@
+PUID=1000
+PGID=1000
+TZ=Europe/Berlin
+SYNC_HOSTNAME=homelab
+DATA_PATH=/srv/syncthing
+SYNC_PATH=/srv/sync
+HTTP_PORT=8384
+SYNC_PORT=22000
+DISCOVERY_PORT=21027
diff --git a/backend/templates/syncthing/compose.yaml b/backend/templates/syncthing/compose.yaml
new file mode 100644
index 0000000..806aff0
--- /dev/null
+++ b/backend/templates/syncthing/compose.yaml
@@ -0,0 +1,18 @@
+services:
+ syncthing:
+ image: lscr.io/linuxserver/syncthing:latest
+ container_name: syncthing
+ restart: unless-stopped
+ hostname: ${SYNC_HOSTNAME:-homelab}
+ environment:
+ - PUID=${PUID:-1000}
+ - PGID=${PGID:-1000}
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8384}:8384"
+ - "${SYNC_PORT:-22000}:22000/tcp"
+ - "${SYNC_PORT:-22000}:22000/udp"
+ - "${DISCOVERY_PORT:-21027}:21027/udp"
+ volumes:
+ - ${DATA_PATH:-/srv/syncthing}:/config
+ - ${SYNC_PATH:-/srv/sync}:/data
diff --git a/backend/templates/syncthing/template.json b/backend/templates/syncthing/template.json
new file mode 100644
index 0000000..0a979e5
--- /dev/null
+++ b/backend/templates/syncthing/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Syncthing",
+ "description": "Continuous peer-to-peer file sync between your own devices — no server in the middle.",
+ "tags": [
+ "files",
+ "sync",
+ "backup"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/tailscale/.env.example b/backend/templates/tailscale/.env.example
new file mode 100644
index 0000000..f1873c2
--- /dev/null
+++ b/backend/templates/tailscale/.env.example
@@ -0,0 +1,5 @@
+DATA_PATH=/srv/tailscale
+TS_HOSTNAME=homelab
+TS_AUTHKEY=
+TS_ROUTES=
+TS_EXTRA_ARGS=--accept-dns=false
diff --git a/backend/templates/tailscale/compose.yaml b/backend/templates/tailscale/compose.yaml
new file mode 100644
index 0000000..f1bfc18
--- /dev/null
+++ b/backend/templates/tailscale/compose.yaml
@@ -0,0 +1,20 @@
+services:
+ tailscale:
+ image: tailscale/tailscale:latest
+ container_name: tailscale
+ restart: unless-stopped
+ hostname: ${TS_HOSTNAME:-homelab}
+ network_mode: host
+ environment:
+ # Generate at https://login.tailscale.com/admin/settings/keys
+ - TS_AUTHKEY=${TS_AUTHKEY:-}
+ - TS_STATE_DIR=/var/lib/tailscale
+ # Advertise your LAN so tailnet clients can reach it, e.g. 192.168.1.0/24
+ - TS_ROUTES=${TS_ROUTES:-}
+ - TS_EXTRA_ARGS=${TS_EXTRA_ARGS:---accept-dns=false}
+ volumes:
+ - ${DATA_PATH:-/srv/tailscale}:/var/lib/tailscale
+ - /dev/net/tun:/dev/net/tun
+ cap_add:
+ - NET_ADMIN
+ - NET_RAW
diff --git a/backend/templates/tailscale/template.json b/backend/templates/tailscale/template.json
new file mode 100644
index 0000000..ea186b5
--- /dev/null
+++ b/backend/templates/tailscale/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Tailscale",
+ "description": "Mesh VPN node, optionally advertising your LAN as a subnet router.",
+ "tags": [
+ "network",
+ "vpn",
+ "security"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/traefik/.env.example b/backend/templates/traefik/.env.example
new file mode 100644
index 0000000..25cacfd
--- /dev/null
+++ b/backend/templates/traefik/.env.example
@@ -0,0 +1,6 @@
+DATA_PATH=/srv/traefik
+HTTP_PORT=80
+HTTPS_PORT=443
+DASHBOARD_PORT=8180
+DASHBOARD_HOST=traefik.example.com
+ACME_EMAIL=admin@example.com
diff --git a/backend/templates/traefik/compose.yaml b/backend/templates/traefik/compose.yaml
new file mode 100644
index 0000000..2c8f8cb
--- /dev/null
+++ b/backend/templates/traefik/compose.yaml
@@ -0,0 +1,29 @@
+services:
+ traefik:
+ image: traefik:v3
+ container_name: traefik
+ restart: unless-stopped
+ command:
+ - --api.dashboard=true
+ - --providers.docker=true
+ - --providers.docker.exposedbydefault=false
+ - --entrypoints.web.address=:80
+ - --entrypoints.web.http.redirections.entrypoint.to=websecure
+ - --entrypoints.web.http.redirections.entrypoint.scheme=https
+ - --entrypoints.websecure.address=:443
+ - --certificatesresolvers.le.acme.email=${ACME_EMAIL:-admin@example.com}
+ - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
+ - --certificatesresolvers.le.acme.tlschallenge=true
+ ports:
+ - "${HTTP_PORT:-80}:80"
+ - "${HTTPS_PORT:-443}:443"
+ - "${DASHBOARD_PORT:-8180}:8080"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ${DATA_PATH:-/srv/traefik}/letsencrypt:/letsencrypt
+ labels:
+ - traefik.enable=true
+ - traefik.http.routers.dashboard.rule=Host(`${DASHBOARD_HOST:-traefik.example.com}`)
+ - traefik.http.routers.dashboard.service=api@internal
+ - traefik.http.routers.dashboard.entrypoints=websecure
+ - traefik.http.routers.dashboard.tls.certresolver=le
diff --git a/backend/templates/traefik/template.json b/backend/templates/traefik/template.json
new file mode 100644
index 0000000..354d0d5
--- /dev/null
+++ b/backend/templates/traefik/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Traefik",
+ "description": "Reverse proxy that discovers containers via Docker labels and issues Let's Encrypt certificates.",
+ "tags": [
+ "network",
+ "proxy"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/trilium/.env.example b/backend/templates/trilium/.env.example
new file mode 100644
index 0000000..7a9e05d
--- /dev/null
+++ b/backend/templates/trilium/.env.example
@@ -0,0 +1,3 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/trilium
+HTTP_PORT=8101
diff --git a/backend/templates/trilium/compose.yaml b/backend/templates/trilium/compose.yaml
new file mode 100644
index 0000000..cdbfa77
--- /dev/null
+++ b/backend/templates/trilium/compose.yaml
@@ -0,0 +1,11 @@
+services:
+ trilium:
+ image: triliumnext/trilium:latest
+ container_name: trilium
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8101}:8080"
+ volumes:
+ - ${DATA_PATH:-/srv/trilium}:/home/node/trilium-data
diff --git a/backend/templates/trilium/template.json b/backend/templates/trilium/template.json
new file mode 100644
index 0000000..d1d98ff
--- /dev/null
+++ b/backend/templates/trilium/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Trilium Notes",
+ "description": "Hierarchical note taking for large personal knowledge bases, with sync and scripting.",
+ "tags": [
+ "notes",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/vikunja/.env.example b/backend/templates/vikunja/.env.example
new file mode 100644
index 0000000..710eb87
--- /dev/null
+++ b/backend/templates/vikunja/.env.example
@@ -0,0 +1,6 @@
+DATA_PATH=/srv/vikunja
+HTTP_PORT=3456
+PUBLIC_URL=http://localhost:3456
+DB_PASSWORD=change-me
+# Required. Generate with: openssl rand -hex 32
+SERVICE_SECRET=
diff --git a/backend/templates/vikunja/compose.yaml b/backend/templates/vikunja/compose.yaml
new file mode 100644
index 0000000..26dda03
--- /dev/null
+++ b/backend/templates/vikunja/compose.yaml
@@ -0,0 +1,36 @@
+services:
+ vikunja:
+ image: vikunja/vikunja:latest
+ container_name: vikunja
+ restart: unless-stopped
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ - VIKUNJA_SERVICE_PUBLICURL=${PUBLIC_URL:-http://localhost:3456}
+ - VIKUNJA_SERVICE_SECRET=${SERVICE_SECRET:?service secret required}
+ - VIKUNJA_DATABASE_TYPE=postgres
+ - VIKUNJA_DATABASE_HOST=db
+ - VIKUNJA_DATABASE_DATABASE=vikunja
+ - VIKUNJA_DATABASE_USER=vikunja
+ - VIKUNJA_DATABASE_PASSWORD=${DB_PASSWORD:-vikunja}
+ ports:
+ - "${HTTP_PORT:-3456}:3456"
+ volumes:
+ - ${DATA_PATH:-/srv/vikunja}/files:/app/vikunja/files
+
+ db:
+ image: postgres:16-alpine
+ container_name: vikunja-db
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=vikunja
+ - POSTGRES_USER=vikunja
+ - POSTGRES_PASSWORD=${DB_PASSWORD:-vikunja}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U vikunja"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ volumes:
+ - ${DATA_PATH:-/srv/vikunja}/db:/var/lib/postgresql/data
diff --git a/backend/templates/vikunja/template.json b/backend/templates/vikunja/template.json
new file mode 100644
index 0000000..68f1ecf
--- /dev/null
+++ b/backend/templates/vikunja/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Vikunja",
+ "description": "To-do and project management with list, gantt, table and kanban views.",
+ "tags": [
+ "productivity",
+ "tasks"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/watchtower/.env.example b/backend/templates/watchtower/.env.example
new file mode 100644
index 0000000..569ab1d
--- /dev/null
+++ b/backend/templates/watchtower/.env.example
@@ -0,0 +1,5 @@
+TZ=Europe/Berlin
+# Six-field cron (seconds first). Default: every day at 04:00.
+SCHEDULE=0 0 4 * * *
+MONITOR_ONLY=false
+NOTIFICATION_URL=
diff --git a/backend/templates/watchtower/compose.yaml b/backend/templates/watchtower/compose.yaml
new file mode 100644
index 0000000..228793b
--- /dev/null
+++ b/backend/templates/watchtower/compose.yaml
@@ -0,0 +1,16 @@
+services:
+ watchtower:
+ image: containrrr/watchtower:latest
+ container_name: watchtower
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ - WATCHTOWER_CLEANUP=true
+ - WATCHTOWER_INCLUDE_RESTARTING=true
+ - WATCHTOWER_SCHEDULE=${SCHEDULE:-0 0 4 * * *}
+ # Report only, never pull: set to true for a dry run.
+ - WATCHTOWER_MONITOR_ONLY=${MONITOR_ONLY:-false}
+ # e.g. shoutrrr URL for ntfy/Gotify/Discord notifications
+ - WATCHTOWER_NOTIFICATION_URL=${NOTIFICATION_URL:-}
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
diff --git a/backend/templates/watchtower/template.json b/backend/templates/watchtower/template.json
new file mode 100644
index 0000000..9d60e33
--- /dev/null
+++ b/backend/templates/watchtower/template.json
@@ -0,0 +1,9 @@
+{
+ "name": "Watchtower",
+ "description": "Watches running containers and pulls new images on a schedule.",
+ "tags": [
+ "docker",
+ "automation"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/wg-easy/.env.example b/backend/templates/wg-easy/.env.example
new file mode 100644
index 0000000..9a4e9e7
--- /dev/null
+++ b/backend/templates/wg-easy/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/wg-easy
+WG_PORT=51820
+HTTP_PORT=51821
diff --git a/backend/templates/wg-easy/compose.yaml b/backend/templates/wg-easy/compose.yaml
new file mode 100644
index 0000000..dd81308
--- /dev/null
+++ b/backend/templates/wg-easy/compose.yaml
@@ -0,0 +1,17 @@
+services:
+ wg-easy:
+ image: ghcr.io/wg-easy/wg-easy:15
+ container_name: wg-easy
+ restart: unless-stopped
+ ports:
+ - "${WG_PORT:-51820}:51820/udp"
+ - "${HTTP_PORT:-51821}:51821/tcp"
+ volumes:
+ - ${DATA_PATH:-/srv/wg-easy}:/etc/wireguard
+ - /lib/modules:/lib/modules:ro
+ cap_add:
+ - NET_ADMIN
+ - SYS_MODULE
+ sysctls:
+ - net.ipv4.ip_forward=1
+ - net.ipv4.conf.all.src_valid_mark=1
diff --git a/backend/templates/wg-easy/template.json b/backend/templates/wg-easy/template.json
new file mode 100644
index 0000000..f9201dd
--- /dev/null
+++ b/backend/templates/wg-easy/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "WG-Easy",
+ "description": "WireGuard VPN server with a web UI for clients and QR codes. Admin account is created on first visit.",
+ "tags": [
+ "network",
+ "vpn",
+ "security"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/wiki-js/.env.example b/backend/templates/wiki-js/.env.example
new file mode 100644
index 0000000..f784b68
--- /dev/null
+++ b/backend/templates/wiki-js/.env.example
@@ -0,0 +1,3 @@
+DATA_PATH=/srv/wiki-js
+HTTP_PORT=3080
+DB_PASSWORD=change-me
diff --git a/backend/templates/wiki-js/compose.yaml b/backend/templates/wiki-js/compose.yaml
new file mode 100644
index 0000000..4179a75
--- /dev/null
+++ b/backend/templates/wiki-js/compose.yaml
@@ -0,0 +1,27 @@
+services:
+ wiki:
+ image: ghcr.io/requarks/wiki:2
+ container_name: wiki-js
+ restart: unless-stopped
+ depends_on:
+ - db
+ environment:
+ - DB_TYPE=postgres
+ - DB_HOST=db
+ - DB_PORT=5432
+ - DB_NAME=wiki
+ - DB_USER=wiki
+ - DB_PASS=${DB_PASSWORD:-wiki}
+ ports:
+ - "${HTTP_PORT:-3080}:3000"
+
+ db:
+ image: postgres:16-alpine
+ container_name: wiki-js-db
+ restart: unless-stopped
+ environment:
+ - POSTGRES_DB=wiki
+ - POSTGRES_USER=wiki
+ - POSTGRES_PASSWORD=${DB_PASSWORD:-wiki}
+ volumes:
+ - ${DATA_PATH:-/srv/wiki-js}/db:/var/lib/postgresql/data
diff --git a/backend/templates/wiki-js/template.json b/backend/templates/wiki-js/template.json
new file mode 100644
index 0000000..bd53d3b
--- /dev/null
+++ b/backend/templates/wiki-js/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Wiki.js",
+ "description": "Modern wiki with Markdown and WYSIWYG editors, git sync and fine-grained permissions.",
+ "tags": [
+ "documents",
+ "wiki",
+ "productivity"
+ ],
+ "gpu": null
+}
diff --git a/backend/templates/zigbee2mqtt/.env.example b/backend/templates/zigbee2mqtt/.env.example
new file mode 100644
index 0000000..5f2ae53
--- /dev/null
+++ b/backend/templates/zigbee2mqtt/.env.example
@@ -0,0 +1,4 @@
+TZ=Europe/Berlin
+DATA_PATH=/srv/zigbee2mqtt
+HTTP_PORT=8095
+ZIGBEE_DEVICE=/dev/ttyUSB0
diff --git a/backend/templates/zigbee2mqtt/compose.yaml b/backend/templates/zigbee2mqtt/compose.yaml
new file mode 100644
index 0000000..48a2e6a
--- /dev/null
+++ b/backend/templates/zigbee2mqtt/compose.yaml
@@ -0,0 +1,18 @@
+services:
+ zigbee2mqtt:
+ image: koenkk/zigbee2mqtt:latest
+ container_name: zigbee2mqtt
+ restart: unless-stopped
+ environment:
+ - TZ=${TZ:-Europe/Berlin}
+ ports:
+ - "${HTTP_PORT:-8095}:8080"
+ volumes:
+ # The config lives with the stack so you can edit it here; the directory
+ # underneath holds the device database Zigbee2MQTT builds as you pair.
+ - ${DATA_PATH:-/srv/zigbee2mqtt}:/app/data
+ - ./configuration.yaml:/app/data/configuration.yaml
+ - /run/udev:/run/udev:ro
+ devices:
+ # Your Zigbee coordinator. Find it with: ls -l /dev/serial/by-id/
+ - ${ZIGBEE_DEVICE:-/dev/ttyUSB0}:/dev/ttyACM0
diff --git a/backend/templates/zigbee2mqtt/configuration.yaml b/backend/templates/zigbee2mqtt/configuration.yaml
new file mode 100644
index 0000000..73d5d4e
--- /dev/null
+++ b/backend/templates/zigbee2mqtt/configuration.yaml
@@ -0,0 +1,13 @@
+# Point `server` at your MQTT broker. Zigbee2MQTT also writes back to this file
+# as you pair devices, so keep it in the stack folder.
+homeassistant:
+ enabled: true
+frontend:
+ port: 8080
+mqtt:
+ base_topic: zigbee2mqtt
+ server: mqtt://mosquitto:1883
+serial:
+ port: /dev/ttyACM0
+advanced:
+ log_level: info
diff --git a/backend/templates/zigbee2mqtt/template.json b/backend/templates/zigbee2mqtt/template.json
new file mode 100644
index 0000000..23eadfe
--- /dev/null
+++ b/backend/templates/zigbee2mqtt/template.json
@@ -0,0 +1,10 @@
+{
+ "name": "Zigbee2MQTT",
+ "description": "Bridges Zigbee devices to MQTT without a vendor hub. Point it at your broker and coordinator stick.",
+ "tags": [
+ "home-automation",
+ "iot",
+ "mqtt"
+ ],
+ "gpu": null
+}
diff --git a/backend/version.py b/backend/version.py
index 62ebe57..6a55ffd 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,3 +1,3 @@
"""Single source of truth for the StackPilot release version."""
-APP_VERSION = "0.42.2"
+APP_VERSION = "0.43.0"
diff --git a/frontend/package.json b/frontend/package.json
index 0c862f9..04068a4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "stackpilot-frontend",
"private": true,
- "version": "0.42.2",
+ "version": "0.43.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/pages/Templates.tsx b/frontend/src/pages/Templates.tsx
index 76ea29d..0394772 100644
--- a/frontend/src/pages/Templates.tsx
+++ b/frontend/src/pages/Templates.tsx
@@ -1,7 +1,7 @@
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { LayoutTemplate, Cpu, Package, Trash2, FileCode } from "lucide-react";
+import { LayoutTemplate, Cpu, Package, Trash2, FileCode, Search } from "lucide-react";
import { Badge, Button, Card, Input, Spinner } from "@/components/ui";
import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
import { templatesApi, type TemplateDetail, type TemplateSummary } from "@/api/templates";
@@ -18,8 +18,31 @@ export function Templates() {
const queryClient = useQueryClient();
const [selected, setSelected] = useState(null);
const [toDelete, setToDelete] = useState(null);
+ const [search, setSearch] = useState("");
+ const [tag, setTag] = useState(null);
const { data, isLoading } = useQuery({ queryKey: ["templates"], queryFn: templatesApi.list });
+ // Tags sorted by how many templates carry them — the useful ones surface first.
+ const tags = useMemo(() => {
+ const counts = new Map();
+ for (const t of data ?? []) {
+ for (const tg of t.tags) counts.set(tg, (counts.get(tg) ?? 0) + 1);
+ }
+ return [...counts.entries()]
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
+ .map(([name, count]) => ({ name, count }));
+ }, [data]);
+
+ const shown = useMemo(() => {
+ const q = search.trim().toLowerCase();
+ return (data ?? []).filter((t) => {
+ if (tag && !t.tags.includes(tag)) return false;
+ if (!q) return true;
+ const haystack = `${t.name} ${t.description ?? ""} ${t.tags.join(" ")}`.toLowerCase();
+ return haystack.includes(q);
+ });
+ }, [data, search, tag]);
+
const open = async (t: TemplateSummary) => {
try {
setSelected(await templatesApi.get(t.id));
@@ -50,8 +73,36 @@ export function Templates() {
Templates are stored as ready-to-run stack folders. Pull one to copy it into a new stack,
then edit it like any other.
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ setTag(null)} />
+ {tags.map((tg) => (
+ setTag(tag === tg.name ? null : tg.name)}
+ />
+ ))}
+
+
+
+ {shown.length === 0 && (
+ No template matches that filter.
+ )}
+
- {data?.map((t) => (
+ {shown.map((t) => (
{t.source === "custom" ? (
@@ -64,10 +115,15 @@ export function Templates() {
{t.description && {t.description}
}
- {t.tags.map((tag) => (
-
- {tag}
-
+ {t.tags.map((tg) => (
+
))}
{t.gpu && (
@@ -109,6 +165,30 @@ export function Templates() {
);
}
+function TagChip({
+ label,
+ active,
+ onClick,
+}: {
+ label: string;
+ active: boolean;
+ onClick: () => void;
+}) {
+ return (
+
+ );
+}
+
function UseTemplateDialog({
template,
onClose,