A complete Infrastructure-as-Code (IaC) setup for running Home Assistant and supporting services on a Raspberry Pi (or any Linux server), with secure external access via Cloudflare Tunnel. I use something similar to this, and since AI can template out my configuration, I might as well help others get a homelab up and running.
- MIT license, use at your own risk
- Cut an issue if you have trouble and will try to help
- Feel free to contribute any cool things you are adding
- Nextcloud, grafana, is still a work in progress on my end
- What This Provides
- Services Overview
- Architecture
- Prerequisites
- Token & Credential Setup
- Quick Start
- Project Structure
- Make Commands
- Security
- Production Parallels
- Documentation
- Adding More Services
- Troubleshooting
- Contributing
- License
- Acknowledgments
- Hardened Linux Server - Disables root access, enables backups, sets up firewall with UFW, installs docker and some other things that I like to use on in linux.
- Home Assistant - Open-source home automation
- Traefik - Modern reverse proxy with automatic SSL
- Cloudflare Tunnel - Secure external access without port forwarding
- AdGuard Home - Network-wide ad blocking and DNS
- Prometheus + Grafana - Monitoring stack with beautiful dashboards
- Nextcloud - Self-hosted cloud storage (Google Drive alternative)
- Homarr - Beautiful dashboard for your services
- Portainer - Docker container management
- OpenVPN - VPN access to your home network
- AirConnect - Chromecast to AirPlay bridge
| Service | Port | Purpose | External Access |
|---|---|---|---|
| Home Assistant | 8123 | Smart home automation platform. Controls lights, sensors, switches, and automations. The brain of your smart home. | β Via Cloudflare Tunnel |
| Traefik | 80, 8080 | Reverse proxy and load balancer. Routes incoming traffic to the correct service and adds security headers. Dashboard on 8080 (localhost only). | Internal routing only |
| Cloudflared | - | Cloudflare Tunnel connector. Creates a secure outbound connection to Cloudflare, enabling HTTPS access without opening ports on your router. | Outbound only |
| AdGuard Home | 53, 3000 | Network-wide ad blocker and DNS server. Blocks ads, trackers, and malware at the DNS level for all devices on your network. | β Web UI via Tunnel |
| Homarr | 7575 | Beautiful dashboard for your homelab. Provides a single page to access all your services with customizable widgets. | |
| Portainer | 9000 | Docker management UI. View containers, logs, networks, and volumes through a web interface instead of CLI. | |
| AirConnect | - | Chromecast/DLNA to AirPlay bridge. Allows Apple devices to stream audio to Chromecast or DLNA speakers via AirPlay. | β Local only |
| Service | Port | Purpose | External Access |
|---|---|---|---|
| Prometheus | 9090 | Time-series database for metrics. Collects and stores metrics from all your services. Query with PromQL. | |
| Grafana | 3001 | Metrics visualization and dashboards. Beautiful graphs and alerts for your homelab. Pre-configured with Prometheus. | β Via Tunnel (has auth) |
| Node Exporter | 9100 | System metrics collector. Exposes CPU, memory, disk, and network stats from the host machine to Prometheus. | β Internal only |
| cAdvisor | 8081 | Container metrics collector. Monitors resource usage and performance of all Docker containers. | β Internal only |
| Service | Port | Purpose | External Access |
|---|---|---|---|
| Nextcloud | 8082 | Self-hosted cloud storage and collaboration platform. Sync files, calendars, contacts across devices. Alternative to Google Drive/Dropbox. | β Via Tunnel |
| MariaDB | - | Database backend for Nextcloud. Stores user data, file metadata, and app configurations. | β Internal only |
| Redis | - | In-memory cache for Nextcloud. Improves performance by caching frequently accessed data. | β Internal only |
| Service | Port | Purpose |
|---|---|---|
| OpenVPN | 943, 1194 | VPN server for secure remote access to your home network when you're away. |
| Mosquitto | 1883 | MQTT broker for IoT devices. Enables communication between smart home devices. |
| Homebridge | 8581 | Exposes non-HomeKit devices to Apple Home app. |
Internet β Cloudflare (HTTPS) β Cloudflare Tunnel β Traefik β Home Assistant
β
Other Services
- Cloudflare Tunnel: Provides secure HTTPS access without exposing ports
- Traefik: Routes traffic to services, adds required headers for Home Assistant
- Docker Compose: Manages all services as containers
- Raspberry Pi 4 (4GB+ recommended) or any Linux server
- USB Zigbee adapter (optional, for smart home devices)
- External storage (optional, for backups)
- Raspberry Pi OS (64-bit) or Ubuntu Server
- Docker and Docker Compose
- Ansible (on your local machine)
- Terraform (for Cloudflare management)
- Cloudflare account with a domain configured
- Domain name pointed to Cloudflare nameservers
Before deploying, you'll need to create several tokens and credentials. This section walks you through each one.
The API token allows Terraform to manage your Cloudflare tunnel and DNS records.
Steps to create:
-
Go to Cloudflare API Tokens
-
Click "Create Token"
-
Click "Create Custom Token"
-
Configure permissions:
Permission Access Zone β DNS Edit Account β Cloudflare Tunnel Edit Account β Cloudflare Tunnel Read -
Under Zone Resources, select:
Include β Specific zone β your-domain.com -
Under Account Resources, select:
Include β Your Account -
Click "Continue to summary" β "Create Token"
-
Copy the token immediately (you won't see it again!)
Where to use it:
# In terraform/terraform.tfvars
cloudflare_api_token = "your-token-here"These IDs tell Terraform which Cloudflare account and domain to manage.
Finding your Account ID:
- Go to Cloudflare Dashboard
- The Account ID is in the URL:
dash.cloudflare.com/ACCOUNT_ID_HERE/... - Or: Click any domain β scroll down on Overview page β right sidebar shows "Account ID"
Finding your Zone ID:
- Go to Cloudflare Dashboard
- Click on your domain
- On the Overview page, scroll down
- Find "Zone ID" in the right sidebar under "API"
Where to use them:
# In terraform/terraform.tfvars
cloudflare_account_id = "your-account-id-here"
cloudflare_zone_id = "your-zone-id-here"Used by Homarr to encrypt sensitive data. Generate a random 32-byte hex string.
Generate with OpenSSL:
openssl rand -hex 32Or with Python:
python3 -c "import secrets; print(secrets.token_hex(32))"Where to use it:
# In .env
ENCRYPTION_KEY=your-64-character-hex-string-hereDefault credentials for the Grafana dashboard. Change these in production!
Where to use them:
# In .env
GRAFANA_USER=admin
GRAFANA_PASSWORD=your_secure_password_hereNextcloud requires database credentials and an admin account.
Where to use them:
# In .env
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=your_secure_password_here
NEXTCLOUD_DB_PASSWORD=your_db_password_here
NEXTCLOUD_DB_ROOT_PASSWORD=your_db_root_password_here
NEXTCLOUD_TRUSTED_DOMAINS=localhost,192.168.1.100,nextcloud.yourdomain.comImportant: Set
NEXTCLOUD_TRUSTED_DOMAINSto include all hostnames/IPs you'll use to access Nextcloud.
| File | Credentials Needed |
|---|---|
.env |
ENCRYPTION_KEY, CLOUDFLARE_TUNNEL_TOKEN, GRAFANA_PASSWORD, NEXTCLOUD_* |
terraform/terraform.tfvars |
cloudflare_api_token, cloudflare_account_id, cloudflare_zone_id, domain |
ansible/vars.yml |
cloudflare_tunnel_token (copied from Terraform output) |
ansible/inventory.ini |
Your server's IP address |
git clone <your-repo-url> hassio-config
cd hassio-config
# Copy sample files
cp sample.env .env
cp ansible/vars.yml.sample ansible/vars.yml
cp terraform/terraform.tfvars.example terraform/terraform.tfvars
# Edit configuration files with your values
vim .env
vim ansible/vars.yml
vim terraform/terraform.tfvarscd terraform
terraform init
terraform plan
terraform apply
# Get the tunnel token for docker-compose
terraform output -raw tunnel_token
# Add this to your .env file as CLOUDFLARE_TUNNEL_TOKEN
cd ..# Update ansible/inventory.ini with your server IP
nano ansible/inventory.ini
# Initial server setup
make initial
# Or step by step:
cd ansible
ansible-playbook 00_main.yml -i inventory.ini --tags initialLocal Access (LAN):
| Service | URL |
|---|---|
| Home Assistant | http://<server-ip>:8123 |
| Traefik Dashboard | ssh -L 8080:localhost:8080 user@server then http://localhost:8080 |
| Portainer | ssh -L 9000:localhost:9000 user@server then http://localhost:9000 |
| Homarr | ssh -L 7575:localhost:7575 user@server then http://localhost:7575 |
| AdGuard | http://<server-ip>:3000 |
| Grafana | ssh -L 3001:localhost:3001 user@server then http://localhost:3001 |
| Prometheus | ssh -L 9090:localhost:9090 user@server then http://localhost:9090 |
| Nextcloud | http://<server-ip>:8082 |
External Access (via Cloudflare Tunnel):
Enable services in terraform/terraform.tfvars and run make tf-apply:
| Service | URL | Notes |
|---|---|---|
| Home Assistant | https://hassio.yourdomain.com |
β Enabled by default |
| Nextcloud | https://nextcloud.yourdomain.com |
Uncomment in tfvars |
| Grafana | https://grafana.yourdomain.com |
Uncomment in tfvars |
| Prometheus | https://prometheus.yourdomain.com |
|
| AdGuard | https://adguard.yourdomain.com |
Web UI only (DNS stays local) |
βββ ansible/ # Server configuration playbooks
β βββ 00_main.yml # Main orchestration playbook
β βββ inventory.ini # Server inventory
β βββ vars.yml.sample # Sample variables
βββ terraform/ # Cloudflare infrastructure
β βββ main.tf # Tunnel and DNS configuration
β βββ variables.tf # Input variables
β βββ terraform.tfvars.example
βββ traefik/ # Reverse proxy configuration
β βββ traefik.yml # Static config
β βββ dynamic/ # Dynamic routing rules
β βββ security.yml # Security middlewares (NEW)
β βββ hassio.yml # Home Assistant routing
β βββ nextcloud.yml # Nextcloud routing
β βββ grafana.yml # Grafana routing
β βββ prometheus.yml # Prometheus routing (with auth)
β βββ adguard.yml # AdGuard routing
βββ prometheus/ # Prometheus monitoring
β βββ prometheus.yml # Scrape configs
βββ grafana/ # Grafana dashboards
β βββ provisioning/ # Auto-provisioned datasources
βββ nextcloud/ # Nextcloud cloud storage
β βββ config/ # Nextcloud configuration
β βββ custom_apps/ # Custom Nextcloud apps
βββ homeassistant-config/ # Home Assistant configuration
β βββ configuration.yaml
βββ docker-compose.yaml # Service definitions
βββ Makefile # Convenient command shortcuts
βββ SECURITY.md # Security documentation (NEW)
βββ sample.env # Environment template
make help # Show all available commands
# Ansible commands
make deploy # Deploy configs and restart services
make initial # Initial server setup
make pull # Backup configs from server
make check # Dry run (preview changes)
# Terraform commands
make tf-init # Initialize Terraform
make tf-plan # Preview infrastructure changes
make tf-apply # Apply infrastructure changes
make tf-token # Output tunnel token
make tf-update-env # Update .env with tunnel token
# Utility commands
make status # Check container status
make logs # View all container logs
make ssh # SSH to serverFull documentation: See SECURITY.md for threat model, network architecture, and detailed security controls.
This homelab implements production-grade security practices:
| Control | Implementation |
|---|---|
| Docker Socket Protection | Proxy with read-only, filtered access |
| Network Segmentation | Three networks: edge, app, internal |
| Container Hardening | cap_drop: ALL, read_only, no-new-privileges |
| Admin UI Protection | Bound to localhost, SSH tunnel required |
| Rate Limiting | Applied to all external endpoints |
| Security Headers | HSTS, XSS protection, CSP |
| Secrets Management | Environment files, git-ignored |
| Pinned Images | All images use specific versions (no :latest) |
| Service | Reason |
|---|---|
| Prometheus | No authentication by default |
| Portainer | Full Docker control |
| Traefik Dashboard | Infrastructure information |
| Node Exporter | System metrics |
| cAdvisor | Container metrics |
| Databases (MariaDB, Redis) | Data access |
To access these services securely, use SSH port forwarding:
# Access multiple admin services at once
ssh -L 9090:localhost:9090 \
-L 9000:localhost:9000 \
-L 8080:localhost:8080 \
user@your-serverFor production use, enable Cloudflare Access to add identity-based authentication (Google, GitHub, etc.) in front of all exposed services. This provides:
- Zero-trust authentication
- Multi-factor authentication
- Session management
- Audit logging
This homelab isn't just a toyβit mirrors real-world platform engineering practices. Here's how these patterns translate to enterprise infrastructure:
| Homelab | Production |
|---|---|
cap_drop: ALL + specific cap_add |
AWS IAM least privilege policies |
| docker-socket-proxy (read-only) | Service accounts with minimal permissions |
Non-root containers (user: 65534) |
Rootless containers in Kubernetes |
no-new-privileges: true |
PodSecurityPolicies / SecurityContextConstraints |
| Homelab | Production |
|---|---|
| Terraform for Cloudflare | Terraform for AWS/GCP/Azure |
| Ansible playbooks | Ansible Tower / AWX |
| docker-compose.yaml | Kubernetes manifests / Helm charts |
Git-ignored secrets in .env |
HashiCorp Vault, AWS Secrets Manager |
| Homelab | Production |
|---|---|
| Cloudflare Tunnel (no open ports) | Service mesh (Istio, Linkerd) |
| Network segmentation (edge/app/internal) | VPC subnets, security groups |
| SSH tunnels for admin access | Bastion hosts, VPN, AWS SSM |
| Cloudflare Access | Identity-aware proxy (IAP), Okta |
| Homelab | Production |
|---|---|
| Multiple auth layers (Cloudflare + app) | WAF + API Gateway + app auth |
| Rate limiting middleware | AWS WAF rate limiting, API throttling |
| Security headers (HSTS, CSP) | Same, plus additional WAF rules |
| Separate networks per tier | VPC peering, Transit Gateway |
| Homelab | Production |
|---|---|
| Prometheus + Grafana | Datadog, New Relic, or self-hosted |
| Traefik access logs | ELK stack, Splunk |
| Container metrics (cAdvisor) | Container Insights, Prometheus Operator |
| Structured JSON logging | Same, with correlation IDs |
| Homelab | Production |
|---|---|
| Pinned image versions | Image signing (Cosign, Notary) |
No :latest tags |
Immutable tags, digest pinning |
| Official images only | Private registry, vulnerability scanning |
If you've deployed and understand this homelab, you can speak to:
- Container security - Runtime hardening, capability management, privilege restrictions
- Network architecture - Segmentation, zero-trust principles, reverse proxy patterns
- Infrastructure as Code - Terraform, Ansible, declarative configuration
- Secrets management - Environment isolation, rotation strategies
- Observability - Metrics collection, visualization, log aggregation
- Incident response - Log locations, audit trails, recovery procedures
- SECURITY.md - Full security documentation and threat model
- Terraform README - Cloudflare tunnel setup details
- Ansible README - Full playbook documentation
- Quick Reference - Command cheat sheet
The following services already have Traefik routes configured in traefik/dynamic/:
| Service | Traefik Config | Just Enable in Terraform |
|---|---|---|
| Nextcloud | nextcloud.yml |
β Ready |
| Grafana | grafana.yml |
β Ready |
| Prometheus | prometheus.yml |
|
| AdGuard | adguard.yml |
β Ready (Web UI only) |
To enable external access, uncomment the service in terraform/terraform.tfvars and run:
make tf-apply
make deploy myservice:
image: myservice/image:1.0.0 # Always pin version!
container_name: myservice
restart: unless-stopped
# Security hardening
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
# Bind to localhost for admin services
ports:
- "127.0.0.1:8000:8000"
networks:
- app # or internal for backend servicesCreate traefik/dynamic/myservice.yml:
http:
routers:
myservice:
rule: "Host(`myservice.yourdomain.com`)"
service: myservice
entrypoints:
- web
middlewares:
- external-secure@file # Apply security middlewares
priority: 10
services:
myservice:
loadBalancer:
servers:
- url: "http://myservice:8000"In terraform/terraform.tfvars:
additional_services = {
myservice = {
subdomain = "myservice"
service_url = "http://traefik:80"
enabled = true
}
}make tf-apply # Update Cloudflare
make deploy # Deploy to server- Check Cloudflare tunnel status in dashboard
- Verify tunnel token in
.envmatches Terraform output - Check cloudflared logs:
make logsordocker compose logs cloudflared
- Verify
configuration.yamlhas trusted proxies configured - Check Traefik is adding X-Forwarded-Proto header
- Clear browser cookies and try again
make status # Check container status
docker compose logs -f # View logs
docker compose config # Validate compose fileIf you see permission errors after hardening:
- Check the container's required user/group
- Verify volume ownership matches container user
- Some containers need specific capabilities - check logs
Admin services (Prometheus, Portainer, etc.) are bound to localhost. Use SSH tunnels:
# Example: Access Prometheus
ssh -L 9090:localhost:9090 user@your-server
# Then open http://localhost:9090 in your browser- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
Security issues should be reported privately - see SECURITY.md.
MIT License - See LICENSE for details.
- Home Assistant
- Traefik
- Cloudflare
- Ansible
- Terraform
- docker-socket-proxy - For secure Docker socket access