Skip to content

Komodo Stack Guide

Reference: Komodo TOML structure, stack configuration, and operational patterns.

Source: extracted from AGENTS.md

Komodo Stack TOML Structure

Each stack TOML follows this pattern:

[[stack]]
name   = "service-name"
deploy = true
tags   = ["server:unraid", "category:{category}", ...]

[stack.config]
server                    = "unraid"
poll_for_updates          = false
auto_update               = true
destroy_before_deploy     = true
auto_update_all_services  = false
linked_repo               = "docker-compose"
run_directory             = "./stacks/{category}/{service}/"
file_paths                = ["compose.yaml"]
additional_env_files      = ["compose.env"]  # Optional local env
environment               = """
TZ=Asia/Ho_Chi_Minh
VAR_NAME=[[VAR_NAME]]
"""
  • Environment secrets use VAR=[[VAR]] syntax — Komodo resolves from Variables UI or config file secrets
  • Pre-deploy scripts for config file setup (e.g., grafana provisioning)
  • Default TZ: Asia/Ho_Chi_Minh

Tag Conventions

Pattern Example Meaning
server:{name} server:unraid Target server
category:{name} category:applications Stack category
saas:{name} saas:tailscale Expose via SaaS (Tailscale)
selfhost:{name} selfhost:photo Self-hosted replacement for SaaS
app:{name} app:immich Application identifier

Stack Config Fields

Field Default Description
server required Target server name
poll_for_updates false Check for updates but don't auto-deploy
auto_update true Auto-deploy on new image digest
destroy_before_deploy true Remove container before redeploy
auto_update_all_services false Update all services, not just changed
pre_deploy.command Bash script to run before deploy
additional_env_files Local env files (e.g., compose.env)
run_directory required Absolute path on periphery server (see below)

Toggle Use-Case Guide — docs-driven

Rules derive from Komodo docs (what each toggle DOES + correct usage), NOT from "what most stacks currently do". A survey encodes current behavior — including copy-paste / accidental no-ops — as "convention". The "Audit" column checks current homelab state against the correct rule (flags drift), but the Correct Rule is authoritative.

Example: auto_update=true on pinned-tag images is a harmless no-op (pinned digest never changes), not an intentional choice. Survey would call it "convention"; docs call it "no-op". Audit shows 42 stacks use mutable tags (:latest) where auto_update is meaningful; 34 use pinned tags where auto_update=true is no-op.

Toggle Docs semantics Correct rule (set when…) Audit: current homelab
destroy_before_deploy Remove container before redeploy (clean state, all config changes apply) true default. false ONLY if the service must stay continuously available OR owns resources that recreation breaks (macvlan IP it answers on, a network it created, container-identity-tied state). false cannot apply recreation-requiring changes (network/volume/UID) → flip true once or use a non-recreating workaround. true gotchas: volume ownership (chown in pre_deploy), encrypted keys must be fixed Komodo Variables. See lessons/komodo.md. 57 true / 5 false (adguard-home/sync/replica, gluetun, qmd = networking, can't drop) — aligned ✓
auto_update Auto-redeploy when image digest changes (same tag) true for mutable tags (:latest) you want auto-current. No-op on pinned tags (digest fixed) — true is harmless there but meaningless; set false for cleanliness. false for custom/private images you rebuild deliberately (avoid surprise redeploy). 56 true / 6 false. 42 stacks use mutable tags (meaningful) / 34 pinned (true = no-op). No drift to fix; optionally set false on pinned.
poll_for_updates Check registry, show update indicator (no deploy) false normally. true only for a manually-deployed stack where you want a UI badge for available updates. Redundant if auto_update=true (already monitors). 61 false / 1 true — aligned ✓
auto_update_all_services Update all services vs only changed ones false (efficient — only changed services redeploy). true only for tightly-coupled multi-service stacks where a partial update breaks consistency. 61 false / 1 true — aligned ✓
file_paths Which repo files periphery fetches ["compose.yaml"] + any file the compose includes. Files NOT listed are NOT synced → copy via pre_deploy (DAGs, plugins, config). 59 ["compose.yaml"]aligned ✓
server Target server unraid (primary) / development (dev-only stacks) 47 unraid / 16 development ✓
linked_repo Repo to sync from docker-compose (single repo). Empty "" does NOT work. 72 = docker-compose
run_directory Periphery path to compose Absolute path (relative → double-path bug, see below)
deploy Whether stack is active true; false to disable a stack without deleting it

Standard new-stack template (homelab defaults)

[[stack]]
name   = "<service>"
deploy = true
tags   = ["app:<type>", "server:<unraid|development>", "category:<cat>"]

[stack.config]
server                   = "unraid"
linked_repo              = "docker-compose"
run_directory            = "/etc/komodo/repos/docker-compose/stacks/<category>/<service>/"
file_paths               = ["compose.yaml"]
poll_for_updates         = false
auto_update              = true            # false if custom/pinned image
auto_update_all_services = false
destroy_before_deploy    = true            # false ONLY for networking/DNS/VPN
environment              = """
KEY=[[KOMODO_VARIABLE]]
"""
pre_deploy.command = """
# copy non-compose files to appdata; chown volumes for non-root images
"""

Decision shortcuts

  • Networking / DNS / VPN stack (destroy breaks deps): destroy_before_deploy=false, auto_update=false (don't surprise-recreate). Expect recreation-changes to fail deploy — use host-IP reverse_proxy or flip true once.
  • Custom-built image (:local, private registry you rebuild): auto_update=false (you control rebuilds; auto_update on a digest you pin is noise).
  • additional_env_files (e.g. compose.env): list local env files the periphery loads beyond environment. Rarely needed — prefer Komodo Variables in environment.
  • Encrypted-state service (Fernet/JWT in DB): destroy_before_deploy=true is fine (DB persists) BUT the key MUST be a fixed Komodo Variable — never regenerate.

run_directory MUST be absolute path

Periphery resolves run_directory relative to stack_dir (/etc/komodo/stacks/), NOT relative to the repo clone. Using relative path like ./stacks/foo/ will resolve to /etc/komodo/stacks/stacks/foo/double path.

Correct:

linked_repo  = "docker-compose"
run_directory = "/etc/komodo/repos/docker-compose/stacks/{category}/{service}/"

Wrong:

run_directory = "./stacks/{category}/{service}/"  # BUG: resolves to /etc/komodo/stacks/stacks/...

WARNING: linked_repo = "" (empty) does NOT work. Periphery requires linked_repo to know where files are. Even though the repo is already cloned locally, the field must be set.

Circular Dependency with Gitea

Gitea stack depends on the repo it hosts. If Gitea goes down:

  • Periphery cannot git pull new changes
  • But periphery can deploy using the last cloned files (already on disk at /etc/komodo/repos/docker-compose/)
  • linked_repo = "docker-compose" still works because files are cached locally
  • Only risk: if someone does docker volume prune or deletes /etc/komodo/repos/

Recovery steps if Gitea is down:

  1. SSH to unraid: ssh root@100.68.251.84
  2. Edit files directly: /etc/komodo/repos/docker-compose/stacks/{category}/{service}/
  3. Deploy via Komodo UI (periphery reads local files, no git needed)

Docker Compose Pitfalls

  • security_opt values must not have double quotes: no-new-privileges:true"no-new-privileges:true"
  • All YAML files in file_paths are parsed by Core — only include YAML files
  • Container names vs Host IPs: Only use container names (e.g., redis:6379) when services are on the same server and same Docker network. Cross-server or cross-network communication must use Tailscale IPs (e.g., 100.68.251.84:6379), NOT domain names
  • Port conflicts: Unraid nginx occupies port 80/443 on Tailscale IP. Never bind 3000:3000 or other common ports that may conflict.

Gitea Actions Runner (act_runner) Pitfalls

  • DooD mode (bind mount docker.sock): act_runner creates isolated Docker networks for job containers. container.network and container.options in runner-config.yaml are IGNORED in v0.3.1.
  • Job containers cannot resolve internal hostnames (e.g., gitea) because they run in separate networks.
  • Fix: Set GITEA_INSTANCE_URL to host-accessible URL (e.g., http://100.68.251.84:3005) so checkout action uses an IP that job containers can resolve via host.docker.internal or DNS.
  • Force re-register: Delete /data/.runner file to force runner re-registration after config changes.
  • destroy_before_deploy = true restarts runner but does NOT re-register. Only deleting .runner triggers re-registration.
  • container.options like --add-host and --network are NOT applied by act_runner v0.3.1 in DooD mode — it always creates its own network.

Adding a New Stack

  1. Create stacks/{category}/{service}/compose.yaml
  2. Create komodo/stacks/{category}/{service}.toml manually (copy from existing stack as template)
  3. Add custom tags (e.g., saas:tailscale) to the TOML
  4. Add alerter tag to route alerts to Ntfy
  5. Commit and push — Komodo webhook will detect pending changes

With Custom Image Build

If the stack needs a custom Dockerfile (app not on Docker Hub, custom pip install, etc.), follow the Custom Image Build Workflow instead.

Non-YAML Config Files

Stacks that need extra config files (Caddyfile, grafana provisioning, etc.) must NOT include them in file_paths — Core parses all listed files as YAML and will fail on non-YAML files.

Instead, use pre_deploy.command to copy files before deploy:

pre_deploy.command = """
#!/usr/bin/env bash
set -eu pipefail
echo "${PWD}"
mkdir -p /etc/komodo/repos/docker-compose/stacks/networking/caddy
cp ./Caddyfile /etc/komodo/repos/docker-compose/stacks/networking/caddy/Caddyfile
"""
  • PWD = run_directory on the periphery server
  • Runs on the target server before docker compose up
  • Use for: Caddyfile, grafana provisioning, nginx configs, etc.

Docker Compose Pitfalls

  • security_opt values must not have double quotes: no-new-privileges:true"no-new-privileges:true"
  • All YAML files in file_paths are parsed by Core — only include YAML files
  • Container names vs Host IPs: Only use container names (e.g., redis:6379) when services are on the same server and same Docker network. Cross-server or cross-network communication must use Tailscale IPs (e.g., 100.68.251.84:6379), NOT domain names

Adding a New Category

  1. Create stacks/{new_category}/{service}/compose.yaml
  2. Add new [[resource_sync]] entry in komodo/stacks/root_syncs/resource_sync.toml:
[[resource_sync]]
name = "new_category"
[resource_sync.config]
linked_repo       = "docker-compose"
resource_path     = ["./komodo/stacks/new_category"]
include_resources = true
pending_alert     = true
webhook_enabled   = false

Resource Sync Architecture

Each category has its own resource_sync in komodo/stacks/root_syncs/resource_sync.toml:

Sync Name Path Purpose
root-syncs ./komodo/stacks/root_syncs Infrastructure (servers, alerter, procedures)
applications ./komodo/stacks/applications User-facing applications
database ./komodo/stacks/database Database services
development ./komodo/stacks/development Dev environments
llm ./komodo/stacks/llm LLM services
monitoring ./komodo/stacks/monitoring Observability
networking ./komodo/stacks/networking Network services
orchestration ./komodo/stacks/orchestration Workflow engines
storage ./komodo/stacks/storage Storage services

Server Definitions

Servers are in komodo/stacks/root_syncs/server.toml:

[[server]]
name = "unraid"
description = "Main NAS server"
[server.config]
address = "http://100.68.251.84:8120"
enabled = true

Current servers: unraid (active), development (active)