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

TipWhy
Use gather_subset: minFull fact gathering is slow on Pi
Use pipelining = trueReduces SSH round trips
Use forks = 5Don't overwhelm a Pi controller
Avoid shell moduleapt, copy, template are faster
Use async for long tasksDon'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

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.