Homelab Deploy Role¶
Ansible role to deploy the Homelab Django application with dual router Traefik authentication.
Description¶
This role deploys the Homelab service, a Django application for managing home infrastructure and services. It provides:
Django application with SQLite database
Granian WSGI server
Rsync deployment from local source (development workflow)
Dual router Traefik authentication (no auth on LAN/Tailscale, basic auth on public)
Static file and media management
Systemd service configuration
Note: This role only supports rsync deployment from a local source directory. Unlike some other services, homelab does not support git-based deployment.
Requirements¶
Ubuntu/Debian-based target system
Python 3.12+ installed
uv package manager installed globally (role uses
uv_installby default;/usr/local/bin/uv,homelab_use_uvmust be true)Traefik reverse proxy installed (optional, but recommended)
ansible-core 2.20+
Required collection:
ansible.posix(for synchronize/rsync module)Local source directory with homelab code (for rsync deployment)
Role Variables¶
Required Variables¶
These variables MUST be set when using this role:
homelab_source_path: "" # Local path to Homelab source code
homelab_secret_key: "" # Django secret key (generate with: openssl rand -hex 32)
Required for Traefik Basic Auth¶
When homelab_basic_auth_enabled: true (default), you must also provide:
homelab_basic_auth_password: "" # Plain text password for basic auth
Common Configuration¶
Frequently modified settings with sensible defaults:
# Application settings
homelab_app_port: 10010
homelab_app_host: "127.0.0.1"
homelab_workers: 4
# Django settings
homelab_django_settings_module: "config.settings.production"
homelab_django_allowed_hosts: "127.0.0.1"
homelab_django_debug: false
# Traefik configuration
homelab_traefik_enabled: true
homelab_traefik_host: "home.example.com"
# Traefik dual router authentication
homelab_basic_auth_enabled: true
homelab_basic_auth_user: "admin"
# Internal network IP ranges (for dual router pattern)
homelab_internal_ip_ranges:
- "192.168.0.0/16" # RFC1918 private
- "10.0.0.0/8" # RFC1918 private
- "172.16.0.0/12" # RFC1918 private
- "100.64.0.0/10" # Tailscale CGNAT
- "fd7a:115c:a1e0::/48" # Tailscale IPv6
- "fd50:3e45:c454:0::/64" # ULA IPv6
Advanced Configuration¶
# User and paths
homelab_user: homelab
homelab_group: "{{ homelab_user }}"
homelab_home: /home/{{ homelab_user }}
homelab_site_path: "{{ homelab_home }}/site"
# Database
homelab_database_path: "{{ homelab_site_path }}/db.sqlite3"
# Static and media
homelab_static_root: "{{ homelab_site_path }}/staticfiles"
homelab_media_root: "{{ homelab_site_path }}/media"
homelab_media_subdirs:
- services
- services/logos
# Service restart behavior
homelab_service_restart_on_change: true
For a complete list of variables, see defaults/main.yml.
Dependencies¶
None.
Example Playbook¶
Basic Usage¶
---
- name: Deploy Homelab
hosts: homelab
become: true
vars:
secrets: "{{ lookup('community.sops.sops', 'secrets/prod/homelab.yml') | from_yaml }}"
roles:
- role: local.ops_library.homelab_deploy
vars:
homelab_source_path: "/path/to/your/homelab"
homelab_secret_key: "{{ secrets.django_secret_key }}"
homelab_basic_auth_password: "{{ secrets.traefik_basic_auth_password }}"
homelab_traefik_host: "home.example.com"
homelab_django_allowed_hosts: "127.0.0.1,home.example.com"
Disable Basic Auth (Single Router)¶
---
- name: Deploy Homelab without Basic Auth
hosts: homelab
become: true
vars:
secrets: "{{ lookup('community.sops.sops', 'secrets/prod/homelab.yml') | from_yaml }}"
roles:
- role: local.ops_library.homelab_deploy
vars:
homelab_source_path: "/path/to/your/homelab"
homelab_secret_key: "{{ secrets.django_secret_key }}"
homelab_traefik_host: "home.example.com"
homelab_django_allowed_hosts: "127.0.0.1,home.example.com"
homelab_basic_auth_enabled: false
Custom Internal IP Ranges¶
---
- name: Deploy Homelab with Custom IP Ranges
hosts: homelab
become: true
vars:
secrets: "{{ lookup('community.sops.sops', 'secrets/prod/homelab.yml') | from_yaml }}"
roles:
- role: local.ops_library.homelab_deploy
vars:
homelab_source_path: "/path/to/your/homelab"
homelab_secret_key: "{{ secrets.django_secret_key }}"
homelab_basic_auth_password: "{{ secrets.traefik_basic_auth_password }}"
homelab_traefik_host: "home.example.com"
homelab_django_allowed_hosts: "127.0.0.1,home.example.com"
homelab_internal_ip_ranges:
- "192.168.1.0/24" # Your specific LAN
- "100.64.0.0/10" # Tailscale
- "2001:db8::/32" # Your IPv6 prefix
Dual Router Authentication Pattern¶
How It Works¶
The role implements a dual router pattern in Traefik:
Internal Router (priority 120, higher)
Matches: ClientIP in
homelab_internal_ip_rangesAuthentication: NONE
Use case: LAN and Tailscale access without auth prompts
External Router (priority 100, lower)
Matches: All other IPs (public internet)
Authentication: HTTP Basic Auth
Use case: Secure public access
Network Ranges¶
Default internal IP ranges include:
RFC1918 Private:
192.168.0.0/16,10.0.0.0/8,172.16.0.0/12Tailscale IPv4:
100.64.0.0/10(CGNAT range)Tailscale IPv6:
fd7a:115c:a1e0::/48(example, customize per deployment)ULA IPv6:
fd50:3e45:c454:0::/64(example, customize per deployment)
Important: Keep internal ranges private-only (RFC1918/Tailscale/ULA). Do not add public ISP IPv6 prefixes.
To find your IPv6 prefixes:
ip -6 addr show | grep "scope global"
Testing the Dual Router¶
After deployment, test from different networks:
# From LAN - should not prompt for auth
curl -I https://home.example.com
# From Tailscale - should not prompt for auth
curl -I https://home.example.com
# From public (LTE/external) - should return 401 without auth
curl -I https://home.example.com
# From public with auth - should return 200
curl -u admin:password -I https://home.example.com
Check Traefik logs to verify router matching:
journalctl -u traefik -f
# Look for: RouterName=homelab-int (internal) or RouterName=homelab-ext (external)
# Verify ClientAddr shows your real IP, not a gateway IP
Architecture¶
Deployment Flow¶
User and Directories: Create homelab user and directory structure
Source Sync: Rsync Django source from local machine
Python Environment: Create venv with uv and install dependencies
Django Setup: Run migrations, collect static files, add default services
Systemd Service: Configure and start Granian WSGI server
Traefik Config: Deploy dual router configuration
File Structure on Target¶
/home/homelab/
├── site/
│ ├── src/ # Django application source
│ ├── manage.py # Django management script
│ ├── pyproject.toml # Python dependencies
│ ├── uv.lock # Locked dependencies
│ ├── .env # Environment configuration
│ ├── .venv/ # Virtual environment
│ ├── venv -> .venv # Symlink for compatibility
│ ├── db.sqlite3 # SQLite database
│ ├── staticfiles/ # Collected static files
│ ├── media/ # User-uploaded media
│ └── cache/ # Django cache directory
Troubleshooting¶
Service Won’t Start¶
Check the systemd service status:
systemctl status homelab
journalctl -u homelab -n 50
Common issues:
Missing uv binary at
/usr/local/bin/uvDatabase permission problems
Port 10010 already in use
Traefik Not Routing¶
Verify Traefik configuration:
cat /etc/traefik/dynamic/homelab.yml
journalctl -u traefik -f
Check that:
Traefik service is running
DNS resolves to correct IP
Port 443 is accessible
Let’s Encrypt certificate was issued
Basic Auth Not Working¶
Verify password hash generation:
echo 'password' | htpasswd -nBi admin | cut -d: -f2
Check Traefik logs for auth failures:
journalctl -u traefik -f | grep -i auth
Wrong Router Matching¶
If clients from LAN/Tailscale hit the external router:
Verify Traefik sees real client IPs:
tail -f /var/log/traefik/access.log | jq -r '.ClientAddr'
Check your actual IP ranges:
ip addr show ip -6 addr show | grep "scope global"
Update
homelab_internal_ip_rangesto match your network
Static Files Not Loading¶
Ensure static files are collected:
sudo -u homelab bash -c "cd /home/homelab/site && source .venv/bin/activate && python manage.py collectstatic --noinput"
Check permissions:
ls -la /home/homelab/site/staticfiles
Security Considerations¶
Secret Management¶
Never commit secrets to version control
Use SOPS or Ansible Vault for
homelab_secret_keyandhomelab_basic_auth_passwordGenerate strong secrets:
openssl rand -hex 32 # Django secret key openssl rand -base64 16 # Basic auth password
Systemd Hardening¶
The role applies systemd security hardening:
NoNewPrivileges=truePrivateTmp=trueProtectSystem=strictProtectHome=trueLimited write access to application directories
Database Security¶
SQLite database file has 0644 permissions (user-only write)
Located within user home directory
Not accessible via web server (served by Django)
License¶
MIT
Changelog¶
v0.1.0 (2025-10-07)¶
Initial implementation
Django/Granian/SQLite deployment
Rsync deployment method
Dual router Traefik authentication
IPv4 and IPv6 support
Systemd service with security hardening
Static file and media management