Skip to content

Adding a Service

How to onboard a new Docker Compose service into the homelab monorepo so that Komodo deploys it automatically and Homepage picks up a tile.

Prerequisites

  • Docker and Docker Compose installed on the target host
  • The Traefik stack is running (it owns the traefik Docker network)
  • Komodo is running and connected to the host

Steps

1. Copy the template

cp -r services/_template services/<service-name>
cd services/<service-name>

2. Edit compose.yml

  • Replace all REPLACE_ME placeholders with real values
  • Set the correct image, container name, and ports
  • Configure Traefik labels with the desired subdomain (<name>.infra.realemail.app)
  • Add traefik.http.routers.<name>.middlewares=authentik@file (ADR-002 SSO — see authentik-cross-host-sso.md)
  • Add volumes for persistent data if needed
  • Ensure the service joins the traefik network (external)

3. Configure secrets

If the service needs secrets (API keys, passwords, DB credentials):

  1. Copy .env.sops.yaml.example to .env.sops.yaml and fill in real values
  2. Encrypt in WSL: sops -e -i services/<service-name>/.env.sops.yaml
  3. Commit and push the encrypted file
  4. On the host, decrypt SOPS YAML to a dotenv-format .env with restrictive permissions:
umask 077
SOPS_AGE_KEY_FILE=/etc/homelab/age-key.txt \
  sops --decrypt --output-type dotenv .env.sops.yaml > .env.tmp
mv .env.tmp .env

Prefer a service-specific renderer with cleanup traps, validation, and atomic rename. See secrets runbook.

4. Update inventory (if applicable)

Create inventory/services/<service-name>.yaml for each new Komodo-managed service. Existing stacks represented by legacy host links can migrate incrementally:

id: example
name: Example
status: active
host: infra-services
port: 8080
url: https://example.infra.realemail.app
backup:
  policy: tier-2
monitoring:
  healthcheck: /healthz
  alert_owner: homelab
documentation: docs/services/example.md
tags: [infrastructure]

Active and decommissioning services require host. An active service with a url becomes a Homepage tile. Tags monitoring or observability place it in the Monitoring category; infrastructure or ops place it in Infrastructure.

Then:

  1. Add or update the inventory entry.
  2. Run generators to update downstream files:
uv run python inventory/generators/render-discovery-inventory.py
uv run python inventory/generators/render-ansible.py
uv run python inventory/generators/render-homepage.py
uv run python inventory/generators/render-doc-stubs.py
uv run python inventory/generators/render-prometheus.py
uv run python inventory/generators/render-diagram.py
uv run python inventory/generators/render-dns-rewrites.py

Before push, run the same generators with --check; CI fails on generator drift.

5. Update backup policy

Edit backup.yml in the service directory:

  • Set the correct tier (1 = critical, 2 = nice-to-have, 3 = replaceable)
  • Set hosts to the Docker host(s) where the listed paths exist
  • List the paths that need backing up
  • Add pre/post hooks if the service needs to quiesce before backup

Per-service services/<name>/backup.yml is the runtime manifest. Update restore.md when a service needs non-obvious restore steps.

6. Write the README

Fill in the service README with:

  • What the service does
  • Quick reference table (image, port, URL)
  • Any manual setup steps
  • Troubleshooting tips

7. Add Komodo resources

Add the stack to services/komodo/resources.toml so Komodo reconciles it from /opt/homelab/services/<service-name>. Use compose-only file_paths; the komodo-resources-check CI job validates referenced paths and TOML syntax.

8. Commit and push

git add services/<service-name>
git commit -m "Add <service-name> service"
git push

Komodo detects the push (via webhook or polling) and deploys the stack automatically. The Homepage tile appears after the generator runs.

DNS

New services on infra-services use *.infra.realemail.app subdomains. The static AdGuard wildcard in services/adguard/infra-dns-rewrites.yaml already maps that zone to 192.168.6.17; the inventory DNS generator manages host records under *.lab.local, not individual service hostnames. Use Cloudflare DNS only if a service is intentionally public.

Checklist

  • [ ] compose.yml has no REPLACE_ME placeholders
  • [ ] .env.sops.yaml is encrypted (pre-commit hook verifies this)
  • [ ] backup.yml tier is set correctly
  • [ ] README documents the service
  • [ ] inventory/services/<name>.yaml validates and generates a Homepage tile
  • [ ] Generators run cleanly (--check passes)
  • [ ] services/komodo/resources.toml includes the stack and passes CI
  • [ ] Service starts with docker compose up -d on the target host
  • [ ] Traefik routes traffic correctly to the service
  • [ ] Traefik router includes authentik@file middleware (infra-services only)
  • [ ] Authentik provider covers the new hostname (or domain-level provider)
  • [ ] Prometheus target or scrape config is added when the service exposes metrics
  • [ ] Restore runbook covers any state that cannot be recreated from git/SOPS