Ctrl K
ComunidadGuías…

AGENTS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# khe-homelab

Family homelab as code: one Proxmox host, one Debian 13 VM running Docker
Compose stacks for core, media, home and app services, no Kubernetes
([estate ADR-010](https://github.com/khelias/khe-architecture/blob/main/decisions/010-one-host-docker-compose.md)).
Every compose file,
AdGuard and Homepage config lives here. This file holds rules and
pointers; the reference lives in:

- [README.md](README.md) - architecture, services table, security model,
  resilience, network, setup, day-to-day ops
- [docs/runbook.md](docs/runbook.md) - what to do when something is broken
- [docs/operational-notes.md](docs/operational-notes.md) - per-service quirks
  and measured integration behaviour
- [docs/service-choices.md](docs/service-choices.md) - why each service, and
  what would force a re-evaluation
- [SECURITY.md](SECURITY.md), [ROADMAP.md](ROADMAP.md),
  [infrastructure/](infrastructure/) (Proxmox, network, Cloudflare, Tailscale)

Home Assistant's configuration, dashboard generator and 3D house view are in
[khe-home-assistant](https://github.com/khelias/khe-home-assistant), whose
`AGENTS.md` points on to the house documentation in the private `khe-meta`
repo. This repo keeps HA's container and ops layer.

## Layout

```
services/
  core/            NPM, AdGuard, Cloudflare Tunnel, Vaultwarden,
                   Uptime Kuma, Homepage, autoheal
  media/           Immich
  home/            Home Assistant (container only), Mosquitto, PAI (Paradox alarm)
  apps/            landing, games, pages, trips
  observability/   Loki, Grafana, Alloy, Alertmanager (one stack)
infrastructure/    Proxmox, network, Cloudflare, Tailscale docs;
                   edge-fallback/ holds the edge copy's Worker
scripts/           setup, deploy, backup, hardening, validate-compose;
                   lib.sh holds the helpers they share
```

Image tags live in each `docker-compose.yml`; Renovate opens the bump PRs,
except for the estate's own images (rule 3). CI (`validate.yml`, the required
check) runs the pin App guard, `scripts/verify-estate-pins.sh`, `bash -n` on
each `scripts/*.sh` and `scripts/validate-compose.sh`, every script from the
base branch against the PR tree. The edge fallback's Worker tests
(`node --test infrastructure/edge-fallback/test/`, no install needed) run in
`edge-fallback.yml` before every deploy and in the workspace commit gate.

## Rules

1. **Each service directory** has a `docker-compose.yml` and, if it uses env
   vars, a `.env.example`. Live `.env` files are gitignored.
2. **Public repo: nothing secret or identifying.** No `.env` files, keys,
   tokens, bcrypt or crypt hashes, MAC addresses or device identifiers. The
   gitleaks pre-commit hook enforces it for secrets (`./scripts/install-hooks.sh`
   after cloning). A false positive gets an exclusion in `.gitleaks.toml`, never
   `--no-verify`. Personal-data patterns (MAC and LAN addresses, coordinates,
   e-mail, phone) are caught by the khe-workspace commit gate, rules in its
   `.claude/hooks/pii-rules.toml`; names and street addresses by nobody.
3. **Pin image versions**, no `:latest`. Images the estate builds itself
   (`ghcr.io/khelias/*`, [estate ADR-008](https://github.com/khelias/khe-architecture/blob/main/decisions/008-container-images.md)) are pinned as
   `:main@sha256:<digest>`. The app's CI publishes them and then opens a pin
   PR here through a per-repo GitHub App (`khe-adventure-pins`) with
   auto-merge; Renovate is disabled for them (`renovate.json`). An App PR is
   safe to automerge because `validate.yml` lets the App change nothing but
   its own `:main@` digests, one line for one line, and
   `verify-estate-pins.sh` requires every pinned digest to be attested by its
   repo's `ci.yml` on main, all images of a repo from one commit.
4. **State in named volumes or bind mounts under `/srv/data/<service>/`**,
   never `/home` or arbitrary paths. HA's config is the named volume
   `homeassistant_ha_config`, not `/srv/data`, because the recorder is SQLite
   and `/srv/data` is a soft NFS mount; khe-home-assistant's deploy writes
   its managed files there. State that lives in the checkout: Mosquitto's
   `./data`, and AdGuard's live `config/AdGuardHome.yaml` (gitignored, seeded
   from the committed template, rule 6). `backup.sh` tars HA's config volume (plus a recorder dump) and
   Mosquitto's `config/`, not its `data/`.
5. **Ingress through the shared `proxy` network** for NPM; separate networks
   isolate databases. The `draft.khe.ee` editor, which runs without an app
   login, sits on `draft-tunnel`, shared only with `cloudflare-tunnel`; the
   tunnel stack owns that network because it deploys first. Public traffic
   enters only through the Cloudflare Tunnel, never a forwarded port, and an
   app without a login of its own sits behind Cloudflare Access
   ([estate ADR-011](https://github.com/khelias/khe-architecture/blob/main/decisions/011-cloudflare-tunnel-and-access-edge.md)).
   The one exception for the private khe-trips atlas is its public demo at
   `khe.ee/trips/`, which shows only trips that are over, and only their
   short title, place and start date for trips that do not open
   ([estate ADR-013](https://github.com/khelias/khe-architecture/blob/main/decisions/013-trips-public-demo.md)).
   Landing serves it as khe-trips built it, like `/r/`: no edge cache and
   never indexed.
6. **AdGuard's live `AdGuardHome.yaml` is gitignored.** Apply changes as
   delta patches against `AdGuardHome.template.yaml`; replacing it wholesale
   wipes admin credentials and sessions
   ([notes](docs/operational-notes.md#adguard-home)).
7. **Deployment surface changes update their doc in the same commit:** a
   service or domain goes into the README services table and diagram, a
   service quirk into `docs/operational-notes.md`, a network, DNS,
   Cloudflare or Tailscale change into `infrastructure/*.md`, a security
   model change into `SECURITY.md`.
8. **The edge fallback mirrors the landing and games nginx**
   ([estate ADR-016](https://github.com/khelias/khe-architecture/blob/main/decisions/016-static-fallback-at-the-edge.md)).
   Two Cloudflare Workers (`infrastructure/edge-fallback/`) serve a copy of
   the static sites when the homelab answers 5xx or not at all. A header,
   redirect or SPA-fallback change in `services/apps/landing/nginx.conf` or
   `services/apps/games/nginx.conf` is mirrored in
   `infrastructure/edge-fallback/src/rules.mjs` in the same commit; the
   Worker tests catch header drift. The allowlist in
   `scripts/edge-fallback-stage.sh` is the only way a tree reaches the copy:
   `trips.khe.ee` and secrets never do, and the adventure `config.js` is a
   stub without `API_SECRET`. The Worker routes live in the dashboard, fail
   open, and are not in `wrangler.jsonc`. games.khe.ee's root is khe-sites'
   launcher build, and /study and /adventure load the consent banner from
   its `/assets/analytics-consent.js`
   ([estate ADR-019](https://github.com/khelias/khe-architecture/blob/main/decisions/019-consent-banner-from-the-origin-root.md)),
   so a route change keeps that path on the launcher, in the games nginx,
   in `rules.mjs` and in the staged copy.

## Access and operations

- Proxmox `192.168.0.10` (`pve.khe.ee`, UI on `:8006`); Docker VM
  `192.168.0.11`, SSH `khe@docker-vm` over Tailscale MagicDNS, key auth
  only. Admin credentials live in the VM's `.env` files, except NPM's login
  (in its database) and AdGuard's (in its live `AdGuardHome.yaml`).
- On the VM: `./scripts/deploy.sh status|pull|up|down`, `./scripts/backup.sh`
  (Postgres dumps, config snapshots, HA's config volume and recorder). Per service:
  `cd services/<group>/<service> && docker compose up -d`.
- **A push to `main` is the deploy.** `deploy.yml` runs when `services/**`,
  `scripts/deploy.sh`, `scripts/deploy-stacks.sh`, `scripts/lib.sh` (which
  deploy-stacks.sh and the cron backups source), `scripts/homelab-status.sh`,
  `scripts/retire-stack.sh` (the status timer runs those two from the VM
  checkout) or the workflow changes: the runner pulls `/home/khe/homelab`
  with `git pull --ff-only` and deploys the stacks changed since
  `.deploy/last-successful-sha`. A manual deploy after that push is a no-op.
  One is needed only for changes outside the paths filter or when CI is
  down, and then it is `gh workflow run deploy.yml` (inputs `mode`, `stack`,
  `force_recreate`, `dry_run`).
- **Rolling back an estate app** is a git change here. Pin every image of
  the app to the same `sha-<full commit>@sha256:<digest>` and push. The pin
  job skips a `sha-` pin, so the rollback stays until the operator returns
  the pins to `:main@sha256:<current>`, the digest from an anonymous
  manifest fetch of `ghcr.io/v2/khelias/<image>/manifests/main`; the next
  app push then moves them again. `homelab-status.sh` prints the revision
  each estate container runs.
- **Automated writers**
  ([estate ADR-007](https://github.com/khelias/khe-architecture/blob/main/decisions/007-automated-writers.md)): Renovate
  automerges Docker patch, minor and digest updates except Home Assistant
  minors and the critical-infrastructure group (`renovate.json`), the pin
  Apps automerge estate image pins (rule 3), every merge to `main` deploys,
  and scheduled jobs back up and prune, verify a restore and clean the
  runner. `edge-fallback.yml` deploys the edge copy hourly when a served tree
  changed; it is undone by disabling the workflow and deleting the two
  Worker routes. The way back from a bad update is to revert its commit on `main`
  and push; for an image that keeps state, also restore its data from the
  backup taken before the update
  ([runbook](docs/runbook.md#undoing-an-automerged-update)). A new automated
  writer joins the estate's writer register in khe-meta in the same change.
- **A new app database** joins `POSTGRES_JOBS` in `scripts/backup.sh` and the
  weekly restore check (`restore-verify.yml`, Immich only today) in the
  change that creates it, and one passing restore of it comes before any
  data that is not the family's own
  ([estate ADR-004](https://github.com/khelias/khe-architecture/blob/main/decisions/004-app-backend-and-identity.md) point 6).
- **Check the branch before writing.** The VM pulls `main`, and finished work
  sometimes sits on a feature branch for weeks. A push to the wrong branch
  shows up as "Already up to date" on the VM and "Unknown stack" from the
  deploy.
- **Operations go through `workflow_dispatch`** on the homelab runner:
  `ops-status.yml` refreshes the status snapshot ([runbook](docs/runbook.md)),
  `deploy.yml` deploys (the operator's call), `retire-stack.yml` removes what
  a stack deleted from git leaves on the VM (the agent dispatches it only
  with `apply=false`; `apply=true` is the operator's call). A workflow never
  takes a shell command as input; that would be SSH under another name.
  Narrow, pre-written actions only.
- **The agent reads the VM through Loki**, with the official mcp-grafana
  server (`scripts/mcp-grafana.sh`, read-only, Grafana Viewer token in
  `~/.config/khe/grafana-token`): container logs, and `{job="homelab-status"}`
  for the `homelab-status.sh` snapshot that the `khe-homelab-status` timer
  writes every 5 min. This repo's Actions logs are public, so `ops-status.yml`
  prints only the summary. Loki has no per-stream access control, so
  Vaultwarden, Home Assistant, Immich and trips logs are reachable too: query
  only what the task needs, and never quote personal data or tokens from them.
- **Fork PRs can reach the runner.** Fork PR approval stays at
  `all_external_contributors`, and a fork run is approved only after reading
  its `.github/workflows/` diff ([estate ADR-020](https://github.com/khelias/khe-architecture/blob/main/decisions/020-agent-work-through-pull-requests.md)).

## Working on Home Assistant

The procedure (API, token, reloads, the dashboard generator) is in
[khe-home-assistant's AGENTS.md](https://github.com/khelias/khe-home-assistant/blob/main/AGENTS.md);
its deploy writes the config into the `homeassistant_ha_config` volume and
never restarts HA. What stays here: the container in
`services/home/homeassistant/docker-compose.yml` and its volume, the image
pin, the Renovate hold on HA bumps and the backup in `scripts/backup.sh`
([notes](docs/operational-notes.md#home-assistant)).