Skip to content

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:

echo "your-real-vault-password" > .vault-pass
chmod 600 .vault-pass

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:

ansible-playbook -i inventory/hosts.yml playbooks/site.yml --ask-vault-pass

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:

/usr/local/sbin/proxmox-restore-check.sh <source-vmid>

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.