Automated configuration of a secure Raspberry Pi with VPN, reverse proxy, and Docker services. This Ansible project transforms a Raspberry Pi (or Debian server) into a secure personal server with WireGuard VPN access, container management via Portainer, and Nginx reverse proxy to securely expose your services.
Main features:
- 🔐 WireGuard VPN for secure remote access
- 🐳 Docker + Portainer for container management
- 🌐 Nginx Proxy Manager for reverse proxy with SSL
- 🛡️ SSH hardening, UFW firewall, CrowdSec, Fail2ban
- 🔀 Split DNS for private subdomains accessible only via VPN
- Ansible Installation
- WireGuard Configuration
- Portainer Configuration
- Nginx Proxy Manager Configuration
- Split DNS for Private Services
- Diagnostic Commands
- Ansible 2.9+
- A Raspberry Pi with Raspberry Pi OS (or Debian)
- SSH access to the Pi
cp inventories/prod/hosts.ini.example inventories/prod/hosts.ini
cp inventories/prod/group_vars/all/secrets.yml.example inventories/prod/group_vars/all/secrets.ymlEdit inventories/prod/hosts.ini with your Pi's IP address you can get using ip a | grep 192 on the Pi.
Edit inventories/prod/group_vars/all/secrets.yml and add your SSH public key:
ssh_authorized_key: "ssh-ed25519 AAAA... [email protected]"Tip: To generate an SSH key:
ssh-keygen -t ed25519 -C "[email protected]" cat ~/.ssh/id_ed25519.pub
Find your Pi's network interface:
ip route | grep default
# Example: default via 192.168.x.x dev eth0 proto dhcp src 192.168.x.x metric 100
# here its "eth0"Update inventories/prod/group_vars/all/secrets.yml:
wireguard:
external_interface: eth0 # Adapt to your machineBefore running Ansible, set up your WireGuard client(s) to avoid running the playbook twice.
| Platform | Installation |
|---|---|
| macOS | App Store |
| Windows | wireguard.com/install |
| iOS | App Store → "WireGuard" |
| Android | Play Store → "WireGuard" |
- Open the WireGuard app
- Click "+" → "Add empty tunnel"
- The app automatically generates a private key and displays the public key
- Copy the displayed public key
Edit inventories/prod/group_vars/all/secrets.yml:
wireguard_peers:
- name: my-device
public_key: "PASTE_PUBLIC_KEY_HERE"
allowed_ips: "10.8.0.2/32"For multiple devices, add more peers with unique IPs (
10.8.0.3/32,10.8.0.4/32, etc.)
ansible-playbook playbooks/site.yml -KImportant: Note the WireGuard public key displayed in the step
Display server public key, you'll need it to finish your clients configuration.
After running the playbook, complete your WireGuard client configuration.
Complete the configuration in the WireGuard app:
[Interface]
PrivateKey = (already filled automatically)
Address = 10.8.0.2/32
DNS = 10.8.0.1
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = PI_PUBLIC_IP:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25Replace:
SERVER_PUBLIC_KEY: key displayed during playbook executionPI_PUBLIC_IP: public IP of your router (or local IP if on same network)
Activate the tunnel and test:
ping 10.8.0.1
ssh [email protected]For each new device:
- Create an empty tunnel in the WireGuard app → copy the public key
- Add a peer in
secrets.ymlwith a unique IP (10.8.0.3/32,10.8.0.4/32, etc.) - Re-run the playbook
- Complete the tunnel configuration in the app
After running the playbook, Portainer is accessible via VPN.
⚠️ Important: Create your admin account within minutes of first launch, otherwise Portainer will disable registration for security reasons.
- Connect to the VPN
- Access
https://10.8.0.1:9443/ - Create your administrator account
Nginx Proxy Manager allows you to create reverse proxies with automatic SSL certificates.
- Connect to the VPN
- Access
http://10.8.0.1:81 - Create your administrator account
To restrict access to VPN users only:
-
Go to Access Lists → Add Access List
-
Details: Name =
WireguardorVPN Onlyor any name you want -
Options: Check the
Satisfy anybox -
Rules:
- Add
10.8.0.0/24in theAllowsection - Let the default
allin theDenysection
- Add
-
Click Save
- Go to Proxy Hosts → Add Proxy Host
- Details:
- Domain Names:
portainer.<your-domain>.fr - Scheme:
https← Important! - Forward Hostname / IP:
portainer - Forward Port:
9443 - Enable Websockets Support
- Enable Block Common Exploits
- Domain Names:
- SSL:
- Request a new SSL Certificate (Let's Encrypt)
- Enable Force SSL
- ✅ Check "Ignore Invalid SSL"
- Access List: Select the access list you created earlier
- Click Save
To access NPM via a clean URL instead of 10.8.0.1:81:
- Go to Proxy Hosts → Add Proxy Host
- Details:
- Domain Names:
nginx.<your-domain>.fr - Scheme:
http - Forward Hostname / IP:
nginx-proxy-manager - Forward Port:
81 - Enable Block Common Exploits
- Domain Names:
- SSL: Request a Let's Encrypt certificate if desired
- Access List: Select the access list you created earlier
- Click Save
| Error | Cause | Solution |
|---|---|---|
| 403 Forbidden | Access List misconfigured | Ensure allow 10.8.0.0/24 comes BEFORE deny all |
| 502 Bad Gateway | Container unreachable | Use the container name (portainer), not IP |
| HTTP to HTTPS error | Wrong scheme | Set Scheme to https for Portainer (port 9443) |
Split DNS allows you to have public and private subdomains:
- Public:
<your-domain>.fr→ accessible to everyone - Private:
portainer.<your-domain>.fr,nginx.<your-domain>.fr→ accessible only via VPN
┌─────────────────────────────────────────────────────────────────┐
│ Internet User │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ <your-domain>.fr │ ✅ Public │
│ └──────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ portainer.<your-domain>.fr nginx.<your-domain>.fr │
│ ❌ Blocked ❌ Blocked │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ VPN User (WireGuard) │
│ │ │
│ DNS: 10.8.0.1 (dnsmasq) │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ portainer.<your-domain>.fr nginx.<your-domain>.fr │
│ → 10.8.0.1 ✅ → 10.8.0.1 ✅ │
└─────────────────────────────────────────────────────────────────┘
You can add a new private subdomain without re-running Ansible:
# Connect to the server
ssh [email protected]
# Add the DNS entry
sudo nano /etc/dnsmasq.d/split-dns.conf
# Add: address=/new.<your-domain>.fr/10.8.0.1
# Restart dnsmasq
sudo systemctl restart dnsmasqThen configure the Proxy Host in Nginx Proxy Manager.
Alternatively, add the subdomain to secrets.yml and re-run the playbook for a reproducible setup.
- Without VPN:
portainer.<your-domain>.fr→ 403 Forbidden ❌ - With VPN:
portainer.<your-domain>.fr→ Works ✅
# Verify DNS resolution (with VPN)
nslookup portainer.<your-domain>.fr 10.8.0.1
# Should return 10.8.0.1# Service status
sudo systemctl status dnsmasq
sudo systemctl status wg-quick@wg0
# Docker containers
docker ps
docker logs nginx-proxy-manager
docker logs portainer
# Docker networks
docker network ls
docker network inspect proxy_network
# DNS test (with VPN)
nslookup portainer.<your-domain>.fr 10.8.0.1