Automated infrastructure provisioning for Raspberry Pi devices using Ansible and cloud-init. This project automatically configures a Raspberry Pi to run containerized services with secure external access via Cloudflare Tunnel.
- Zero-touch deployment: Insert SD card, power on, and the Pi configures itself
- Secure remote access: Cloudflare Tunnel provides HTTPS access without exposing your home IP
- Infrastructure as Code: Ansible playbooks for reproducible configurations
- Secret management: Secrets injected at build time, never committed to git
- Auto-updates: Bootstrap script pulls latest configuration from git on each run
- Raspberry Pi (3/4/5 or Zero 2 W)
- MicroSD card (16GB+ recommended)
- Ubuntu Server for Raspberry Pi (22.04 LTS or newer)
- Cloudflare account with a configured tunnel
- SSH key pair
- Git repository (this one, forked/cloned)
# On Windows PowerShell
ssh-keygen -t ed25519 -C "$env:USERNAME@$env:COMPUTERNAME" -f "$env:USERPROFILE\.ssh\id_ed25519"# On Linux/macOS
ssh-keygen -t ed25519 -C "$USER@$HOSTNAME" -f ~/.ssh/id_ed25519- Go to Cloudflare Zero Trust Dashboard
- Navigate to Networks → Tunnels
- Create a new tunnel or use an existing one
- Copy the tunnel token (starts with
eyJh...)
Copy .env.public to .env and fill in your values:
cp .env.public .envEdit .env:
CLOUDFLARE_TUNNEL_TOKEN="eyJhIjoiZ..."
USER_NAME="your-username"
DEVICE_HOSTNAME="ev-rpi"
SSH_PUBLIC_KEY_PATH="~/.ssh/id_ed25519.pub"
REPO_URL="https://github.com/your-username/ev_infra.git"./generate_boot_config.sh /path/to/sd-card/system-bootThis creates configuration files with your secrets injected. These files are not committed to git.
- Download Ubuntu Server for Raspberry Pi
- Flash to SD card using Raspberry Pi Imager or
dd - Copy the generated files from step 4 to the
system-bootpartition on the SD card- Note: Ubuntu Server uses cloud-init by default, so the files will be automatically detected
- Insert SD card into Raspberry Pi
- Connect to network (Ethernet recommended for first boot)
- Power on
- Wait 2-5 minutes for initial setup and package installation
ssh your-username@ev-rpi.local
# or
ssh your-username@<pi-ip-address>-
Cloud-init reads
system-boot/ssh_authorized_keys.yml:- Creates user with sudo privileges
- Configures SSH authorized keys
- Installs git and Ansible
- Writes Cloudflare token to Ansible facts directory
- Executes
ansible_bootstrap.sh
-
Ansible Bootstrap (
/usr/local/bin/ansible_bootstrap.sh):- Clones this repository to
/etc/ansible/code - Runs
playbooks/site.ymllocally
- Clones this repository to
-
Ansible Playbook configures:
- Docker and Docker Compose
- Service directory at
/opt/services .envfile with secrets- Deploys services via Docker Compose
-
Docker Compose starts:
- Demo web service (nginx)
- Cloudflare Tunnel daemon
┌─────────────────────────────────────────┐
│ Raspberry Pi │
│ │
│ ┌───────────────────────────────────┐ │
│ │ Cloud-init (First Boot) │ │
│ │ • User creation │ │
│ │ • SSH setup │ │
│ │ • Package installation │ │
│ └──────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────▼────────────────────┐ │
│ │ Ansible Bootstrap │ │
│ │ • Git clone repository │ │
│ │ • Run playbook locally │ │
│ └──────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────▼────────────────────┐ │
│ │ Ansible Playbook │ │
│ │ • Install Docker │ │
│ │ • Configure services │ │
│ │ • Inject secrets │ │
│ └──────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────▼────────────────────┐ │
│ │ Docker Compose │ │
│ │ • Web services │ │
│ │ • Cloudflare Tunnel │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
│ Secure Tunnel
▼
┌────────────────┐
│ Cloudflare │
│ Network │
└────────────────┘
│
▼
Internet Users
.
├── playbooks/
│ └── site.yml # Main Ansible playbook
├── roles/
│ └── ev_base/
│ ├── tasks/
│ │ └── main.yml # Ansible tasks
│ ├── handlers/
│ │ └── main.yml # Service restart handlers
│ └── files/
│ └── docker-compose.yml # Service definitions
├── system-boot/
│ ├── ssh_authorized_keys.yml # Cloud-init configuration (template)
│ ├── metadata.yml # Instance metadata (template)
│ └── network-config.yml # Network configuration
├── generate_boot_config.sh # Build script
├── .env.public # Environment template
└── .env # Your secrets (gitignored)
Edit roles/ev_base/files/docker-compose.yml to add new services:
services:
your-app:
image: your-image:latest
restart: unless-stopped
ports:
- "8080:80"
environment:
- YOUR_VAR=${YOUR_VAR}Add corresponding secrets to .env and update the Ansible task in roles/ev_base/tasks/main.yml to include them in /opt/services/.env.
All infrastructure changes should be made in the Ansible playbook:
roles/ev_base/tasks/main.yml- Main configuration tasksroles/ev_base/handlers/main.yml- Service restart logic
Push changes to git, then SSH into your Pi and run:
sudo bash /usr/local/bin/ansible_bootstrap.sh- Never commit
.env- It contains secrets - Cloudflare Tunnel provides secure access without exposing your home IP
- SSH keys only - Password authentication is disabled
- Automatic updates - Keep packages updated in the cloud-init configuration
- Minimal permissions - Services run as non-root in containers
- Check Ethernet cable connection
- Verify SD card was flashed correctly
- Check cloud-init logs:
sudo cat /var/log/cloud-init-output.log
- Verify SSH key is correctly configured in
.env - Check hostname resolves:
ping ev-rpi.local - Try IP address instead of hostname
- Check SSH service:
sudo systemctl status ssh
- Check Ansible logs:
sudo journalctl -u ansible-pull - Verify Docker is running:
sudo systemctl status docker - Check Docker Compose logs:
sudo docker compose -f /opt/services/docker-compose.yml logs
- Verify token is correct in
.env - Check tunnel status in Cloudflare dashboard
- Check container logs:
sudo docker compose -f /opt/services/docker-compose.yml logs cloudflared
MIT
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
