Skip to content

auberge deploy

Deploy one or more apps — or a whole Host composition — to a host. Hardening runs first automatically, then infrastructure dependencies are resolved. Alias: auberge dp.

Terminal window
auberge deploy [apps...] [OPTIONS]
Option Description Default
apps... App or playbook names (positional, variadic) Interactive
-H, --host HOST Target host Interactive
--all Deploy every apps.yml app false
-C, --check Dry-run (ansible check mode) false
-f, --force Skip confirmation prompt false
--verify-public-dns Verify each app’s public A record after deploy false
Terminal window
auberge deploy # interactive
auberge deploy paperless freshrss --host prod
auberge deploy --all --host prod -f # CI/CD
auberge deploy paperless --check # preview
auberge deploy paperless --host prod --verify-public-dns
auberge deploy ruche --host ruche # a whole composition

Every deploy runs hardening → infrastructure → apps in that order. Hardening (firewall, fail2ban, kernel) is mandatory and untagged. To skip it, use auberge ansible run directly.

The CLI shows the resolved plan before running (suppress with -f):

Execution plan:
Host: prod (203.0.113.10)
→ hardening
→ infrastructure
→ apps (tags: paperless, freshrss)

A name outside the apps.yml roster resolves to the standalone playbook of that name and runs after Substrate:

Execution plan:
Host: ruche (203.0.113.42)
→ hardening
→ infrastructure
→ ruche

ruche is the agent tier’s composition — machine-user identity, the OpenCode runtime and the aoe dashboard. A composition run pulls infrastructure.yml in for the same reason an app does: the dashboard is unreachable until Caddy holds a certificate for its zone (ADR-0075).

[!IMPORTANT] Every entry on ruche.yml — and on aoe.yml and opencode.yml — is guarded on when: "'agent' in group_names", so the Host must carry tags = ["agent"] in hosts.toml or the run is a green no-op that installs nothing. Check it with auberge host list; the TAGS column must show agent. This is the hosts.toml tags field (an ansible group, deciding which roles run) — not tailnet_tag, which happens to take the same word for the ACL tier and decides what the Host may reach on the tailnet. They are unrelated fields (ADR-0062).

bootstrap is deliberately not a deploy target, because it is a lifecycle operation rather than a convergence:

Playbook Run it with Why not deploy
bootstrap auberge ansible bootstrap connects as root on port 22 before the ansible user exists

A name that is both an apps.yml role and a standalone playbook (calibre, immich, gokapi, hermes) keeps going through the roster, which is where those apps have always deployed from.

auberge deploy with no positional args offers every target — the apps.yml roster in declaration order, then the standalone playbooks the roster does not already hold, marked as such:

Select app(s) or playbook(s) to deploy (space to toggle, enter to confirm)>
[all apps]
paperless
freshrss
…
aoe (playbook)
memsearch (playbook)
opencode (playbook)
ruche (playbook)

The marker reads (playbook) and not (composition) because only ruche is a Composition — a Playbook whose roster is several Apps. aoe, opencode and memsearch are plain standalone Playbooks, and one word has to cover all four.

[all apps] selects the roster and nothing else — the same set --all deploys. A playbook is opt-in by name, because a deploy builds its Preflight for the whole plan before the first task runs: sweeping ruche into [all apps] would make every host in the fleet demand aoe_passphrase, opencode_openrouter_api_key and agents_domain (ADR-0077). Picking [all apps] alongside individual entries deploys the union.

Substrate Apps (Caddy, Blocky, Headscale) are declared in infrastructure.yml, not apps.yml, so they are not valid deploy targets — they deploy on every auberge deploy <app> run instead. To push one alone:

Terminal window
auberge ansible run -t blocky

After each app’s playbook run (not in --check). A run’s apps are its tags where it has them, and its roster where it does not — so a composition is checked through the apps on it:

  • Tailnet-only apps (apps declaring {app}_tailscale_ip): the tailnet’s Blocky is queried on UDP/53 and must answer with the app’s own address. Blocky is the host whose config answers blocky_subdomain, at the tailscale_ip that auberge host detect-tailscale-ip cached for it — a different host from the app’s as soon as the fleet grows.
  • Public apps: opt-in via --verify-public-dns. 1.1.1.1 is queried; the A record must match ansible_host. Opt-in because Cloudflare propagation can lag.

A mismatch aborts the deploy:

DNS mismatch for essaim.example.com: queried 100.64.0.1, expected 100.64.0.9, got [100.64.0.1]

So does not knowing which host answers for the tailnet — a tailnet-only app’s records live in that Blocky or nowhere, so an unanswerable check is a failed one:

Cannot verify essaim.example.com on the tailnet: 2 Hosts answer `blocky_subdomain` (auberge, ruche), but the tailnet has one resolver (ADR-0052); withdraw the gate on the others with `[hosts.<name>] blocky_subdomain = ""`