Examples
Three ready-to-copy starters. Each is a small set of files you drop into your own repo, they keep your config separate from the shared roles, which install from Ansible Galaxy via requirements.yml.
| Starter | Directory | Where vars live |
|---|---|---|
| One machine | examples/single/ | playbook.yaml |
| Multiple machines | examples/multiple/ | one inventory.yaml |
| Many machines | examples/many/ | group_vars / host_vars |
All three drop into your own repo. They differ in where vars live and how roles vary per machine: Multiple machines uses one play per host, Many machines drives roles as data, see Configuration.
Start with One machine and graduate when a second machine appears. Each file below is also downloadable directly, e.g. curl -LsSf https://dotfiles.pro/examples/single/playbook.yaml.
Each starter also vendors an ./install (the same script as dotfiles.pro/install.sh), so once you push your repo a fresh machine bootstraps with git clone <your-repo> && cd <your-repo> && ./install, no Ansible needed first.
One machine
Everything in playbook.yaml.
Requirements
The requirements file requirements.yml pulls the collection from Ansible Galaxy.
---
collections:
- name: dotfilespro.rolesPlaybook
The Ansible playbook playbook.yaml lists the roles to run plus your identity and repos.
---
- name: Configure my machine
hosts: localhost
connection: local
gather_facts: true
diff: true
roles:
# OS base: the matching one runs, the other is skipped (facts decide), so this
# same playbook applies on a Mac or an Ubuntu box. Listed as roles (not a
# templated name) so each role still tags its own tasks for --tags runs.
- role: dotfilespro.roles.ubuntu
when: ansible_facts.os_family == 'Debian'
- role: dotfilespro.roles.macos
when: ansible_facts.os_family == 'Darwin'
- dotfilespro.roles.dotpro
- dotfilespro.roles.ssh
- dotfilespro.roles.git
- dotfilespro.roles.starship
- dotfilespro.roles.docker
- dotfilespro.roles.kubernetes
# Add any other roles you want — see the Roles table in the docs.
# Replace the placeholders below with your own identity and repos.
vars:
git_config:
user:
name: Your Name
email: you@example.com
git_repositories_path: /projects/ # default is ~/
git_repositories:
project: git@gitlab.com:yourname/project.git # cloned to /projects/project
.dotfiles:
repository: git@gitlab.com:yourname/mydotfiles.git
path: ~/.dotfiles # absolute and ~ paths ignore git_repositories_pathConfiguration (Ansible)
The optional (but recommended) ansible.cfg enables the Mitogen speedup and hash_behaviour = merge.
[defaults]
# Append a full log of every run to this file, for later auditing
log_path = ~/.ansible/audit.log
# Run tasks through Mitogen for a large speedup over the default executor
strategy = mitogen_linear
# Skip the expensive fact groups (hardware probing, network interface enumeration,
# virtualization, ohai/facter); the roles only read os/distribution, env, user, and
# date facts, so a normal apply gathers far less up front
gather_subset = !hardware,!network,!virtual,!ohai,!facter
# Deep-merge dict vars across files instead of replacing them wholesale
hash_behaviour = merge
# Render task output as readable YAML blocks instead of dense single-line JSON.
# (community.general.yaml was removed in 12.0.0; use the built-in default's
# callback_result_format, the default callback's own option, not result_format.)
stdout_callback = default
callback_result_format = yaml
# Quieter runs: report only changed/failed tasks and their diffs (hide ok/skipped)
display_ok_hosts = False
display_skipped_hosts = False
# A single-host setup plays implicit localhost without an inventory file; that is
# by design, so silence the two warnings pointing it out on every run
localhost_warning = False
[inventory]
inventory_unparsed_warning = FalseMultiple machines
Multiple machines sharing one inventory.yaml; per-host vars override the shared defaults, and one play per host lets each machine run a different set of roles.
Requirements
---
collections:
- name: dotfilespro.rolesPlaybook
The Ansible playbook playbook.yaml is a shared hosts: all baseline plus one play per host; run with --limit $(hostname).
---
# One playbook, multiple plays: a shared baseline, then one play per host so
# each machine can run a different set of roles. Per-host vars still come from
# the inventory. Run the whole file; each play only touches its own host(s).
#
# gather_facts is set globally in ansible.cfg (gathering = smart); diff is set
# per play with diff: true (a global [diff] always would override the roles' own
# diff: false opt-outs, so keep diff at the play level).
- name: Baseline — every machine
hosts: all
connection: local
diff: true
roles:
# OS base: the matching one runs, the other is skipped (facts decide). Listed
# as roles (not a templated name) so each still tags its own tasks.
- role: dotfilespro.roles.ubuntu
when: ansible_facts.os_family == 'Debian'
- role: dotfilespro.roles.macos
when: ansible_facts.os_family == 'Darwin'
- dotfilespro.roles.dotpro
- dotfilespro.roles.ssh
- dotfilespro.roles.git
- dotfilespro.roles.starship
- name: Work-laptop only
hosts: work-laptop
connection: local
diff: true
roles:
- dotfilespro.roles.docker
- dotfilespro.roles.kubernetes
- name: Home-pc only
hosts: home-pc
connection: local
diff: true
roles:
- dotfilespro.roles.docker
# Add any other roles you want — see the Roles table in the docs.
# For roles-as-data across a larger fleet, see examples/many/.Variables
Inventory
The inventory inventory.yaml holds shared all vars, with per-host overrides.
---
all:
# Shared across every machine:
vars:
ansible_connection: local
git_config:
user:
name: Your Name
email: you@example.com
git_repositories:
.dotfiles: git@gitlab.com:yourname/mydotfiles.git # key = clone path under ~/
hosts:
work-laptop:
git_config:
user:
email: you@work.com # merges with all.vars, overrides email only
git_repositories_path: /work/projects/
git_repositories: # merged with all.vars repos
work/work-project: git@gitlab.com:yourorg/work-project.git
home-pc:
git_repositories:
personal: git@gitlab.com:yourname/personal.gitConfiguration (Ansible)
The same ansible.cfg as above, plus the extras shown below: it points at inventory.yaml and caches gathered facts (gathering = smart).
# Same as the One machine ansible.cfg above, plus:
# Default inventory file, so you can omit -i on every command
inventory = inventory.yaml
# Gather facts once per host and cache them, so the per-host plays reuse the
# baseline play's facts instead of re-gathering. No need for gather_facts: true.
gathering = smartMany machines
Once you run enough of your own machines that one play per host gets unwieldy, drive the roles as data. The ansible role is the finalizer: it applies ansible_roles_all (the baseline every machine gets, in group_vars/all.yaml) merged with each host's roles_extra (in host_vars/<hostname>.yaml), so each machine runs a different role set from one host_vars file, the playbook never changes as you add machines.
Requirements
---
collections:
- name: dotfilespro.rolesPlaybook
The Ansible playbook playbook.yaml is a single play that runs the ansible finalizer.
---
- name: Configure machine
hosts: all
connection: local
gather_facts: true
diff: true
roles:
# OS base, applied per host from facts (the other role is skipped): every
# machine needs exactly one, so it lives here rather than in ansible_roles_all.
# Listed as roles so each still tags its own tasks for --tags runs.
- role: dotfilespro.roles.ubuntu
when: ansible_facts.os_family == 'Debian'
- role: dotfilespro.roles.macos
when: ansible_facts.os_family == 'Darwin'
# The `ansible` role is the finalizer: it installs Ansible, then applies the
# roles in ansible_roles_all (baseline) merged with each host's roles_extra —
# so different machines run different roles from this one repo.
- dotfilespro.roles.ansibleVariables
Inventory
The inventory inventory.yaml has one entry per machine, keyed by hostname.
---
all:
hosts:
# One entry per machine, keyed by its real hostname (run: hostname).
# host_vars/<hostname>.yaml is loaded automatically when present; a host
# without one just gets the ansible_roles_all baseline from group_vars/all.yaml.
# ansible_connection: local is set once in group_vars/all.yaml.
# Run on each machine with --limit $(hostname).
work-laptop: # has host_vars/work-laptop.yaml (extra roles + work identity)
home-pc:
macbook:
dev-server:
build-box:
cloud-vm:Group vars
The baseline every machine gets lives in group_vars/all.yaml.
---
# OS-neutral baseline roles every machine gets. The `ansible` role applies these,
# merged with each host's `roles_extra` (set in host_vars). The OS base (ubuntu or
# macos) is not in this list — each machine needs a different one, so it's applied
# per host in playbook.yaml, picked from facts.
ansible_roles_all:
- dotpro
- ssh
- git
- starship
ansible_connection: local
git_config:
user:
name: Your Name
email: you@example.com
git_repositories_path: /projects/ # default is ~/
git_repositories:
.dotfiles: git@gitlab.com:yourname/mydotfiles.git # cloned to /projects/.dotfilesHost vars
One host's extra roles and overrides live in host_vars/work-laptop.yaml.
---
# Filename matches the machine's hostname (run: hostname). Dicts merge with
# group_vars/all.yaml; lists replace, so this host's extra roles go in roles_extra.
# This machine's extra roles, merged onto the baseline:
roles_extra:
- wsl
- docker
- kubernetes
- googlecloud
- terraform
git_config:
user:
email: you@work.com
git_repositories:
work/work-project: git@gitlab.com:yourorg/work-project.gitConfiguration
Toolchain
The toolchain file pyproject.toml pins ansible-core across all your machines (uv install path).
# uv project for the dotfiles control plane — the fleet (`many`) starter ships one
# so every machine can pin the same ansible-core; the `single` / `multiple` starters
# omit it and let the installer resolve the floor. Declares the ansible-core floor
# the collection needs. To pin a version across your fleet, run `uv lock` and commit
# the generated uv.lock to your own repo (intentionally not committed here — this is
# a template). Used by the dotfiles.pro installer's uv path (the default on every
# OS; DOTPRO_ANSIBLE_METHOD=apt opts into Ubuntu 24.04+ system packages instead).
[project]
name = "dotfiles-bootstrap"
version = "0.0.0"
requires-python = ">=3.11"
dependencies = [
"ansible-core>=2.16",
# "mitogen", # optional speedup — also set strategy=mitogen_linear and
# # interpreter_python=/usr/bin/python3 in ansible.cfg (Linux).
]Ansible
The optional (but recommended) ansible.cfg points at inventory.yaml.
[defaults]
# Append a full log of every run to this file, for later auditing
log_path = ~/.ansible/audit.log
# Run tasks through Mitogen for a large speedup over the default executor
strategy = mitogen_linear
# Skip the expensive fact groups (hardware probing, network interface enumeration,
# virtualization, ohai/facter); the roles only read os/distribution, env, user, and
# date facts, so a normal apply gathers far less up front
gather_subset = !hardware,!network,!virtual,!ohai,!facter
# Deep-merge dict vars across files instead of replacing them wholesale
hash_behaviour = merge
# Default inventory file, so you can omit -i on every command
inventory = inventory.yaml
# Render task output as readable YAML blocks instead of dense single-line JSON.
# (community.general.yaml was removed in 12.0.0; use the built-in default's
# callback_result_format, the default callback's own option, not result_format.)
stdout_callback = default
callback_result_format = yaml
# Quieter runs: report only changed/failed tasks and their diffs (hide ok/skipped)
display_ok_hosts = False
display_skipped_hosts = False