A complete homeserver setup using Traefik reverse proxy with automatic HTTPS and Portainer for easy container management. Features VPN-protected downloading and both local and external access options.
✅ Local-first architecture - Fast streaming without internet dependencies
✅ Automatic HTTPS - Set-and-forget SSL certificates via Let's Encrypt
✅ VPN-protected downloads - Secure torrenting through isolated VPN container
✅ Easy service deployment - Deploy services via Portainer's web UI
✅ Hybrid access - Local network speed + selective external access
✅ Service auto-discovery - Traefik automatically detects new services
✅ No port conflicts - Everything routed through Traefik on 80/443
cp .env.example .env
# Edit .env and configure:
# - SERVER_DOMAIN=example.com
# - ACME_EMAIL=your-email@example.com
# - CF_DNS_API_TOKEN=your_cloudflare_dns_token
# - VPN credentials for downloading services
For automatic HTTPS certificates, you need a Cloudflare API token with DNS permissions:
- Buy a domain - Cloudflare offers domains at cost (no markup) and provides excellent DNS management
- Create API Token:
- Go to Cloudflare API Tokens
- Click "Create Token"
- Use "Custom token" template
- Permissions:
- Zone:Zone:Read
- Zone:DNS:Edit
- Zone Resources:
- Include: Specific zone:
your-domain.com
- Include: Specific zone:
- Client IP Address Filtering: Leave empty (optional)
- Click "Continue to summary" → "Create Token"
- Save the token - You'll use this for
CF_DNS_API_TOKEN
in your.env
file
Alternative DNS Providers: If not using Cloudflare, check Traefik's supported ACME providers and adjust the configuration accordingly.
Point *.example.com
to your NAS IP address:
- Router DNS: Add wildcard DNS entry
- Pi-hole: Add local DNS record
- Hosts file:
192.168.1.100 jellyfin.example.com
(etc.)
For external access and Let's Encrypt certificates:
- In Cloudflare Dashboard: Go to your domain → DNS
- Add DNS Record:
- Type:
A
- Name:
*
(wildcard) - IPv4 address: Your NAS IP address (e.g.,
192.168.1.100
) - Proxy status: ☁️ Disabled (DNS only, not proxied)
- TTL: Auto
- Type:
- Click Save
Important: The proxy status must be disabled for Let's Encrypt DNS challenges to work properly.
docker-compose up -d
This starts:
- Traefik v3.0 (reverse proxy with automatic HTTPS)
- Portainer 2.32.0 (container management)
- Cloudflared (selective external access)
IMPORTANT: Deploy in this order!
- Access Portainer:
https://portainer.example.com
- Configure App Templates:
- Go to Settings > App Templates
- Set URL:
https://raw.githubusercontent.com/tomwojcik/homeserver-traefik-portainer/master/template.json
- Deploy VPN stack FIRST: Deploy "Gluetun VPN"
- Deploy download services: Deploy metube, media-server (depend on gluetun)
- Deploy other services: Any order
Deploy any of these through Portainer's App Templates:
- Jellyfin/Plex - Media streaming servers
- Sonarr/Radarr - TV show/movie management
- Jellyseerr - Media request system
- MeTube - YouTube downloader (VPN-protected)
- qbittorrent - Torrent client (VPN-protected)
- Nextcloud - File sync and collaboration
- Heimdall - Dashboard for organizing services
- Uptime Kuma - Service monitoring
- Gitea/Gogs - Git repositories
- Docker Registry - Private container registry
- n8n - Workflow automation
- Dozzle - Container log viewer
- CyberChef - Data transformation tools
- MinIO - S3-compatible object storage
After setup, your services will be available at:
- https://traefik.example.com (Traefik dashboard)
- https://portainer.example.com (Container management)
- https://jellyfin.example.com (Media streaming - local speed)
- https://sonarr.example.com (TV show management)
- https://qbittorrent.example.com (Torrents via VPN)
- https://metube.example.com (YouTube downloads via VPN)
- https://nextcloud.example.com (File sync)
- https://uptime.example.com (Service monitoring)
Local Device → Router DNS → NAS:443 → Traefik → Service
Benefits: Full bandwidth, no internet dependency, lowest latency
External → Cloudflare Tunnel → Specific Services
Use for: Nextcloud (file sync), Jellyseerr (remote requests)
qbittorrent/metube → Gluetun VPN → Internet
Benefits: IP masking, geographic flexibility, ISP protection
- In Portainer App Templates, deploy "Gluetun VPN"
- Configure your VPN credentials:
VPN_SERVICE_PROVIDER
(surfshark, nordvpn, etc.)WIREGUARD_PRIVATE_KEY
WIREGUARD_ADDRESSES
- Check gluetun logs:
docker logs gluetun
- Test IP:
docker exec gluetun curl ifconfig.me
- Should show VPN server IP, not your real IP
- metube: Regular deployment (no VPN needed for YouTube downloads)
- qbittorrent: Routes through gluetun VPN for security
qBittorrent Access (Setup Only)
- Normal operation: No direct access needed - Sonarr/Radarr communicate automatically
- Initial setup: Temporarily expose port in gluetun stack:
# Add to gluetun service temporarily ports: - "8080:8080" # Remove after setup complete
- After setup: Remove port exposure for maximum security
- Troubleshooting: Re-add port temporarily when needed
Workflow After Setup:
- Request media: Jellyseerr → Sonarr/Radarr
- Automatic download: *arr stack → qBittorrent (via VPN)
- Watch content: Jellyfin/Plex (local network speed)
- Zero manual intervention: qBittorrent operates invisibly through VPN
- Deploy service through Portainer App Templates
- Service automatically gets Traefik labels
- Instantly available at
https://service.example.com
Perfect for deploying your own applications:
version: "3.8"
services:
my-app:
build: .
container_name: my-app
networks:
- homeserver
restart: unless-stopped
labels:
- "my.zone=homeserver"
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.${SERVER_DOMAIN}`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=myresolver"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"
networks:
homeserver:
name: homeserver
external: true
- In Portainer: Stacks → Add Stack → Git Repository
- Enter your repo URL:
https://github.com/yourusername/project
- Add environment variables: Include
SERVER_DOMAIN
- Deploy: Service available at
https://project.example.com
Configure tunnels for services needing external access:
- Nextcloud: File sync from anywhere
- Jellyseerr: Remote media requests
- Uptime Kuma: Service monitoring
Forward ports 80/443 to your NAS:
- Pros: Simple setup
- Cons: Synology nginx port conflicts, security risks
Recommendation: Use Cloudflare Tunnels for security
Synology DSM uses ports 80 and 443 for its web interface, which conflicts with Traefik. Here's how to resolve this:
-
Change DSM ports (recommended approach):
sed -i -e 's/80/81/' -e 's/443/444/' /usr/syno/share/nginx/server.mustache /usr/syno/share/nginx/DSM.mustache /usr/syno/share/nginx/WWWService.mustache
-
Restart nginx service:
- DSM < 7:
synoservicecfg --restart nginx
- DSM ≥ 7:
sudo systemctl restart nginx
- DSM < 7:
-
Access DSM: Use
http://nas-ip:81
orhttps://nas-ip:444
instead of default ports
Instead of changing DSM ports, you can modify Traefik to use different ports in the docker-compose.yml:
ports:
- "8080:80" # HTTP
- "8443:443" # HTTPS
Then access services via https://service.example.com:8443
- Check DNS: Ensure
*.example.com
points to NAS IP - Check certificates:
docker logs traefik
- Cloudflare API: Verify
CF_DNS_API_TOKEN
is correct - Domain ownership: Must control DNS for Let's Encrypt
- Check gluetun logs:
docker logs gluetun
- Verify credentials: WireGuard keys must be valid
- Test connection:
docker exec gluetun curl ifconfig.me
- Port forwarding: Check if VPN supports it
- Check Traefik dashboard:
https://traefik.example.com
- Verify labels: Service must have proper Traefik labels
- Check networks: Service must be on
homeserver
network - DNS cache: Clear browser/system DNS cache
If Portainer shows "timeout.html" or security timeout message:
- Cause: Portainer requires admin setup within 2 minutes of first start
- Fix:
docker restart portainer # Immediately go to portainer.example.com and create admin user
- Alternative: Access directly via
http://nas-ip:9000
during setup - Note: This only happens on first setup - once admin is created, normal access works
Important: If a service has exposed ports in docker-compose (e.g., ports: - "9000:9000"
), it will bypass Traefik routing:
- Problem:
portainer.example.com
returns 404, butnas-ip:9000
works - Cause: Exposed ports take precedence over Traefik routing
- Solution:
- For initial setup: Temporarily expose ports, access via
nas-ip:port
- For production: Comment out port mappings to force Traefik routing
# ports: # - "9000:9000" # Disable for Traefik routing
- For initial setup: Temporarily expose ports, access via
- When to expose ports: Only for troubleshooting or when Traefik fails
- Local vs External: Use local URLs for best performance
- GPU transcoding: Uncomment device mappings in media services
- Storage: Ensure fast storage for media files
- Downloads isolated in VPN container
- Services communicate only through defined networks
- No direct port exposure (except Traefik 80/443)
- Local network: Full access to all services
- External: Only selected services via Cloudflare
- Authentication: Cloudflare Access for sensitive services
- Automatic Let's Encrypt renewal
- Wildcard certificates for all subdomains
- DNS challenge (no port 80 requirement)
- Local access only: No tunnel overhead
- Direct file access: Mount media directories properly
- GPU acceleration: Enable for transcoding if available
- Health checks: Services restart automatically
- Resource limits: Configured for containers
- Monitoring: Use cAdvisor and Uptime Kuma
- Core services:
docker-compose up -d
(Traefik + Portainer) - VPN stack: Deploy gluetun via Portainer (for qBittorrent only)
- Download services:
- Deploy metube (standalone - no VPN needed)
- Deploy media-server (qBittorrent uses gluetun VPN)
- Other services: Deploy in any order
Important: Only qBittorrent requires VPN deployment order - metube can be deployed anytime
This is a production-ready homeserver setup optimized for performance and security. If you find it useful:
- ⭐ Star the repository
- 🍴 Fork for your own modifications
- 📝 Share improvements via issues/PRs