Deployment¶
Exact commands to go from zero to a running homelab. Run everything from the
ansible/ directory unless stated otherwise.
Prerequisites¶
# Linters used by `make lint` — must be on PATH
pipx install ansible-lint yamllint
# community.proxmox.proxmox_kvm (the proxmox_vm role) needs these on the
# controller; not pulled in by the collection install below. Needed for any
# real provision-vms.yml run, not just for the offline test/lint gate.
pip install proxmoxer requests --break-system-packages
cd ansible
ansible-galaxy collection install -r requirements.yml
Vault password¶
ansible.cfg sets vault_password_file = .vault-pass, so a populated,
git-ignored .vault-pass file means no vault flag is needed on any command
below:
Without it, pass --ask-vault-pass (or --vault-password-file) on every
command that touches vault-encrypted vars.
Populate secrets¶
inventory/group_vars/all/vault.yml is the live file;
vault.yml.example lists every vault_* key a full deploy needs (see
Vault variables).
ansible-vault encrypt_string --name vault_cloudflare_dns_api_token 'the-real-token'
# paste the resulting `vault_<name>: !vault |` block into vault.yml,
# repeating for every key in vault.yml.example
# or edit the whole file in place:
ansible-vault edit inventory/group_vars/all/vault.yml
Sanity checks before deploying¶
ansible-inventory -i inventory/hosts.yml --graph
ansible-playbook -i inventory/hosts.yml --syntax-check playbooks/site.yml
make test # tests/test_*.sh: structure, rendering, lint
make lint # yamllint . && ansible-lint
Full deploy¶
site.yml runs the five playbooks below in order via import_playbook, so
a single invocation does the whole thing:
Or run each stage individually, in this order, for more control:
# 1. Build the cloud-init template if absent; clone swarm-core and the
# appliance VMs; import the HAOS qcow2 for Home Assistant.
ansible-playbook -i inventory/hosts.yml playbooks/provision-vms.yml --ask-vault-pass
# 2. common + docker + the matching appliance role (pihole/nextcloud/unifi)
# on each appliance VM.
ansible-playbook -i inventory/hosts.yml playbooks/configure-appliances.yml --ask-vault-pass
# 3. common, nfs_client, docker, swarm, db_backup on swarm-core and
# swarm-media: installs Docker, mounts NFS, inits/joins the Swarm,
# applies tier labels, creates the shared traefik overlay network,
# installs the DB-dump timer.
ansible-playbook -i inventory/hosts.yml playbooks/bootstrap-swarm.yml --ask-vault-pass
# 4. Deploys every stack whose tier is in deploy_stack_tiers (default: all
# four — core, apps, media, experimental).
ansible-playbook -i inventory/hosts.yml playbooks/deploy-stacks.yml --ask-vault-pass
# 5. Installs the Proxmox VM backup (vzdump) timer on the Proxmox host.
ansible-playbook -i inventory/hosts.yml playbooks/configure-proxmox-backups.yml --ask-vault-pass
Deploying a subset of stacks¶
Useful when swarm-media (physical, holds the media/experimental tiers) is
offline:
ansible-playbook -i inventory/hosts.yml playbooks/deploy-stacks.yml \
--ask-vault-pass \
-e '{"deploy_stack_tiers": ["core", "apps"]}'
Valid tier values: core, apps, media, experimental.
Rotating or correcting a Swarm secret¶
Docker secrets are immutable. A plain deploy-stacks.yml re-run fails
outright the moment a vault.yml value changes for an already-deployed
stack (Docker refuses to remove a secret its running service still
references) — it does not silently keep the old value. Push the correction
through explicitly:
ansible-playbook -i inventory/hosts.yml playbooks/deploy-stacks.yml \
--ask-vault-pass \
-e '{"stack_force_secret_rotation": true}'
This is global to the run, not scoped to one stack: every stack in scope
(per deploy_stack_tiers) is torn down (docker stack rm), has its secrets
recreated, and is redeployed — expect full downtime across the whole
catalogue for the run's duration, not just a rolling restart of the one
stack whose secret actually changed. There is no per-stack selector; narrow
the blast radius with deploy_stack_tiers if only one tier needs the
correction. See
Gotchas.
Limiting to specific hosts¶
Standard Ansible --limit, not special-cased by any playbook here:
ansible-playbook -i inventory/hosts.yml playbooks/bootstrap-swarm.yml \
--ask-vault-pass --limit swarm-core
Post-deploy manual steps¶
Not automated by Ansible — see each service's own page for detail:
| Step | Command / location |
|---|---|
| Gitea OIDC source | docker exec -u git <gitea-task> sh /oidc-bootstrap.sh — see Gitea |
| Fleet SSO org settings | fleetctl gitops -f /opt/stacks/fleet/fleet-sso.yml — see Fleet |
| Snipe-IT SAML | Admin UI, Settings > SAML — see Snipe-IT |
| Matomo LoginOIDC plugin | Admin UI — see Matomo |
| Nextcloud AIO first-run wizard | AIO interface, port from nextcloud_aio_interface_port |
| OpenProject/Matomo first-run wizards | Each service's own admin UI |
Test suite and linters¶
make test # loops tests/test_*.sh, exits non-zero on first failure
make lint # yamllint . && ansible-lint
make syntax # ansible-playbook --syntax-check playbooks/site.yml
make inventory # ansible-inventory --graph
make molecule # requires Docker; runs `molecule test` per role in MOLECULE_ROLES
# (cloud_init_template, home_assistant, proxmox_vm need a live
# Proxmox host and are excluded)
The offline gate (make test, make lint) runs in CI on every push and
needs no Docker or live Proxmox API — it checks structure, template
rendering, and lint cleanliness. Molecule scenarios need Docker (and, for the
three excluded roles, a live Proxmox API) and are not run in this
environment; they should run in CI on a Docker-capable runner.
Proxmox backup restore drill¶
Manual, not on any timer or playbook — run on the pve host itself:
Restores the newest archive for <source-vmid> into a scratch VM
(proxmox_backup_restore_check_vmid, default 9000), verifies it reaches
running, then destroys the scratch VM. See Backups.