From ISP Lock-in to Control: Self-Hosting Nextcloud/Supervised Behind Cloudflare Tunnels
Published: May 27, 2026 | CommsNet
Your ISP gave you a CGNAT address, blocked port 25, and thinks "advanced router settings" means changing the WiFi password. You're paying for a connection you don't control, and every cloud subscription is another monthly tax on data you should own.
This article is the exit strategy. We're going to self-host Nextcloud for file sync, calendar, and contacts — and Home Assistant Supervised for home automation — all behind Cloudflare Tunnels so nothing is exposed directly to the internet. No port forwarding, no DDNS, no static IP required. Your ISP doesn't even need to know you're running services.
The Problem with ISP Defaults
| What ISPs Do | Why It Matters |
|---|---|
| CGNAT (Carrier-Grade NAT) | You can't receive inbound connections — no port forwarding possible |
| Blocked ports (25, 80, 443) | Even with a public IP, common services are blocked |
| Dynamic IP addresses | DDNS works around this, but it's fragile |
| Residential TOS restrictions | "No servers" clauses in fine print |
| No IPv6 or broken IPv6 | Dual-stack isn't optional anymore |
| Asymmetric bandwidth | Upload is 10% of download — your "cloud" uploads crawl |
The answer isn't a VPS that adds another monthly bill and another provider who can suspend you. The answer is Cloudflare Tunnels — outbound-only connections that punch through CGNAT, avoid blocked ports, and give you your own domain with TLS termination.
Architecture
┌─────────────────┐
│ Cloudflare Edge │
│ (Anycast CDN) │
└────────┬─────────┘
│ HTTPS
┌────────┴─────────┐
│ Cloudflare Tunnel │
│ (cloudflared) │
│ ← outbound only → │
└────────┬─────────┘
│ localhost
┌──────────────┼──────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Nextcloud │ │ Home │ │ Other │
│ :8080 │ │ Assistant │ │ Services │
│ │ │ :8123 │ │ :XXXX │
└────────────┘ └───────────┘ └────────────┘
│ │ │
┌─────┴─────────────┴──────────────┴────┐
│ Docker Network │
│ (internal only) │
└───────────────────────────────────────┘
Key insight: All connections are outbound from your server to Cloudflare. Your firewall never opens an inbound port. Your ISP never sees a server.
Step 1: Domain and Cloudflare Setup
Prerequisites
- A domain name (transfer to Cloudflare Registrar or use any registrar with Cloudflare DNS)
- A Cloudflare account (free tier works for tunnels)
- Docker and Docker Compose on your server
DNS Configuration
Point your domain's nameservers to Cloudflare, then add A/AAAA records for your services:
nextcloud.commsnet.org → Proxied (orange cloud) → Tunnel
hass.commsnet.org → Proxied (orange cloud) → Tunnel
files.commsnet.org → Proxied (orange cloud) → Tunnel
The "Proxied" toggle is critical — it enables Cloudflare's TLS termination and DDoS protection. Your origin server IP never appears in DNS.
Step 2: Cloudflare Tunnel
Install cloudflared
# Debian/Ubuntu
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
# Or via package manager
sudo cloudflared --version
Authenticate and Create Tunnel
# Authenticate with your Cloudflare account
cloudflared tunnel login
# Create the tunnel
cloudflared tunnel create homelab
# Note the tunnel ID from the output — you'll need it
# Example: TUNNEL_ID=a1b2c3d4-e5f6-7890-abcd-ef1234567890
Configure the Tunnel
Create ~/.cloudflared/config.yml:
tunnel: TUNNEL_ID
credentials-file: /home/youruser/.cloudflared/TUNNEL_ID.json
ingress:
# Nextcloud
- hostname: nextcloud.commsnet.org
service: http://localhost:8080
originRequest:
noTLSVerify: true
# Home Assistant
- hostname: hass.commsnet.org
service: http://localhost:8123
originRequest:
noTLSVerify: true
# Catch-all rule (required)
- service: http_status:404
DNS Records
# Create CNAME records pointing to the tunnel
cloudflared tunnel route dns homelab nextcloud.commsnet.org
cloudflared tunnel route dns homelab hass.commsnet.org
This automatically creates the CNAME records in Cloudflare DNS pointing <subdomain>.commsnet.org to <TUNNEL_ID>.cfargotunnel.com.
Test the Tunnel
cloudflared tunnel run homelab
If everything works, you'll see the tunnel connect and your services will be accessible at their respective hostnames — all over HTTPS, all without opening a single inbound port.
Run as a Service
# Install as systemd service
sudo cloudflared service install
# Enable and start
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
# Check status
sudo systemctl status cloudflared
Step 3: Nextcloud with Docker Compose
Directory Structure
nextcloud/
├── docker-compose.yml
├── .env
├── data/
│ ├── nextcloud/
│ ├── db/
│ └── redis/
└── config/
├── nginx/
│ └── nginx.conf
└── php/
└── uploads.ini
docker-compose.yml
version: "3.8"
services:
nextcloud-db:
image: postgres:16-alpine
container_name: nextcloud-db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./data/db:/var/lib/postgresql/data
networks:
- nextcloud-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 30s
timeout: 10s
retries: 3
nextcloud-redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- ./data/redis:/data
networks:
- nextcloud-internal
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 30s
timeout: 10s
retries: 3
nextcloud-app:
image: nextcloud:30-apache
container_name: nextcloud-app
restart: unless-stopped
depends_on:
nextcloud-db:
condition: service_healthy
nextcloud-redis:
condition: service_healthy
environment:
POSTGRES_HOST: nextcloud-db
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
REDIS_HOST: nextcloud-redis
REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
NEXTCLOUD_ADMIN_USER: ${NEXTCLOUD_ADMIN_USER}
NEXTCLOUD_ADMIN_PASSWORD: ${NEXTCLOUD_ADMIN_PASSWORD}
NEXTCLOUD_TRUSTED_DOMAINS: nextcloud.commsnet.org
OVERWRITEPROTOCOL: https
OVERWRITECLIURL: https://nextcloud.commsnet.org
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: 587
SMTP_SECURE: tls
SMTP_NAME: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
MAIL_FROM_ADDRESS: nextcloud
MAIL_DOMAIN: commsnet.org
volumes:
- ./data/nextcloud:/var/www/html
- ./config/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
networks:
- nextcloud-internal
- nextcloud-exposed
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:80/status.php || exit 1"]
interval: 60s
timeout: 15s
retries: 3
nextcloud-cron:
image: nextcloud:30-apache
container_name: nextcloud-cron
restart: unless-stopped
depends_on:
nextcloud-app:
condition: service_healthy
volumes:
- ./data/nextcloud:/var/www/html
entrypoint: /cron.sh
networks:
- nextcloud-internal
networks:
nextcloud-internal:
driver: bridge
internal: true # No external access — only app is exposed
nextcloud-exposed:
driver: bridge
Environment File
# .env — NEVER commit this to git
POSTGRES_DB=nextcloud
POSTGRES_USER=nextcloud_db
POSTGRES_PASSWORD=<generate-with: openssl rand -hex 32>
REDIS_PASSWORD=<generate-with: openssl rand -hex 32>
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=<generate-with: openssl rand -hex 32>
SMTP_HOST=smtp.example.com
SMTP_USER=nextcloud@commsnet.org
SMTP_PASSWORD=<your-smtp-password>
PHP Upload Limits
; config/php/uploads.ini
upload_max_filesize = 16G
post_max_size = 16G
max_execution_time = 3600
max_input_time = 3600
memory_limit = 512M
Important: Internal Network Segmentation
Notice the two Docker networks in the compose file:
-
nextcloud-internal— no external routing. Database and Redis can only be reached by Nextcloud containers. -
nextcloud-exposed— the app container sits on this network too, so Cloudflare Tunnel can reach it onlocalhost:8080.
The database and Redis are never accessible from outside the Docker network. Even if an attacker compromises the tunnel, they can't directly reach PostgreSQL or Redis.
Step 4: Home Assistant Supervised
Why Supervised, Not Home Assistant OS?
Home Assistant OS is the easy path, but it's a black box — you can't install custom add-ons, manage it with Docker Compose, or integrate it with your existing infrastructure. Supervised gives you the same UI and add-on store, but on your terms.
Prerequisites
# Install required packages for Supervised
sudo apt install -y \
apparmor \
jq \
network-manager \
dbus \
curl \
socat \
avahi-daemon \
udisks2 \
libglib2.0-bin
Docker Compose for Home Assistant
version: "3.8"
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
restart: unless-stopped
environment:
- TZ=America/Chicago
volumes:
- ./config/hass:/config
- /etc/localtime:/etc/localtime:ro
network_mode: host # HA needs host networking for device discovery
# Health check
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8123"]
interval: 60s
timeout: 10s
retries: 5
# Home Assistant Supervised Installer
# Run this ONCE to install the supervisor, then remove this service
hass-supervisor-installer:
image: homeassistant/amd64-hassio-supervisor:latest
container_name: hassio_supervisor
restart: unless-stopped
privileged: true
environment:
- SUPERVISOR_SHARE=/etc/hassio
- SUPERVISOR_NAME=hassio_supervisor
volumes:
- /etc/hassio:/etc/hassio
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/dbus:/var/run/dbus
- /etc/machine-id:/etc/machine-id:ro
network_mode: host
Alternative: Simpler Home Assistant Container (Non-Supervised)
If you don't need the add-on store, a plain HA container is simpler and more maintainable:
version: "3.8"
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
restart: unless-stopped
environment:
- TZ=America/Chicago
volumes:
- ./config/hass:/config
- /etc/localtime:/etc/localtime:ro
ports:
- "8123:8123"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8123"]
interval: 60s
timeout: 10s
retries: 5
networks:
- homeassistant
networks:
homeassistant:
driver: bridge
Cloudflare Tunnel Configuration for HA
Add to your ~/.cloudflared/config.yml:
# Home Assistant
- hostname: hass.commsnet.org
service: http://homeassistant:8123
originRequest:
noTLSVerify: true
Important: Home Assistant requires WebSocket support for real-time updates. Cloudflare Tunnels handle WebSocket proxying automatically — no extra configuration needed.
Step 5: Security Hardening
Nextcloud Security
After first login, configure these in config.php (or via OCC):
<?php
// data/nextcloud/config/config.php additions
'force_ssl' => true,
'default_phone_region' => 'US',
'memcache.local' => '\\OC\\Memcache\\APCu',
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' => [
'host' => 'nextcloud-redis',
'port' => 6379,
'password' => 'your-redis-password',
],
'maintenance' => false,
'theme' => '',
'loglevel' => 2,
'auth.bruteforce.protection.enabled' => true,
'share_folder' => '/Shared',
'trashbin_retention_obligation' => '30,180',
'activity_expire_days' => 90,
'simpleSignUpLink.shown' => false,
];
Fail2Ban Integration (Optional but Recommended)
If someone tries to brute-force your Nextcloud login, fail2ban will ban their IP at the firewall level:
# /etc/fail2ban/filter.d/nextcloud.conf
[Definition]
groups = nextcloud
failregex = ^.*Login failed.*Remote IP.*<HOST>.*$
^.*Bruteforce attempt.*Remote IP.*<HOST>.*$
ignoreregex =
# /etc/fail2ban/jail.d/nextcloud.conf
[nextcloud]
enabled = true
port = 80,443
filter = nextcloud
logpath = /path/to/nextcloud/data/nextcloud.log
maxretry = 5
bantime = 3600
findtime = 600
Note: Cloudflare Tunnels mean all traffic appears from Cloudflare IPs. Fail2ban won't be effective unless you configure the TRUSTED_PROXIES setting and use the X-Forwarded-For header. For most homelab users, Nextcloud's built-in brute-force protection is sufficient.
Home Assistant Security
# config/hass/configuration.yaml additions
# Require login
homeassistant:
auth_providers:
- type: homeassistant
- type: legacy_api_password # Remove after migration
# IP ban after failed attempts
http:
ip_ban_enabled: true
login_attempts_threshold: 5
use_x_forwarded_for: true
trusted_proxies:
- 172.16.0.0/12 # Docker networks
- 127.0.0.1 # Localhost (cloudflared)
Cloudflare Access (Zero Trust Authentication)
For an extra layer, add Cloudflare Zero Trust authentication before your service:
# Create an Access application in Cloudflare Zero Trust dashboard
# Settings → Zero Trust → Access → Applications → Add Application
# Configuration:
# Application name: Home Assistant
# Domain: hass.commsnet.org
# Policy: Email OTP (one-time password to your email)
# Or: Identity provider (Google, GitHub, etc.)
Now visitors hit Cloudflare's login wall before they even see Home Assistant. Two layers of authentication, zero ports open.
Step 6: Monitoring and Backups
Health Checks
# Add to your docker-compose.yml
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * * # 4 AM daily
- WATCHTOWER_NOTIFICATIONS=email
- WATCHTOWER_NOTIFICATION_EMAIL_FROM=watchtower@commsnet.org
- WATCHTOWER_NOTIFICATION_EMAIL_TO=admin@commsnet.org
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.example.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=${SMTP_USER}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=${SMTP_PASSWORD}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- nextcloud-internal
Backup Strategy
#!/bin/bash
# backup-nextcloud.sh — Run daily via cron
# 1. Put Nextcloud in maintenance mode
docker exec -u www-data nextcloud-app php occ maintenance:mode --on
# 2. Dump PostgreSQL
docker exec nextcloud-db pg_dump -U nextcloud_db nextcloud > /backups/nextcloud-db-$(date +%Y%m%d).sql
# 3. Sync data directory
rsync -az --delete /path/to/nextcloud/data/ /backups/nextcloud-data/
# 4. Take Nextcloud out of maintenance mode
docker exec -u www-data nextcloud-app php occ maintenance:mode --off
# 5. Upload to offsite (optional — Backblaze B2, S3, etc.)
# rclone sync /backups/ remote:nextcloud-backups/
Home Assistant Backups
# Home Assistant automatic backups (in configuration.yaml)
automation:
- alias: "Daily Backup"
trigger:
- platform: time
at: "03:00:00"
action:
- service: backup.create
data:
name: "Daily Backup {{ now().strftime('%Y-%m-%d') }}"
keep_days: 7
Troubleshooting
Tunnel Won't Connect
# Check cloudflared logs
sudo journalctl -u cloudflared -f
# Common issues:
# 1. DNS records not created — run `cloudflared tunnel route dns` again
# 2. Credentials file missing — check ~/.cloudflared/ for the JSON file
# 3. Firewall blocking outbound 7844 — Cloudflare uses this port for tunnel protocol
Nextcloud Shows Wrong URL
# Force the correct URL in config.php
docker exec -u www-data nextcloud-app php occ config:system:set overwrite.cli.url --value="https://nextcloud.commsnet.org"
docker exec -u www-data nextcloud-app php occ config:system:set overwriteprotocol --value="https"
Home Assistant Not Accessible
# Check container health
docker inspect homeassistant | jq '.[0].State'
# Check logs
docker logs homeassistant --tail 50
# Common issue: WebSocket not upgrading
# Ensure your Cloudflare tunnel config has noTLSVerify: true
# and that you're using http:// not https:// in the service URL
Performance: Nextcloud Slow
# Enable Redis caching (verify in config.php)
docker exec -u www-data nextcloud-app php occ status
# Check Redis connection
docker exec nextcloud-redis redis-cli -a "$REDIS_PASSWORD" ping
# Run maintenance tasks
docker exec -u www-data nextcloud-app php occ maintenance:repair
docker exec -u www-data nextcloud-app php occ files:scan --all
What You've Gained
| Before (ISP Defaults) | After (This Setup) |
|---|---|
| CGNAT — no inbound connections | Cloudflare Tunnel — outbound only |
| Cloud storage subscriptions | Self-hosted Nextcloud — your data, your rules |
| Exposed ports for services | Zero open inbound ports |
| No home automation centralization | Home Assistant with full control |
| Dynamic IP DDNS hacks | Cloudflare-managed DNS with automatic TLS |
| No backup strategy | Automated daily backups to offsite |
| Single point of failure | Health checks and automated recovery |
Monthly Cost Comparison
| Service | Cloud Subscription | Self-Hosted |
|---|---|---|
| File storage (1TB) | Google One: $9.99/mo | Nextcloud: $0 |
| Calendar/Contacts | Google: Free (data tax) | Nextcloud: $0 |
| Home Automation | Nabu Casa: $6.50/mo | HA Supervised: $0 |
| Domain | — | Cloudflare: ~$10/yr |
| Cloudflare Tunnels | — | Free |
| Electricity (est.) | — | ~$5-10/mo |
| Total | $16.49/mo | ~$1/mo + electricity |
You break even in under a year, and you own your data forever.
Next Steps
- Get a domain — transfer to Cloudflare or point nameservers there
- Set up Cloudflare Tunnel — 15 minutes, zero ports opened
- Deploy Nextcloud — Docker Compose up, configure, migrate your data
- Deploy Home Assistant — start small, add integrations over time
- Set up backups — automated, offsite, tested
- Monitor — add Watchtower for updates, health checks for uptime
Self-hosting isn't just about saving money. It's about owning your infrastructure, your data, and your time. When Google sunsets a product or Dropbox changes their terms, you won't care — because your files are on your hardware, behind your domain, on your terms.
CommsNet builds secure, sovereign infrastructure. More at wiki.commsnet.org
Tags: #selfhosted #nextcloud #homeassistant #cloudflare #tunnels #docker #privacy #homelab #degoogling











