- Shell 35.5%
- Jinja 23.6%
- Lua 22.2%
- Python 17.8%
- Just 0.9%
Full rewrite around What / Bootstrap / Daily use / How it works / Gotchas. Fix everything that no longer matched the repo: boot-incus/boot-do/boot-mp -> boot-remote, drop the non-existent all_roles.yml and the --tags all,never and host_skip_* sections, correct the [homelab]/[vps] -> [rsync_net] groups and the consolidated group_vars/vm.yml, and the GUI-only 1Password push model. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|---|---|---|
| docs/decisions | ||
| inventory | ||
| lib | ||
| playbooks | ||
| plugins/inventory | ||
| roles | ||
| scripts | ||
| .ansible-lint | ||
| .gitignore | ||
| .yamllint | ||
| ansible.cfg | ||
| justfile | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
| requirements.yml | ||
| setup | ||
| vault | ||
Bootstrap & Dotfiles (Ansible)
Ansible for two jobs, sharing one set of roles, one inventory format, and one
just driver:
- Local workstation — bring a fresh macOS/Linux desktop up right after the OS install, then keep its dotfiles, packages, and shell in sync over time.
- Headless remote hosts — provision homelab boxes, VPSes, and disposable VMs (Incus, Multipass, DigitalOcean) and re-converge them on demand — all pushed from your workstation, which holds the secrets.
Supported platforms
| Platform | Used as | Notes |
|---|---|---|
| macOS (Apple Silicon) | workstation | App Store login needed for mas |
| Fedora | workstation, homelab | |
| Debian / Ubuntu | workstation, homelab, VPS | bare hosts via bootstrap_provision.yml |
| FreeBSD (rsync.net) | special-case | via bootstrap_rsync_net.yml |
Bootstrap
Local workstation
git clone https://codeberg.org/bradfrank/dotfiles.git ~/dotfiles
cd ~/dotfiles
. ./setup # MUST be sourced — it activates .venv and exports ANSIBLE_* into your shell
setup prompts for a hostname and sets it on the system, installs Python +
Homebrew, builds .venv from requirements.txt, generates
host_vars/<hostname>.yml and opens it in $EDITOR (populate host_secrets:
and save), then hands off to just boot-local, which runs in order:
just onepassword— install the 1Password app + CLI, enable CLI integration.just vault— pull secrets from 1Password into the encrypted vault.just role base --ask-become-pass— converge the base role.
Remote hosts
Remote hosts are configured by pushing from your workstation, so they never
need 1Password themselves. This requires a vault on your workstation already
(from a prior just vault) — secrets resolve locally and push over SSH.
A brand-new bare Debian/Ubuntu host (no user yet) needs its account + SSH access created first:
ansible-playbook playbooks/bootstrap_provision.yml -i '<ip>,' -u root
Then bring any remote host to a converged state with boot-remote <inventory> <instance> — it installs Homebrew, then runs the base role via the matching
inventory:
just boot-remote incus <instance> # Incus VM/container
just boot-remote digitalocean <instance> # DigitalOcean droplet
just boot-remote multipass <instance> # Multipass VM
just boot-remote remote <hostname> # static host in inventory/remote.ini
<instance> is the name as it appears in that inventory — list them with
ansible-inventory -i inventory/<name>.yml --graph.
rsync.net (FreeBSD) is a one-shot (FreeBSD Python install + the rsync_net
role only — no Homebrew, no general bootstrap):
ansible-playbook playbooks/bootstrap_rsync_net.yml -i inventory/remote.ini --limit de3481b
Daily use
just dots [args] # dotfiles/configs only (base role, --tags dots)
just moredots [args] # + plugins/caches/extras (--tags dots,moredots)
just role <name> [args] # run one role/playbook end-to-end
just vault # regenerate the vault from 1Password
just ssh-config # rebuild ~/.ssh/config from the incus/multipass/DO inventories
just lint # ansible-lint over roles/
just sc # shellcheck the setup script
just role <name> runs playbooks/<name>.yml — covering the reusable roles
(base, cloud, docker, virt) and the bespoke plays (ssh_config,
do_nagios_server). Args pass through, e.g. just dots -i inventory/remote.ini -l memory-alpha.
Opt-in tasks are tagged never and skipped by default; request them by tag:
hardening / host_keys (ssh), and dock / volumes / sleep / firewall
(macOS) — e.g. just role base --tags hardening.
How it works
A few load-bearing design choices — worth knowing before you edit.
Three layers
| Layer | Job | Note |
|---|---|---|
setup (sh) |
bare OS → "can run Ansible": Python, Homebrew, .venv, env vars |
source it, once per host |
just <target> |
wraps ansible-playbook with the right inventory/tags/flags |
daily driver — see justfile |
| playbooks / roles | the actual config | playbooks/ (run-once = bootstrap_-prefixed), roles/, shared helpers in lib/ |
setup aborts if executed instead of sourced, because it activates .venv and
exports ANSIBLE_* for the current shell.
Hostname is identity
The hostname selects the host_vars/<hostname>.yml to load, the 1Password entry
to look up, and the SSH config block to write. setup asks for it and sets it
on the system so the convention holds from the start.
Inventories
inventory/local.py— returns the local host; theansible.cfgdefault.inventory/{incus,multipass,digitalocean}.yml— dynamic plugins that enumerate running instances on demand; their hosts sharegroup_vars/vm.yml.inventory/remote.ini— static hosts (currently[rsync_net]).host_vars/<hostname>.yml— per-host knobs, generated byjust hostvarsthen hand-edited.
Secrets: 1Password → Ansible Vault
just vault (part of boot-local) runs bootstrap_import_vault.yml to pull
secrets from 1Password into inventory/group_vars/all/vault.yml, encrypted with
a per-host vault password (also fetched from 1Password). The vault is
generated, never committed.
- 1Password runs only on the GUI workstation you drive Ansible from
(
bootstrap_onepassword.ymlinstalls the app + CLI and enables integration). Headless hosts are pushed to from there, so they never authenticate 1Password. - Each host needs a 1Password item named after its hostname, with an
ansible_vaultfield holding its vault password.
Gotchas
Warning
Biometric
sudo(e.g.fprintd) breaks plays run locally — see ansible/ansible#80883. Workaround: setansible_host: localhostin host_vars so Ansible connects over SSH instead of running locally.
Note
The Multipass inventory plugin needs both
inventory_plugins = ./plugins/inventoryandenable_plugins = multipass, …inansible.cfg; drop either and Multipass VMs silently vanish from inventory.
Tip
Colorscheme: Solarized Light. Every attempt to switch has ended back here.