--- type: guide title: Deployment Flow description: Service-scoped Git deployment for the two homelab hosts. tags: - deployment - operations - git timestamp: 2026-09-03 --- # Deployment Flow Git is the single source of truth. `scripts/deploy.sh` pushes the committed `main` branch, pulls it on one target, and applies only the runtime services whose tracked inputs changed. A SearXNG change no longer recreates the inference service; an LLM change no longer restarts unrelated containers. ## Workflow ```mermaid graph LR A[Edit in this repo] --> B[Run tests] B --> C[Commit main] C --> D[deploy.sh computes plan] D --> E[Push main] E --> F[Target pulls with ff-only] F --> G[deploy-remote.sh dispatches selected services] G --> H[Per-service health check] ``` 1. Edit files locally in this repository. 2. Run `./tests/run.sh`, then commit the complete change on `main`. 3. Run `./scripts/deploy.sh `. 4. The planner maps changed paths to services. Compose YAML is compared by service definition so an edit to one service does not restart its peers. 5. The target pulls with `git pull --ff-only origin main` and runs the remote dispatcher with the explicit plan. 6. The dispatcher updates only selected Quadlet, compose, homepage, Caddy, or systemd services and records each successful service revision outside Git. ## Commands ```bash ./scripts/deploy.sh dankclaw ./scripts/deploy.sh dankserver ./scripts/deploy.sh frankbook-pro --service qbittorrent # podman-machine target ./scripts/deploy.sh dankclaw --service searxng ./scripts/deploy.sh dankserver --service frank-archon ./scripts/deploy.sh dankclaw --dry-run ``` `--service` is repeatable and is an explicit retry/override. Without it, the planner compares the local `HEAD` to `origin/main`. A documentation-only commit produces an empty plan. The remote state directory can also replay a service whose earlier deployment failed, but it never expands a plan to a service on the other host. ## Service managers | Service family | Runtime manager | Scope behavior | |---|---|---| | Dankclaw compose services | `podman-compose -f dankclaw/compose.yaml up -d --no-deps NAME` | Recreates only `NAME`; Caddy is handled separately because it fronts both hosts. | | SearXNG, Gluetun, Valkey, OpenWebUI, Pi-hole, Gitea database/server | user Quadlet + `systemctl --user` | Installs the selected unit and restarts only the required dependency-coupled units. Gitea's web proxy is the canonical Caddy compose service. | | `frank-dragoon` | systemd + `systemctl` (root unit) | Relinks and restarts only the Dankclaw iGPU inference service; waits up to 20 minutes for `/health`. Shape, gates and boot budget: [[frank-dragoon]]. | | `frank-zealot` | systemd + `systemctl` (root unit) | Relinks and restarts only the Dankclaw 3090 inference service; waits up to 20 minutes for `/health`. Shape, gates and boot budget: [[frank-zealot]]. | | `frank-archon` | systemd + `systemctl` (root unit) | `deploy_llm_archon` plans from repo paths and gates on the port; it does NOT see host-side unit drift (see [[frank-archon]] → Deploy / verify). | | Dankserver media stack (RomM trio, Jellyfin, Calibre-Web, Copyparty) | user Quadlet + `systemctl --user` | Installs `dankserver//quadlet/*.container`, restarts the generated unit. The first cutover per service is idempotent: it retires the compose-era bare container, rescues any anonymous volume into the data root, and merge-moves `/opt/` to `/home/dank/Containers/` (mapped-uid roots move without chown). Steady state is restart-only. | The shared chat template maps to the inference service on the selected target. The `frank-npu` endpoint is retired ([[frank-npu]]) and has no deploy handler; an unrelated large-model deployment never touches it. ## Caddy safety Caddy runs on Dankclaw and fronts both hosts, including Gitea. The former standalone `gitea-caddy` Quadlet is retired; the canonical compose `caddy` service is now the sole reverse-proxy owner of ports 80/443. A Caddy reload failure falls back to a restart of **only** the selected `caddy` container. Before deploying, verify that no retired proxy still owns either port. Homepage files, generated `deployed.js`, the status checker, and the rendered Caddyfile are updated only when their service is selected. A media-only Dankserver deployment does not touch Dankclaw's proxy or homepage. ## Per-service state The remote runner writes one revision file per successfully completed service to: ```text ${XDG_STATE_HOME:-~/.local/state}/serverAppConfigs/deploy-state/ ``` The directory is outside the checkout and is never committed. A failed handler is not marked successful. On the next ordinary deployment, the runner compares the recorded revision with the new checkout and retries only a still-changed service. This prevents a failed Caddy or LLM deployment from being silently forgotten while avoiding a host-wide restart. ## Targets | Target | Host | Repository checkout | |---|---|---| | dankclaw | `dank@dankclaw` (`192.168.0.143`) | `/home/dank/serverAppConfigs` | | dankserver | `dank@dankserver` (`192.168.0.235`) | `/home/dank/serverAppConfigs` | SSH uses the existing key pair. No password is stored or documented in this repository. If sudo is needed on a headless target, `FRANK_SUDO_PW` is read from the environment and is never written to the repository. ## Tests Run the full suite before pushing: ```bash ./tests/run.sh ``` The deployment seams are in `tests/deployment_scoping.bats` and `tests/deployment_flow.bats`. They cover path isolation, compose narrowing, Caddy safety, inference isolation, per-service state, and legacy wrapper routing. See [[Testing Policy]]. ## See Also - [[Testing Policy]] - [LLM deployment](../LLM/deployment.md) - [[Caddy]] - [[SearXNG]]