Why a role beats a symlink: idempotent config the Ansible way
Read almost any dotfiles tutorial and the mechanism is the same: keep your config in a git repo, then symlink each file into place. ln -s ~/dotfiles/gitconfig ~/.gitconfig, or GNU Stow, or dotbot, or a bare git repo checked out over $HOME. It is simple, it is universal, and for a single file it is hard to beat.
It also has a ceiling, and you hit it the moment your config has to survive a software update, vary by machine, or sit next to the packages those files configure. That is where a role earns its keep.
What a symlink actually does
A symlink makes the repo file the file. ~/.gitconfig is now a pointer to ~/dotfiles/gitconfig, and whatever is in that file is your entire git configuration. You own the whole file, all of it, forever.
That is the catch hiding in plain sight. Owning the whole file sounds like control, but most config files are not yours alone. The software writes to them too.
The trouble with owning the whole file
Frozen defaults. Git, your editor, and your shell ship better defaults with every release. A symlinked whole file overrides all of them with whatever you committed once, years ago. You do not get the improvements; you get a fossil.
All or nothing. A .gitconfig you fully own also owns the keys you never meant to manage, and it collides with the keys the tool writes itself (git maintenance, credential helpers, git lfs install). The program edits its file, you re-pull yours, and one of you loses.
Conflicts and cleanup. Symlink farms break on contact with a real file. Stow simply refuses:
$ stow git
WARNING! stowing git would cause conflicts:
* existing target is neither a link nor a directory: .gitconfigSo you move files out of the way by hand first, and when you later drop a package you go hunting for the dangling links it left behind.
Static. A symlink is one file for every machine. The moment you need a different identity at work, a key only on Linux, or a value computed at apply time, you bolt a templating preprocessor onto the front and you are back to generating files anyway.
Files only. A symlink manager moves files. It does not install the packages those files configure, so you still keep a separate install.sh beside it, two tools and two mental models for one machine.
What a role does instead
Where software co-writes its own config, a role manages the keys you declare through the program's own mechanism, git config, osx_defaults, kubeconfig stanzas, rather than owning the whole file. You declare your identity as data:
- hosts: localhost
# ...
vars:
git_config:
user:
name: Your Name
email: you@example.com
git_repositories:
.dotfiles:
repository: git@gitlab.com:yourname/mydotfiles.git
path: ~/.dotfilesThe git role writes exactly those keys and leaves every other line of ~/.gitconfig alone, so the tool's own defaults and its runtime writes survive across updates. This is the settings, not files idea, and it is the opposite of owning the file: you own the handful of keys you actually care about, and nothing else.
Because the config is computed from your vars rather than copied, it can also vary. The same declaration merges and templates into a work identity under ~/work/, a different key on Linux, or a value pulled from a fact, with no preprocessor in front.
Idempotent, and the whole machine
A role does not stop at files. The same apply installs the packages, writes the config, clones the repos, and sets up the shell, in one run, and you can re-run it any time:
dotpro apply # install + configure this machine
ansible-playbook playbook.yaml --check --diff # or a dry run that changes nothingWhen the machine already matches what you declared, nothing changes. A first install and a routine update are the same operation, and --diff previews every key a run would touch before it touches it. There is no second imperative script to keep in sync, and no order-of-operations to remember.
Symlinks still have their place
This is not anti-symlink dogma. Some files genuinely are yours end to end: a tool with no key-level config, a file you author by hand, anything an editor should open as the repo file. For those, link them, and Dotfiles Pro does exactly that. The dotfiles role links, copies, or templates the raw files you list straight from your repo:
---
# playbook.yaml — the provisioning layer, at the repo root beside the raw dotfiles,
# which stay usable on their own (a symlink, stow, or a plain copy). This playbook
# owns everything around them: packages, system config, and the repositories to clone.
- name: Provision this machine
hosts: localhost
connection: local
gather_facts: true
diff: true
vars:
dotpro_limit_host: false # default: true; single-host repo
# The raw dotfiles to link into $HOME. The dotfiles role symlinks each one from
# the repo root (dotfiles_dir defaults to dotpro_path), so there is no inline task
# to write: the files stay the source of truth, the role links them.
dotfiles_list: # default: []
- .bashrc
- .gitconfig
- .config/starship.toml
roles:
# OS base: the matching one runs, the other is skipped (facts decide)
- 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.dotfiles
- dotfilespro.roles.git
- dotfilespro.roles.dockerIt even backs up a pre-existing file before adopting it and prunes the links for entries you drop, the cleanup the bare symlink approach leaves to you. And the config roles that write settings can write through a symlink (follow: true), so a file the dotfiles role links into your repo is still updated in place. You pick settings-driven or verbatim-symlinked per file, in one tool.
The rule of thumb
The line is simple: if the software co-writes the file, manage the settings; if you own the whole file, symlink it. A symlink hands the program a frozen file and hopes it behaves. A role declares the keys you want and lets the program keep the rest. One of those survives the next update, varies per machine, and comes with the packages attached.
See how this compares to the file-only tools in Alternatives, or read Settings, not files for the mechanism in full.