Featured image of post Cloudflare Tunnel + Nginx Self-Hosting Guide: Secure Exposure Without a Public IP

Cloudflare Tunnel + Nginx Self-Hosting Guide: Secure Exposure Without a Public IP

Zero port forwarding, zero public IP needed. Expose your local Nginx services to the internet securely with Cloudflare Tunnel. Covers cloudflared setup, HTTPS, WAF protection, and firewall hardening.

Why Cloudflare Tunnel?

In traditional self-hosting setups, exposing a local service to the internet typically requires:

  • A public IP address (many residential ISPs and basic VPS plans don’t provide one)
  • Port forwarding on your router
  • Manual SSL certificate management
  • Direct exposure to internet threats (CC attacks, DDoS, port scanning)

Cloudflare Tunnel (formerly Argo Tunnel) changes all of this:

Your server never needs to expose any open ports. Cloudflare establishes an encrypted tunnel from its edge network to your server, with all traffic routed through Cloudflare’s global CDN.

Key Advantages

FeatureTraditional Port ForwardingCloudflare Tunnel
Public IP Required✅ Yes❌ No
Ports ExposedAll forwarded portsZero ports
SSL CertificatesManual managementFree & automatic
DDoS ProtectionRequires separate solutionFree Cloudflare CDN
FirewallComplex configurationZero Trust policies
CostPort forwarding is freeFree tier available

Architecture Overview

┌─────────────┐     HTTPS      ┌──────────────────┐     Encrypted Tunnel     ┌──────────────────┐
│  User Browser│ ──────────────→│  Cloudflare CDN  │ ────────────────────────→│  Cloudflared Agent│
│              │     Edge      │  (WAF/CDN/Security)│                        │  (on your server) │
└─────────────┘                 └──────────────────┘                          └────────┬─────────┘
                                                                                        │
                                                                                        ▼
                                                                                 ┌──────────────────┐
                                                                                 │   Nginx (local)  │
                                                                                 │   :80 / :443     │
                                                                                 └──────────────────┘

Prerequisites

  • A Cloudflare account (free tier is sufficient)
  • A domain managed through Cloudflare
  • A VPS or local server (Ubuntu 22.04+ used in examples)
  • Docker installed (optional; binary installation also covered)

Step 1: Create a Cloudflare Tunnel

1. Create Tunnel in Cloudflare Dashboard

Log in to the Cloudflare Zero Trust Dashboard, navigate to Networks → Tunnels, and click Create a tunnel.

Select cloudflared as the connector type. You’ll receive a Tunnel Token — save it!

# Save this token — you'll need it in the next step
TUNNEL_TOKEN="eyJhIjoiY2YtMTIz...truncated..."

2. Register the Tunnel with the Token

# Install cloudflared
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

# Create a tunnel connection using the token
cloudflared tunnel --no-autoupdate create selfvps-tunnel

# Get the Tunnel ID
TUNNEL_ID=$(cat ~/.cloudflared/selfvps-tunnel.json | jq -r .TunnelID)
echo "Tunnel ID: $TUNNEL_ID"

Step 2: Configure Nginx Local Service

1. Install Nginx

sudo apt update && sudo apt install -y nginx
sudo systemctl enable --now nginx

2. Configure Nginx Server Block

Create /etc/nginx/sites-available/selfvps.net:

server {
    listen 80;
    server_name selfvps.net www.selfvps.net;

    root /var/www/selfvps.net;
    index index.html;

    # Basic security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        try_files $uri $uri/ =404;
    }

    # Static resource caching
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/selfvps.net /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

3. Verify Local Nginx is Working

curl -I http://localhost
# Should return 200 OK

Step 3: Configure Tunnel Routing

1. Create DNS Records

Add a CNAME record in Cloudflare DNS:

Hostname: www.selfvps.net
Type: CNAME
Target: <TUNNEL_ID>.cfargotunnel.com

Or manage DNS through Cloudflared directly:

cloudflared tunnel --no-autoupdate route dns selfvps-tunnel www.selfvps.net
cloudflared tunnel --no-autoupdate route dns selfvps-tunnel selfvps.net

2. Create Tunnel Configuration File

sudo mkdir -p /etc/cloudflared
sudo tee /etc/cloudflared/config.yaml > /dev/null << 'EOF'
tunnel: selfvps-tunnel
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json

protocol: http2

# Don't auto-update
no-autoupdate: true

ingress:
  # Main site
  - hostname: www.selfvps.net
    service: http://localhost:80

  # Blog subdomain
  - hostname: blog.selfvps.net
    service: http://localhost:80

  # Admin panel (source-restricted)
  - hostname: admin.selfvps.net
    service: http://localhost:8080
    originRequest:
      noTLSVerify: false
      originServerName: admin.selfvps.net

  # Default 404
  - service: http_status:404
EOF

Security Note: Ensure the <TUNNEL_ID>.json file has strict permissions:

sudo chmod 600 /etc/cloudflared/<TUNNEL_ID>.json
sudo chown root:root /etc/cloudflared/<TUNNEL_ID>.json

3. Use Native Cloudflare DNS Management

If you prefer not to let Cloudflared manage DNS records, add CNAME records manually in the Cloudflare dashboard. This is the recommended approach for production environments.

Step 4: Start the Tunnel Service

# Create systemd service
sudo tee /etc/systemd/system/cloudflared.service > /dev/null << 'EOF'
[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel --config /etc/cloudflared/config.yaml run
Restart=on-failure
RestartSec=5
KillMode=mixed

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

Or Using Docker

docker run -d --name cloudflared-tunnel \
  --restart unless-stopped \
  -v /etc/cloudflared:/etc/cloudflared \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run

Step 5: Configure Cloudflare Security Policies

1. Enable WAF (Web Application Firewall)

In the Cloudflare dashboard, go to Security → WAF:

  • Enable Super Bot Fight Mode (free)
  • Create custom rules for critical pages
# Example: Restrict API paths to specific HTTP methods
http.request.uri contains "/api" and not http.request.method in {"GET", "POST"} → Block

# Example: Challenge on login pages
http.request.uri contains "/admin/login" → Challenge

2. Configure Caching Rules

In Caching → Configuration:

Cache Level: Basic
Cache Everything: Off (for dynamic content)
Browser TTL: 2 hours
Edge TTL: 4 hours

For static assets:

Cache Rule:
Path: /*.{js,css,png,jpg,jpeg,gif,svg,ico,woff,woff2}
Cache Level: Cache Everything
Browser TTL: 1 month
Edge TTL: 1 week
Override Host: {origin}

3. Set Up Firewall Rules

Security → WAF → Managed Headers — Enable all options

Security → WAF → Managed Filters — Enable:

  • Cloudflare Essential Rules
  • Cloudflare OWASP Core Ruleset

4. Enable Always Use HTTPS

In SSL/TLS → Overview:

  • Encryption mode: Full (since cloudflared to origin is HTTP)
  • Always Use HTTPS: On

Step 6: Advanced Hardening

1. Bind Nginx to Loopback Only

# Only listen on 127.0.0.1, reject external connections
server {
    listen 127.0.0.1:80;
    server_name _;
    # ... rest of config
}
sudo nginx -t && sudo systemctl reload nginx

2. Configure UFW Firewall

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 7844/tcp  # cloudflared outbound (usually unnecessary)
sudo ufw enable

3. Enable Cloudflare Access Rules

In Zero Trust → Access → Applications:

  1. Add a Private Application
  2. Enter your subdomain (e.g., admin.selfvps.net)
  3. Configure Access Policy:
    • RequireOne or more of these → specify allowed email domains
    • Or require Cloudflare Access authentication

Only authorized users can now access the admin panel.

4. Configure Rate Limiting

# Max 100 requests per 10 seconds
http.request.uri path eq "/" and http.request.method eq "GET"
Rate limit: 100 requests per 10 seconds
Action: Challenge

Troubleshooting

Tunnel Disconnected

# Check cloudflared logs
journalctl -u cloudflared -f --no-pager

# Common issues:
# 1. DNS resolution failure → check network connectivity
# 2. Token expired → regenerate tunnel token
# 3. Firewall blocking → check outbound ports 7844/443

502 Bad Gateway

Usually indicates Nginx is not running or misconfigured:

sudo systemctl status nginx
sudo nginx -t
sudo journalctl -u nginx -n 50

SSL Certificate Issues

Cloudflare manages certificates automatically. If the certificate isn’t生效:

  1. Verify DNS records correctly point to the Tunnel
  2. SSL mode should be set to Full
  3. Clear browser cache and retry

Monitoring and Alerting

Health Checks

Add an origin health check in the Cloudflare dashboard:

  • URL: http://127.0.0.1:80/health
  • Interval: 60 seconds
  • Timeout: 10 seconds

Monitor cloudflared Status

# Simple health check script
cat > /usr/local/bin/check-tunnel.sh << 'EOF'
#!/bin/bash
if ! systemctl is-active --quiet cloudflared; then
    curl -s -X POST "https://hooks.slack.com/services/YOUR/WEBHOOK" \
        -d '{"text":"🚨 Cloudflared tunnel is DOWN!"}'
    sudo systemctl restart cloudflared
fi
EOF
chmod +x /usr/local/bin/check-tunnel.sh

# Add to crontab, check every minute
(crontab -l 2>/dev/null; echo "* * * * * /usr/local/bin/check-tunnel.sh") | crontab -

Summary

With Cloudflare Tunnel, you can:

  1. Zero port exposure — No ports open to the internet on your server
  2. Automatic HTTPS — Cloudflare manages SSL certificates for free
  3. CDN acceleration — Global edge caching for static resources
  4. DDoS protection — Free Cloudflare CDN layer protection
  5. Zero-trust access — Admin panel accessible only to authorized users
  6. No public IP needed — Deploy from any network environment

This setup is ideal for: blogs, documentation sites, admin panels, internal tools, CI/CD runners, and more.


This guide covers the complete journey from zero to production-ready. If your scenario has special requirements (WebSocket support, load balancing, multi-region deployment), feel free to discuss in the comments.

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials