Posted on: 2025-07-19, Updated on: 2025-07-20 04:44:39 | Read time: 11.1 minutes
Topic(s): unix/linux
On UNIX-like systems, dotfiles are files or directories with names that contain a preceding dot. For example: .gitignore
or .config
. Many file management utilities treat these files as hidden and are only shown upon specific request from the user. Dotfiles located in the user's home directory often contain textual configuration information that affects the behavior of installed software. Running the command: ls -la ~/.*
will give you a list of all the dotfiles and directories in your user's home directory and their contents (in the case of directories).
While managing dotfiles is often done indirectly through graphical programs, it is a common manual task for more tech-savvy users of UNIX-like systems such as GNU/Linux, FreeBSD, or OpenBSD. These users often prefer to use their carefully crafted configuration dotfiles on other machines as well. One popular way of achieving this is by using a VCS (Version Control System) such as Git. Using a VCS enables tracking of configuration changes and synchronization using a remote repository. The Arch Linux Wiki demonstrates how this can be accomplished using the “bare repository and alias method” with Git.
Ansible is an open-source SCM (Software Configuration Management) tool. Ansible automates tasks such as software updates, system configuration, and software deployment across IT infrastructures at any scale. The desired state of the local or remote system is set in human-readable YAML text files called playbooks.
Most SCM systems use similar concepts, and the following list presents those defined by Ansible:
Using a configuration management tool like Ansible will make dealing with config variations among machines much easier. While some host-specific configuration could be accomplished using shell scripts, it is more difficult when dealing with text-based configuration files. Ansible provides the ability to template configuration files and provide conditional processing using rules in a playbook. Ansible's large selection of built-in modules can make common configuration tasks much easier to perform. For example, the ansible.builtin.git
module can be used to clone dotfile repositories, such as one for GNU/Emacs. It could even be used to clone all of your remote repositories hosted on a service like GitHub. Using Ansible will also give you a chance to practice with a tool that is used for large-scale IT automation. While we may not use Git as our means of synchronizing our dotfiles, we will still utilize Git as the means to track changes to our Ansible project.
The process for installing Ansible differs based on which operating system you are using. Thankfully, Ansible provides support for many different platforms, including basically any UNIX-like system with a Python installation. Typically, Ansible is installed on a single machine referred to as the “control node,” which issues commands from playbooks over remote SSH connections to multiple machines. For the sake of simplicity, we will simply install Ansible locally on each of our machines that we intend to deploy our dotfiles.
Ansible can be installed using a Python virtual environment. We begin by creating a directory to store our Ansible project files as well as the Python virtual environment:
mkdir -p ~/Projects/ansible.<YOURSITE.COM> # Replace YOURSITE.COM with your desired name
cd ~/Projects/ansible.<YOURSITE.COM>
Now create and activate the Python virtual environment:
python3 -m venv ./venv
source ./venv/bin/activate
Now install Ansible and view its current version number:
pip install ansible
pip freeze > requirements.txt
ansible --version
You can then exit the Python virtual environment by running the deactivate
command.
Our Ansible project will have the following directory structure:
ansible.yoursite.com/ — Root of the Ansible repository
├── group_vars/ — Variables defined per group of hosts
├── host_vars/ — Variables defined per individual host
├── roles/ — Directory for reusable Ansible roles
│ └── dotfiles/ — Role for managing dotfiles
│ ├── defaults/ — Default variables for all hosts
│ │ └── main.yml — Default values for variables
│ ├── files/ — Static files to copy to hosts
│ │ └── dotfiles_root/ — Files to copy into your home directory
│ │ ├── .bash_profile
│ │ └── .tmux.conf
│ ├── tasks/ — Tasks for configuring systems
│ │ └── main.yml — Primary tasks for dotfiles role
│ ├── templates/ — Template files with dynamic content
│ │ └── .bashrc.j2
│ └── vars/ — Role-specific variables
│ └── linux.yml — Variables for Linux systems
└── workstations.yml — Playbook to run the dotfiles role on the localhost
The directory layout shown above is a slightly slimmed-down version of the one presented here. Using this recommended directory structure will not only ensure you are following best practices, but it will make it easier for you to extend this dotfiles Ansible project to perform other automation tasks.
The first YAML configuration file we will create is the workstations.yml
, which contains the playbook for deploying the dotfiles using its dedicated role.
workstations.yml:
---
# Playbook for deploying dotfiles on an individual host
- name: Deploy dotfiles
hosts: localhost
roles:
- dotfiles
collections:
- ansible.posix # Used for the ansible.posix.synchronize module
The playbook contains a single play named “Deploy dotfiles.” For the purpose of this tutorial, hosts
is set to localhost
. This assumes the play runs only on the local machine. Of course, plays are typically deployed to multiple hosts or groups of hosts in practice. The roles
list declares the single dotfiles
role which our play should run. Recall from earlier that roles are “a way to group tasks and related files to enable sharing and reusability.” The ansible.posix
collection is specified since it contains the useful ansible.posix.synchronize
module that makes use of rsync. You may need install rsync if it doesn't already ship with your system.
roles/dotfiles/defaults/main.yml:
---
key_files:
- "{{ ansible_env.HOME }}/.ssh/id_rsa"
config_repos:
- { path: ".tmux/plugins/tpm", repo: https://github.com/tmux-plugins/tpm }
This file defines variables which are available to all hosts regardless of their group. In this example, it defines the path to a private key file and a list of dictionaries which provide URL to Git repositories and the path to install them. Since this file is named main.yml
, by convention it will be available in any of the tasks defined by this role.
roles/dotfiles/files/:
The files directory can contain any number of files and subdirectories, with which you intend to use in the tasks of the role. In this example, it contains various configuration dotfiles which don't need to make use of the Jinja templating engine. There is also an autostart directory for storing XDG Autostart entries which will be copied to the necessary directory as part of the configuration.
roles/dotfiles/templates/:
The files in this directory will make use of the Jinja templating language and be copied to the system using the ansible.builtin.template
module. This example doesn't use Jinja template, but if you ever find yourself conditionally running code in your .bashrc
based on the current operating system, you can consider using Jinja.
NOTE: Use files/
for static configuration files and templates/
for files that need customization via variables or conditional logic.
roles/dotfiles/templates/bashrc.j2:
export EDITOR="{{ editor | default('vim') }}"
# Show custom greeting
echo "Welcome, {{ ansible_env.USER }} on {{ ansible_env.HOSTNAME }}!"
{% if ansible_system == 'Linux' %}
alias ls='ls --color=auto'
{% elif ansible_system == 'Darwin' %}
alias ls='ls -G'
{% endif %}
This Bash shell configuration file demonstrates how you can use Jinja templating to conditionally choose lines to include in the resulting file created by the ansible.builtin.template
module. Jinja can also substitute variables into configuration files among other features.
roles/dotfiles/vars/linux.yml:
---
placeholder_var: "Placeholder text."
This file contains variables that are defined differently based on the host's operating system. The explanation for the next snippet shows how these OS-specific variable files can be loaded in task files as well as how to create more of them tailored to other operating systems. Variable files in the vars/
directory of a role can be created arbitrarily, but for managing dotfiles it may be necessary to define them for each operating system you use.
roles/dotfiles/tasks/main.yml:
# Dotfile main tasks
# The ansible_system variable provides system platform detection
- name: Load OS-specific vars
ansible.builtin.include_vars: "{{ ansible_system | lower }}.yml" # Here, "lower" is a filter like pipes in the UNIX shell
when: ansible_system in ["Linux", "FreeBSD", "Windows"]
The above snippet "includes" or brings in the variables from the YAML files located in roles/dotfiles/vars/
into scope based on the operating system Ansible is running on. You can use this to provide different configurations based on the variations of Linux or UNIX-like operating systems you use. It will expect the YAML files to have names such as: linux.yml
, freebsd.yml
, or windows.yml
. You can be more specific by using the ansible_os_family
fact variable to detect the distribution of Linux which a host is using, for example.
- name: Deploy templated .bashrc
ansible.builtin.template:
src: bashrc.j2
dest: "{{ ansible_env.HOME }}/.bashrc"
mode: '0644'
This snippet uses the ansible.builtin.template
to install the Jinja templated Bash configuration file. Note how we don't have to specify the path where bashrc.j2
is located since Ansible assumes it will be located in the templates/
directory of our role.
- name: Copy root dotfiles
ansible.posix.synchronize:
src: files/dotfiles_root/
dest: "{{ ansible_env.HOME }}"
recursive: yes
# Do not remove files not in source
delete: no
This snippet uses the ansible.builtin.synchronize
module to copy configuration files stored in files/dotfiles_root/
to the HOME
directory. The synchronize
module uses rsync, and the delete: no
line ensures that extra files within the directories you're copying into aren't removed.
- name: Check if autostart directory exists (in the role)
ansible.builtin.stat:
path: "{{ role_path }}/files/autostart"
register: autostart_stat
delegate_to: localhost
run_once: true
- name: Create a directory for autostart
ansible.builtin.file:
path: "{{ ansible_env.HOME }}/.config/autostart"
state: directory
when: autostart_stat.stat.isdir is defined and autostart_stat.stat.isdir
- name: Copy autostart files
# These are used by the desktop environment to run programs at startup
ansible.posix.synchronize:
src: "{{ role_path }}/files/autostart"
dest: "{{ ansible_env.HOME }}/.config/autostart"
recursive: yes
delete: no # Do not remove files not in source
when: autostart_stat.stat.isdir is defined and autostart_stat.stat.isdir
This snippet first creates the ~/.config/autostart
directory and then copies over any XDG Autostart entry files you have. This may not be relevant to your configuration needs but it can be useful if you have any custom background processes you want to start.
- name: Change permissions of .ssh directory
ansible.builtin.file:
path: "{{ ansible_env.HOME }}/.ssh"
mode: '0700'
state: directory
- name: Create a directory for SSH sockets if it does not exist
ansible.builtin.file:
path: "{{ ansible_env.HOME }}/.ssh/cm_socket"
state: directory
mode: '0700'
- name: Set permissions on SSH private key(s)
ansible.builtin.file:
path: "{{ item }}"
mode: '0600'
state: file
loop: "{{ key_files }}"
when: key_files is defined
This snippet ensures that only your user account can view the contents of your ~/.ssh
SSH configuration directory.
- name: Clone config repos
ansible.builtin.git:
repo: "{{ item.repo }}"
dest: "{{ ansible_env.HOME }}/{{ item.path }}"
clone: true
loop: "{{ config_repos }}"
This snippet uses the ansible.builtin.git
to clone a list of Git repositories. This is useful if you store some parts of your dotfiles in separate repositories. If you use GitHub over HTTPS, then you can login to your account with GitHub CLI before running the playbook. Alternatively, if you use SSH, you can generate an SSH key and add it to your GitHub account. The config_repos
variable is defined under roles/dotfiles/defaults/main.yml
since its value is unlikely to change between the various operating systems you use.
NOTE: It is best to test your Ansible project's playbook on a virtual machine or test machine, since some configuration changes and tasks can be destructive. Using the --check
flag with ansible-playbook
will give you an overview of the playbook's effects without making permanent changes.
Once you have your playbook and tasks (encompassed in the dotfiles
roles) configured, you can run the playbook with the following command:
ansible-playbook workstations.yml
This command will execute the playbook on the localhost, deploying the dotfiles as specified in your dotfiles
role. If you'd like to run this playbook on multiple machines, you can modify the hosts
parameter in the workstations.yml
file to target specific hosts or groups defined in your inventory.
Additionally, if you want to test your playbook without making permanent changes, you can run it in dry-run mode using:
ansible-playbook --check workstations.yml
This will simulate the changes without applying them, giving you a chance to review any potential changes before they are made.
.ssh
directory has proper permissions set to 0700
.For further help, consult the Ansible documentation.
This concludes this tutorial. You now have a working Ansible project to manage your dotfiles, and you have an project base that you can extend upon to fit your configuration needs. Take a look at the GitHub repository for this tutorial to see how the complete Ansible configuration, dotfiles role, and playbook fit together. If you have any questions please leave a comment.
Resources:
Additional Resources: