# SOP-001: Migrate Komodo Stack from UI-Defined to Git-Based **Status:** Active **Created:** April 11, 2026 **Last Updated:** April 11, 2026 **Owner:** Nathan Castaldi **Applies To:** All Komodo-managed Docker Compose stacks --- ## Purpose Convert an existing Komodo stack from UI-defined (manual configuration) to Git-based (GitOps workflow) to enable: - Version control and change tracking - Automated deployments via webhooks - Easier rollback and disaster recovery - Multi-environment consistency --- ## Prerequisites ### Required Access - [ ] SSH access to the target node (e.g., `waldorf`, `heimdall`, `watchtower`) - [ ] Komodo UI access (`komodo.castaldifamily.com`) - [ ] Gitea repository access (`git.castaldifamily.com/nathan/homelab`) - [ ] Git configured on your local machine ### Required Infrastructure - [ ] Komodo Periphery has `/etc/komodo/repos` volume mounted (see [KBA-001](../KBAs/KBA-001-Komodo-GitOps-Stack-Deployment-Failures.md)) - [ ] Gitea webhooks configured and verified - [ ] NFS mounts operational (if using shared storage) --- ## Pre-Migration Data Collection Before making any changes, **capture the current stack configuration** from Komodo UI: ### Step 1: Export Stack Configuration 1. **Navigate to Komodo UI** → Stacks → Select your stack 2. **Copy the following information:** | Field | Value | Notes | |-------|-------|-------| | **Stack Name** | _______________ | (e.g., `plex`, `tunarr`) | | **Node Name** | _______________ | (e.g., `waldorf`, `heimdall`) | | **Container Name(s)** | _______________ | From compose services | | **Image(s)** | _______________ | **Critical:** Note exact tag | | **Ports** | _______________ | Host:Container mappings | | **Volumes** | _______________ | Full paths (host → container) | | **Environment Variables** | _______________ | **Secrets go here, NOT in Git** | | **Network Mode** | _______________ | (bridge/host/custom) | | **Restart Policy** | _______________ | (unless-stopped/always/no) | | **Labels** | _______________ | Traefik, Komodo, etc. | | **Devices** | _______________ | GPU passthrough, /dev/dri, etc. | 3. **Copy the complete `compose.yaml`** from the stack editor 4. **Take a screenshot** of the environment variables section (contains secrets) --- ## Migration Procedure ### Step 2: Create Repository Directory Structure On your **local machine** (or via Working Copy on iPad): ```bash # Navigate to homelab repo cd ~/homelab # Or your local path # Create the directory structure mkdir -p nodes/{node-name}/{stack-name} # Example: mkdir -p nodes/waldorf/sonarr ``` **Directory naming convention:** - Node name: `heimdall`, `waldorf`, `watchtower` - Stack name: Lowercase, matches service (e.g., `plex`, `tunarr`, `sonarr`) --- ### Step 3: Create compose.yaml Create `nodes/{node-name}/{stack-name}/compose.yaml` with the **sanitized** configuration: ```yaml services: {service-name}: image: {registry}/{image}:{tag} # ⚠️ NO 'v' prefix on tag container_name: {container-name} restart: unless-stopped # or 'always' ports: - {host-port}:{container-port} environment: - TZ=America/New_York # ⚠️ DO NOT store passwords/tokens here # Use Komodo UI Environment Variables instead volumes: - /mnt/appdata/{service}/config:/config - /mnt/media/data:/data # If applicable # Optional: GPU passthrough deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] # Optional: Device passthrough (Intel QuickSync, etc.) devices: - /dev/dri:/dev/dri # Optional: Traefik labels labels: - "traefik.enable=true" - "traefik.http.routers.{service}.rule=Host(`{service}.castaldifamily.com`)" - "komodo.managed=true" ``` **⚠️ Critical Configuration Rules:** 1. **Image Tags:** - ✅ Correct: `image: chrisbenincasa/tunarr:1.2.11` - ❌ Wrong: `image: chrisbenincasa/tunarr:v1.2.11` (no `v` prefix) 2. **Secrets Management:** - ❌ **NEVER** commit passwords, API keys, or claim tokens to Git - ✅ Use Komodo Stack Environment Variables for secrets - ✅ Use placeholders in compose: `- PLEX_CLAIM=${PLEX_CLAIM}` 3. **Volume Paths:** - Use absolute paths starting with `/mnt/appdata/` - Ensure paths exist on the target node - Match UID/GID permissions (usually `1000:1000`) --- ### Step 4: Commit and Push to Git ```bash # Stage the new files git add nodes/{node-name}/{stack-name}/ # Commit with descriptive message git commit -m "feat(stacks): migrate {stack-name} to Git-based deployment - Created compose.yaml for {node-name}/{stack-name} - Extracted configuration from Komodo UI - Secrets managed via Komodo Environment Variables" # Push to Gitea git push origin main ``` --- ### Step 5: Reconfigure Stack in Komodo UI 1. **Navigate to Komodo UI** → Stacks → Select your stack 2. **Change stack type:** - Click **Edit Stack** (gear icon) - Change **Source Type** from `Manual` to `Git Repo` 3. **Configure Git settings:** | Field | Value | |-------|-------| | **Repo** | `homelab` | | **Branch** | `main` | | **Run Directory** | `nodes/{node-name}/{stack-name}` | | **File Paths** | *(leave blank - uses compose.yaml by default)* | 4. **Re-add Environment Variables (Secrets):** - Click **Environment Variables** tab - Add back any secrets from your pre-migration notes: ``` PLEX_CLAIM=claim-xxxxxxxxx SONARR_API_KEY=xxxxxxxxxxxxxxxx ``` 5. **Save Configuration** --- ### Step 6: Deploy and Verify 1. **Pull from Git:** - Click **Pull Stack** button - Wait for success notification - Verify "Last pulled" timestamp updated 2. **Deploy Stack:** - Click **Deploy Stack** button - Monitor deployment logs in Komodo UI - Wait for "Running" status 3. **Verify on Target Node:** ```bash # SSH to the node ssh chester@10.0.0.{node-ip} # Check container status docker ps | grep {container-name} # Check logs for errors docker logs {container-name} --tail 50 # Verify volumes mounted correctly docker inspect {container-name} | grep -A 10 "Mounts" # If GPU passthrough required: docker exec {container-name} nvidia-smi ``` 4. **Functional Testing:** - Access the service via browser/app - Verify data persistence (configs, databases) - Test core functionality (playback, API calls, etc.) --- ## Verification Checklist After migration, confirm: - [ ] Container is running (`docker ps`) - [ ] Service accessible via web UI/API - [ ] Data persists across container restart - [ ] Environment variables applied correctly - [ ] GPU accessible (if applicable): `docker exec {name} nvidia-smi` - [ ] Logs show no mount/permission errors - [ ] Traefik routing works (if applicable) - [ ] Auto-deployment triggers on Git push --- ## Rollback Procedure If migration fails, revert to UI-defined stack: 1. **In Komodo UI:** - Edit Stack → Change **Source Type** back to `Manual` - Paste original compose.yaml from pre-migration backup - Re-add environment variables - Deploy stack 2. **On Target Node (Emergency):** ```bash # Navigate to stack directory cd /etc/komodo/stacks/{stack-name} # Manually restore compose.yaml nano compose.yaml # Paste backup content # Restart manually docker compose down docker compose up -d ``` --- ## Post-Migration Tasks ### Test Auto-Deployment 1. Make a minor change to the compose file (e.g., update comment) 2. Commit and push to Git 3. Verify Komodo auto-pulls and redeploys (check timestamps) 4. If auto-deploy doesn't work, manually click "Pull" → "Deploy" ### Update Documentation - [ ] Add stack to node README (e.g., `nodes/waldorf/README.md`) - [ ] Document any special configuration requirements - [ ] Update infrastructure diagrams if necessary --- ## Common Issues ### Issue: "Image not found" after deploy **Cause:** Docker tag has `v` prefix (e.g., `v1.2.11` instead of `1.2.11`) **Fix:** ```yaml # In compose.yaml, remove 'v' prefix image: user/app:1.2.11 # Not v1.2.11 ``` --- ### Issue: Environment variables not applied **Cause:** Secrets defined in Git-based compose.yaml instead of Komodo UI **Fix:** 1. Remove secrets from `compose.yaml` in Git 2. Use placeholders: `- API_KEY=${API_KEY}` 3. Add actual values in Komodo UI → Stack → Environment Variables --- ### Issue: Pull Stack button doesn't update files **Cause:** Known issue (see [KBA-001](../KBAs/KBA-001-Komodo-GitOps-Stack-Deployment-Failures.md)) **Workaround:** ```bash # SSH to node docker exec komodo-periphery-{node} sh -c \ 'cp /etc/komodo/repos/homelab/nodes/{node}/{stack}/compose.yaml \ /etc/komodo/stacks/{stack}/compose.yaml' ``` --- ### Issue: Permission denied on volume mounts **Cause:** UID/GID mismatch between container and host directory **Fix:** ```bash # On target node, set correct ownership sudo chown -R 1000:1000 /mnt/appdata/{service}/ ``` --- ## Security Considerations ### Secrets Management | ❌ DO NOT | ✅ DO | |-----------|-------| | Commit passwords to Git | Use Komodo Environment Variables | | Store API keys in compose.yaml | Use `${VAR_NAME}` placeholders | | Hardcode claim tokens | Inject via Komodo UI | ### Example: Plex Claim Token **Bad (in Git):** ```yaml environment: - PLEX_CLAIM=claim-sxFpsPTDzzF-9RZAxtUL # ❌ Exposed in Git ``` **Good (in Git):** ```yaml environment: - PLEX_CLAIM=${PLEX_CLAIM} # ✅ Placeholder ``` **In Komodo UI:** - Environment Variables → Add: `PLEX_CLAIM=claim-sxFpsPTDzzF-9RZAxtUL` --- ## Related Documentation - [KBA-001: Komodo GitOps Stack Deployment Failures](../KBAs/KBA-001-Komodo-GitOps-Stack-Deployment-Failures.md) - [TECHNICAL_RUNBOOK.md](../TECHNICAL_RUNBOOK.md) - Infrastructure overview - Repository Memory: `/memories/repo/active-tasks.md` --- ## Appendix: Example Migrations ### Example 1: Simple Service (No GPU) **Stack:** Sonarr on Waldorf ```yaml services: sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr restart: unless-stopped ports: - 8989:8989 environment: - PUID=1000 - PGID=1000 - TZ=America/New_York volumes: - /mnt/appdata/sonarr:/config - /mnt/media/tvshows:/tv - /mnt/media/downloads:/downloads ``` --- ### Example 2: GPU Transcoding Service **Stack:** Plex on Waldorf (NVIDIA GTX 1060) ```yaml services: plex: image: lscr.io/linuxserver/plex:latest container_name: plex network_mode: host restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=America/New_York - VERSION=docker - PLEX_CLAIM=${PLEX_CLAIM} # Set in Komodo UI - NVIDIA_VISIBLE_DEVICES=all - NVIDIA_DRIVER_CAPABILITIES=compute,video,utility volumes: - /mnt/appdata/plex:/config - /mnt/media/tvshows:/tv - /mnt/media/movies:/movies deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] ``` --- ### Example 3: Traefik-Routed Service **Stack:** Custom App on Heimdall (Behind Traefik) ```yaml services: myapp: image: myregistry/myapp:2.1.0 container_name: myapp restart: unless-stopped ports: - 3000:3000 environment: - NODE_ENV=production - DATABASE_URL=${DATABASE_URL} # Secret in Komodo UI volumes: - /mnt/appdata/myapp/data:/app/data labels: - "traefik.enable=true" - "traefik.http.routers.myapp.rule=Host(`myapp.castaldifamily.com`)" - "traefik.http.routers.myapp.entrypoints=websecure" - "traefik.http.routers.myapp.tls=true" - "traefik.http.routers.myapp.tls.certresolver=cloudflare" - "traefik.http.services.myapp.loadbalancer.server.port=3000" ``` --- **Document Version:** 1.0 **Tested On:** Komodo v2.1.2, Gitea v1.21 **Validation Status:** Production-Ready ✅