Introduction
Raspberry Pi is the most popular homelab platform — affordable, low-power, and capable of running everything from Pi-hole to Kubernetes clusters. Ansible automates the entire lifecycle: initial setup, package management, service configuration, monitoring, backups, and fleet management across multiple Pis. This guide covers everything from installing Ansible on a Pi to managing a full homelab cluster.
Install Ansible
On Raspberry Pi OS (Debian-based)
# Update system
sudo apt update && sudo apt upgrade -y
# Install via pip (recommended — latest version)
sudo apt install python3-pip python3-venv -y
python3 -m venv ~/ansible-venv
source ~/ansible-venv/bin/activate
pip install ansible
# Or via apt (older version, simpler)
sudo apt install ansible -y
# Verify
ansible --version
On Ubuntu Server for Pi
sudo apt update
sudo apt install ansible -y
Use a Separate Controller
For best results, run Ansible from a laptop/desktop and manage Pis remotely:
# inventory/homelab.ini
[pis]
pi-hole ansible_host=192.168.1.10
pi-docker ansible_host=192.168.1.11
pi-media ansible_host=192.168.1.12
pi-monitor ansible_host=192.168.1.13
[pis:vars]
ansible_user=pi
ansible_become=true
ansible_python_interpreter=/usr/bin/python3
Bootstrap New Raspberry Pi
---
- name: Bootstrap Raspberry Pi
hosts: pis
become: true
vars:
pi_user: pi
pi_hostname: "{{ inventory_hostname }}"
ssh_pub_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
tasks:
- name: Set hostname
ansible.builtin.hostname:
name: "{{ pi_hostname }}"
- name: Update /etc/hosts
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '^127\.0\.1\.1'
line: "127.0.1.1 {{ pi_hostname }}"
- name: Update all packages
ansible.builtin.apt:
upgrade: dist
update_cache: true
cache_valid_time: 3600
- name: Install essential packages
ansible.builtin.apt:
name:
- vim
- htop
- curl
- git
- python3-pip
- unattended-upgrades
- ufw
- fail2ban
state: present
- name: Deploy SSH key
ansible.posix.authorized_key:
user: "{{ pi_user }}"
key: "{{ ssh_pub_key }}"
state: present
- name: Disable password authentication
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
notify: restart ssh
- name: Configure UFW
community.general.ufw:
rule: allow
name: OpenSSH
notify: enable ufw
- name: Set timezone
community.general.timezone:
name: "Europe/London"
- name: Enable automatic updates
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
handlers:
- name: restart ssh
ansible.builtin.service:
name: ssh
state: restarted
- name: enable ufw
community.general.ufw:
state: enabled
Deploy Pi-hole
---
- name: Deploy Pi-hole
hosts: pi-hole
become: true
vars:
pihole_password: "{{ vault_pihole_password }}"
tasks:
- name: Create Pi-hole config directory
ansible.builtin.file:
path: /etc/pihole
state: directory
mode: '0755'
- name: Configure Pi-hole
ansible.builtin.copy:
dest: /etc/pihole/setupVars.conf
content: |
PIHOLE_INTERFACE=eth0
PIHOLE_DNS_1=1.1.1.1
PIHOLE_DNS_2=1.0.0.1
QUERY_LOGGING=true
INSTALL_WEB_SERVER=true
INSTALL_WEB_INTERFACE=true
LIGHTTPD_ENABLED=true
CACHE_SIZE=10000
DNS_FQDN_REQUIRED=true
DNS_BOGUS_PRIV=true
DNSMASQ_LISTENING=local
WEBPASSWORD={{ pihole_password | hash('sha256') | hash('sha256') }}
- name: Install Pi-hole
ansible.builtin.shell: |
curl -sSL https://install.pi-hole.net | bash /dev/stdin --unattended
args:
creates: /usr/local/bin/pihole
- name: Add custom blocklists
ansible.builtin.lineinfile:
path: /etc/pihole/adlists.list
line: "{{ item }}"
create: true
loop:
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
- https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
notify: update pihole gravity
- name: Configure DNS for local devices
ansible.builtin.lineinfile:
path: /etc/pihole/custom.list
line: "{{ item }}"
create: true
loop:
- "192.168.1.10 pihole.local"
- "192.168.1.11 docker.local"
- "192.168.1.1 router.local"
handlers:
- name: update pihole gravity
ansible.builtin.command: pihole -g
Deploy Docker on Pi
---
- name: Install Docker on Raspberry Pi
hosts: pi-docker
become: true
tasks:
- name: Install Docker
ansible.builtin.shell: |
curl -fsSL https://get.docker.com | sh
args:
creates: /usr/bin/docker
- name: Add user to docker group
ansible.builtin.user:
name: pi
groups: docker
append: true
- name: Enable Docker service
ansible.builtin.service:
name: docker
enabled: true
state: started
- name: Install Docker Compose plugin
ansible.builtin.apt:
name: docker-compose-plugin
state: present
- name: Deploy Portainer
community.docker.docker_container:
name: portainer
image: portainer/portainer-ce:latest
ports:
- "9443:9443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
restart_policy: unless-stopped
Deploy Monitoring Stack
---
- name: Deploy monitoring on Raspberry Pi
hosts: pi-monitor
become: true
tasks:
- name: Create monitoring directory
ansible.builtin.file:
path: /opt/monitoring
state: directory
- name: Deploy monitoring stack
ansible.builtin.copy:
dest: /opt/monitoring/docker-compose.yml
content: |
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: {{ vault_grafana_password }}
restart: unless-stopped
node-exporter:
image: prom/node-exporter:latest
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
- name: Deploy Prometheus config
ansible.builtin.copy:
dest: /opt/monitoring/prometheus.yml
content: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'node'
static_configs:
- targets:
- '192.168.1.10:9100'
- '192.168.1.11:9100'
- '192.168.1.12:9100'
- '192.168.1.13:9100'
- name: Start monitoring stack
community.docker.docker_compose_v2:
project_src: /opt/monitoring
state: present
K3s Cluster on Raspberry Pi
---
- name: Deploy K3s on Pi cluster
hosts: pis
become: true
tasks:
- name: Install K3s server (first node)
ansible.builtin.shell: |
curl -sfL https://get.k3s.io | sh -s - server \
--write-kubeconfig-mode 644
args:
creates: /usr/local/bin/k3s
when: inventory_hostname == groups['pis'][0]
- name: Get K3s token
ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/node-token
register: k3s_token
when: inventory_hostname == groups['pis'][0]
delegate_to: "{{ groups['pis'][0] }}"
- name: Install K3s agent (worker nodes)
ansible.builtin.shell: |
curl -sfL https://get.k3s.io | K3S_URL=https://{{ hostvars[groups['pis'][0]].ansible_host }}:6443 \
K3S_TOKEN={{ hostvars[groups['pis'][0]].k3s_token.content | b64decode | trim }} \
sh -
args:
creates: /usr/local/bin/k3s-agent
when: inventory_hostname != groups['pis'][0]
Automated Backups
---
- name: Backup Raspberry Pi configurations
hosts: pis
become: true
tasks:
- name: Create backup directory
ansible.builtin.file:
path: /backups
state: directory
- name: Backup Pi-hole config
ansible.builtin.archive:
path:
- /etc/pihole
- /etc/dnsmasq.d
dest: "/backups/pihole-{{ ansible_date_time.date }}.tar.gz"
when: inventory_hostname == 'pi-hole'
- name: Backup Docker volumes
ansible.builtin.shell: |
docker run --rm \
-v {{ item }}:/source:ro \
-v /backups:/backup \
alpine tar czf /backup/{{ item }}-{{ ansible_date_time.date }}.tar.gz -C /source .
loop:
- portainer_data
- grafana-data
when: inventory_hostname == 'pi-docker' or inventory_hostname == 'pi-monitor'
- name: Sync backups to NAS
ansible.posix.synchronize:
src: /backups/
dest: rsync://nas.local/pi-backups/{{ inventory_hostname }}/
mode: push
delegate_to: "{{ inventory_hostname }}"
Performance Tips for Pi
| Tip | Why |
|---|---|
Use gather_subset: min | Full fact gathering is slow on Pi |
Use pipelining = true | Reduces SSH round trips |
Use forks = 5 | Don't overwhelm a Pi controller |
Avoid shell module | apt, copy, template are faster |
Use async for long tasks | Don't block on updates |
# ansible.cfg optimized for Raspberry Pi
[defaults]
forks = 5
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-facts
fact_caching_timeout = 3600
[connection]
pipelining = true
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Related Articles
Conclusion
Ansible turns a collection of Raspberry Pis into a managed homelab — consistent configuration, automated updates, monitoring, backups, and even Kubernetes clusters. Run Ansible from a separate controller (laptop or dedicated Pi) targeting your fleet over SSH. Start with the bootstrap playbook to harden and standardize every Pi, then layer on services like Pi-hole, Docker, and monitoring with dedicated playbooks per role.