DEV Community

Luna Commsnet
Luna Commsnet

Posted on • Edited on • Originally published at wiki.commsnet.org

From ISP Lock-in to Control: Self-Hosting Nextcloud/Home Assistant Behind Cloudflare Tunnels

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, plus 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, so no port forwarding is possible
Blocked ports (25, 80, 443) Even with a public IP, common service ports 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 is no longer optional
Asymmetric bandwidth Upload is 10% of download, so 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)                │
        └───────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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 and DNS Setup

You'll need a domain name (transfer to Cloudflare Registrar or use any registrar with Cloudflare DNS), a Cloudflare account (free tier works for tunnels), and Docker with Docker Compose on your server.

I learned this the hard way when I first tried self-hosting behind a CGNAT connection. Spent three days fighting port forwarding before realizing it was never going to work. Cloudflare Tunnels solved it in under an hour.

Point your domain's nameservers to Cloudflare, then add A/AAAA records for your services.

nextcloud.commsnet.orgProxied (orange cloud) → Tunnel
hass.commsnet.orgProxied (orange cloud) → Tunnel
files.commsnet.orgProxied (orange cloud) → Tunnel
Enter fullscreen mode Exit fullscreen mode

The "Proxied" toggle is critical because it enables Cloudflare's TLS termination and DDoS protection. Your origin server IP never appears in DNS.


Step 2: Cloudflare Tunnel

Install and Authenticate 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
Enter fullscreen mode Exit fullscreen mode

Authenticate and create your 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
Enter fullscreen mode Exit fullscreen mode

Now create ~/.cloudflared/config.yml with your tunnel configuration.

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
Enter fullscreen mode Exit fullscreen mode

DNS Records, Testing, and Service Setup

# Create CNAME records pointing to the tunnel
cloudflared tunnel route dns homelab nextcloud.commsnet.org
cloudflared tunnel route dns homelab hass.commsnet.org
Enter fullscreen mode Exit fullscreen mode

This automatically creates the CNAME records in Cloudflare DNS pointing <subdomain>.commsnet.org to <TUNNEL_ID>.cfargotunnel.com.

Test the tunnel by running it manually first.

cloudflared tunnel run homelab
Enter fullscreen mode Exit fullscreen mode

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. Honestly, the first time I saw this work behind a CGNAT connection, it felt like magic.

Once confirmed, install it as a systemd 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
Enter fullscreen mode Exit fullscreen mode

Step 3: Nextcloud with Docker Compose

Directory Structure and docker-compose.yml

nextcloud/
├── docker-compose.yml
├── .env
├── data/
│   ├── nextcloud/
│   ├── db/
│   └── redis/
└── config/
    ├── nginx/
    │   └── nginx.conf
    └── php/
        └── uploads.ini
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Environment File and PHP Tuning

# .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>
Enter fullscreen mode Exit fullscreen mode
; config/php/uploads.ini
upload_max_filesize = 16G
post_max_size = 16G
max_execution_time = 3600
max_input_time = 3600
memory_limit = 512M
Enter fullscreen mode Exit fullscreen mode

Notice the two Docker networks in the compose file. nextcloud-internal has no external routing, so the database and Redis can only be reached by Nextcloud containers. nextcloud-exposed is where the app container also sits, so Cloudflare Tunnel can reach it on localhost: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 Instead of 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.

Install the required packages first.

# Install required packages for Supervised
sudo apt install -y \
  apparmor \
  jq \
  network-manager \
  dbus \
  curl \
  socat \
  avahi-daemon \
  udisks2 \
  libglib2.0-bin
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Simpler Alternative (Non-Supervised)

If you don't need the add-on store, a plain HA container is simpler and more maintainable. This surprised me when I set it up for a family member who just wanted basic automations without the overhead.

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
Enter fullscreen mode Exit fullscreen mode

Cloudflare Tunnel Configuration for HA

Add this to your ~/.cloudflared/config.yml.

  # Home Assistant
  - hostname: hass.commsnet.org
    service: http://homeassistant:8123
    originRequest:
      noTLSVerify: true
Enter fullscreen mode Exit fullscreen mode

Important. Home Assistant requires WebSocket support for real-time updates. Cloudflare Tunnels handle WebSocket proxying automatically, so no extra configuration is needed.


Step 5: Security Hardening

Nextcloud Security and Fail2Ban

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,
];
Enter fullscreen mode Exit fullscreen mode

For brute-force protection, fail2ban can ban offending IPs at the firewall level. Here is the configuration.

# /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
Enter fullscreen mode Exit fullscreen mode

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 and Cloudflare Access

# 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)
Enter fullscreen mode Exit fullscreen mode

For an extra layer, add Cloudflare Zero Trust authentication before your service. This is worth the extra setup time.

# 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.)
Enter fullscreen mode Exit fullscreen mode

Now visitors hit Cloudflare's login wall before they even see Home Assistant. Two layers of authentication, zero ports open. I run this on my own homelab and it has been rock solid for months.


Step 6: Monitoring and Backups

Health Checks and Monitoring

# 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
Enter fullscreen mode Exit fullscreen mode

Backups

#!/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/
Enter fullscreen mode Exit fullscreen mode

For Home Assistant, add automatic backups to configuration.yaml.

# 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
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Common Issues and Fixes

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Nextcloud performance is 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
Enter fullscreen mode Exit fullscreen mode

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 $0 Cloudflare: ~$10/yr
Cloudflare Tunnels $0 Free
Electricity (est.) $0 ~$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

  1. Get a domain by transferring to Cloudflare or pointing nameservers there
  2. Set up Cloudflare Tunnel in 15 minutes, zero ports opened
  3. Deploy Nextcloud with Docker Compose, then configure and migrate your data
  4. Deploy Home Assistant starting small, adding integrations over time
  5. Set up backups that are automated, offsite, and tested
  6. Monitor with Watchtower for updates and 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


This article is also published on:

Originally published at wiki.commsnet.org. Follow CommsNet for more homelab, networking, and self-hosting content.

Sponsor our work: GitHub Sponsors

Top comments (0)