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 fliptrueonce. - 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 beyondenvironment. Rarely needed — prefer Komodo Variables inenvironment.- Encrypted-state service (Fernet/JWT in DB):
destroy_before_deploy=trueis 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:
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 pruneor deletes/etc/komodo/repos/
Recovery steps if Gitea is down:
- SSH to unraid:
ssh root@100.68.251.84 - Edit files directly:
/etc/komodo/repos/docker-compose/stacks/{category}/{service}/ - Deploy via Komodo UI (periphery reads local files, no git needed)
Docker Compose Pitfalls¶
security_optvalues must not have double quotes:no-new-privileges:true✅"no-new-privileges:true"❌- All YAML files in
file_pathsare 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:3000or 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.networkandcontainer.optionsin 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_URLto host-accessible URL (e.g.,http://100.68.251.84:3005) so checkout action uses an IP that job containers can resolve viahost.docker.internalor DNS. - Force re-register: Delete
/data/.runnerfile to force runner re-registration after config changes. destroy_before_deploy = truerestarts runner but does NOT re-register. Only deleting.runnertriggers re-registration.container.optionslike--add-hostand--networkare NOT applied by act_runner v0.3.1 in DooD mode — it always creates its own network.
Adding a New Stack¶
- Create
stacks/{category}/{service}/compose.yaml - Create
komodo/stacks/{category}/{service}.tomlmanually (copy from existing stack as template) - Add custom tags (e.g.,
saas:tailscale) to the TOML - Add
alertertag to route alerts to Ntfy - 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_directoryon the periphery server- Runs on the target server before
docker compose up - Use for: Caddyfile, grafana provisioning, nginx configs, etc.
Docker Compose Pitfalls¶
security_optvalues must not have double quotes:no-new-privileges:true✅"no-new-privileges:true"❌- All YAML files in
file_pathsare 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¶
- Create
stacks/{new_category}/{service}/compose.yaml - Add new
[[resource_sync]]entry inkomodo/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)