Skip to content

Adding a Service

How to onboard a new Docker Compose service into the homelab monorepo for explicit Komodo deployment and a generated Homepage tile. Pushing or merging the configuration does not start the Komodo relay.

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

set -euo pipefail
cp -r services/_template services/<service-name>
cd services/<service-name> || exit 1

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. Only for a new service with no encrypted source, create .env.sops.yaml using sops .env.sops.yaml on the operator machine. Use the example as a schema, not an unconditional copy over an existing secret file. Quote YAML values as strings. Save encrypted; commit only the encrypted source.
  2. Review the required keys and consumers in compose.yml, including passwords embedded in URLs. Choose a maintained service renderer where available.
  3. Otherwise use the shared renderer below on the host from the service directory. Replace SERVICE_REQUIRED_KEY with the actual required key and repeat --require for every required value; add --url-safe KEY for URL-embedded passwords under the default restricted contract. This is a template, not a ready-to-run service contract. Do not rewrite an existing credential to fit it.
set -euo pipefail
cd /opt/homelab/services/<service-name> || exit 1
sudo -n python3 /opt/homelab/scripts/render-sops-env.py \
  --source .env.sops.yaml --output .env --require SERVICE_REQUIRED_KEY || exit 1

Follow the rendering contract: reviewed full checkout, Python 3, SOPS, authorized key context, private ignored staging, strict validation and atomic promotion are mandatory, not optional. The default host key and resulting .env are root-only; use the same authorized root context for Compose. No deploy after a failed render, and do not force-add plaintext. Unsupported dotenv characters fail closed; do not bypass this by redirecting SOPS. Only LiteLLM callers use the reviewed --literal POSTGRES_PASSWORD and --url-encode POSTGRES_PASSWORD=POSTGRES_PASSWORD_URLENCODED exception, retaining both required keys and preserving the existing database credential. It does not relax other values or placeholders. Do not copy those flags into another service without a separately reviewed consumer contract; Phoenix retains --url-safe, and Komodo/Firefly defaults are unchanged. See the narrow exception and pre-cutover checks. Source tests do not grant deployment approval or prove target-client compatibility.

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. Choose the deployment-policy tag and corresponding host-local Procedure target explicitly. List non-Compose deployment inputs too: native config_files needs an explicit requires action and must not expose plaintext secrets. Source-built applications need input-aware selection, not just Compose changes. Reuse the private fingerprint pattern where appropriate; see the Komodo capability audit. Render through the maintained safe renderer before fingerprinting; predeploy only verifies the inputs, and does not replace the secret/restore gates above.

8. Commit and push

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

Review and merge the service configuration to main. Pushes and merges do not start the Komodo relay. Native GitHub reviewer/branch protection is not currently configured; the environment label is not an enforced approval gate.

Coordinate the new resource's reviewed external bootstrap Sync and relay Read grants before dispatching the general workflow. The API relay refuses missing or unreadable declared resources, rather than guessing whether list results were permission-filtered. See rollout and bootstrap. Check the stack's deployment policy and Procedure coverage, then follow the explicit main dispatch with DEPLOY_HOMELAB and the terminal Update and health checks. The relay waits for exact successful Sync and Procedure Updates; polling is not a deployment fallback. Application acceptance remains separate from relay success. The Homepage generator updates repository configuration; the tile becomes available when that configuration reaches the running Homepage instance.

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
  • [ ] Approved deployment has terminal successful Updates and verified service health
  • [ ] 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