Post

How to Tunnel Your Full-Stack Project to the Internet: A Complete Guide

How to Tunnel Your Full-Stack Project to the Internet: A Complete Guide

Ever wanted to share your local development project with the world? Whether it’s for demos, client reviews, or testing with remote users, tunneling your full-stack application to the internet is easier than you think. Let’s dive into the complete process, from setup to troubleshooting mixed content issues.

Why Tunnel Your Local Project?

Before we dive in, let’s understand why you’d want to expose your local development environment:

  • Client Demos: Show your work-in-progress to stakeholders instantly
  • Remote Testing: Let team members test features from anywhere
  • API Testing: Share your backend with frontend developers
  • Mobile Testing: Test your web app on real devices
  • Integration Testing: Connect with external services that need webhooks

The Architecture: How It All Works

Here’s what we’re building:

1
2
3
4
Internet → ngrok (HTTPS) → Caddy (Reverse Proxy) → Your Services
                                    ├── /api/* → Django Backend (Port 8000)
                                    ├── /media/* → Django Backend (Port 8000)  
                                    └── /* → React Frontend (Port 5173)

Key Components:

  • ngrok: Creates secure HTTPS tunnel from internet to your local machine
  • Caddy: Reverse proxy that routes requests to appropriate services
  • Django Backend: Your API server
  • React Frontend: Your user interface

Prerequisites

  • Docker and Docker Compose installed
  • ngrok account (free tier works fine)
  • A full-stack project (we’ll use Django + React as example)

Step 1: Setting Up ngrok

Install and Configure ngrok

1
2
3
4
5
6
7
# Install ngrok (Ubuntu/Debian)
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok

# Configure with your auth token
ngrok config add-authtoken YOUR_AUTH_TOKEN

Test ngrok

1
2
3
4
# Test with a simple web server
python -m http.server 8080
# In another terminal
ngrok http 8080

Step 2: Setting Up Caddy Reverse Proxy

Create a Caddyfile in your project root:

:8080 {
    # API routes go to Django backend
    reverse_proxy /api/* localhost:8000 {
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Host {host}
    }
    
    # Media files go to Django backend
    reverse_proxy /media/* localhost:8000 {
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-For {remote}
        header_up X-Forwarded-Host {host}
    }
    
    # Everything else goes to React frontend
    reverse_proxy /* localhost:5173
}

Install and run Caddy:

1
2
3
4
5
6
7
8
9
# Install Caddy
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

# Run Caddy
caddy run --config Caddyfile --adapter caddyfile

Step 3: Configuring Your Backend (Django)

Update Django Settings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# settings.py
import os

# Allow all hosts for tunneling
ALLOWED_HOSTS = ['*']

# CORS settings for frontend
CORS_ALLOW_ALL_ORIGINS = True

# Media files configuration
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# HTTPS detection for tunnel
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = False  # Let Caddy handle SSL

Update URL Configuration

1
2
3
4
5
6
7
8
9
10
# urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Step 4: Configuring Your Frontend (React)

Smart URL Detection

The key to making your frontend work with tunneling is smart URL detection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// api/axios.ts
import axios from 'axios';

// Auto-detect API URL based on protocol
const getBaseURL = () => {
    // If we're in production or accessed via tunnel (https), use relative URL
    if (window.location.protocol === 'https:' || import.meta.env.PROD) {
        return '/api';  // Relative URL goes through Caddy proxy
    }
    // For local development, use environment variable or default local IP
    return (import.meta.env.VITE_API_URL || 'http://localhost:8000') + '/api';
};

const api = axios.create({
    baseURL: getBaseURL(),
    timeout: 10000,
});

export default api;

Image URL Handling

1
2
3
4
5
6
7
8
9
10
11
// components/ImageComponent.tsx
const getImageUrl = (imagePath: string) => {
    if (imagePath.startsWith('http')) return imagePath;
    
    // Auto-detect base URL based on protocol
    const baseUrl = (window.location.protocol === 'https:' || import.meta.env.PROD) 
        ? ''  // Use relative URL for tunnel/production
        : 'http://localhost:8000';  // Fallback for local development
    
    return `${baseUrl}/media/${imagePath}`;
};

Step 5: Docker Compose Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  backend:
    build: ./backend
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./backend/media:/app/media
    environment:
      - DEBUG=${DEBUG}
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - DB_HOST=${DB_HOST}
      - DB_PORT=${DB_PORT}
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - DJANGO_ALLOWED_HOSTS=*
    ports:
      - "8000:8000"
    depends_on:
      - db

  frontend:
    build: ./frontend
    ports:
      - "5173:5173"
    depends_on:
      - backend

  caddy:
    image: caddy:2.7-alpine
    ports:
      - "8080:8080"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
    depends_on:
      - backend
      - frontend
    restart: unless-stopped

volumes:
  db_data:

Step 6: Running Your Tunnel

Quick Start Script

Create a simple script to start everything:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/bin/bash
# tunnel.sh

echo "🚀 Starting tunnel setup..."

# Start services if not running
if ! docker compose ps | grep -q "Up"; then
    echo "📦 Starting Docker services..."
    docker compose up -d
    sleep 8
fi

# Start Caddy if not running
if ! netstat -tlnp 2>/dev/null | grep -q ":8080"; then
    echo "🔧 Starting Caddy reverse proxy..."
    caddy run --config Caddyfile --adapter caddyfile &
    sleep 3
fi

# Start ngrok tunnel
echo "🌐 Starting ngrok tunnel..."
ngrok http 8080 --log=stdout &

# Wait for tunnel to establish
sleep 5

# Get tunnel URL
TUNNEL_URL=$(curl -s http://localhost:4040/api/tunnels 2>/dev/null | jq -r '.tunnels[0].public_url' 2>/dev/null)

if [ "$TUNNEL_URL" != "null" ] && [ -n "$TUNNEL_URL" ]; then
    echo ""
    echo "✅ Tunnel Active!"
    echo "🌍 Public URL: $TUNNEL_URL"
    echo "🔧 ngrok Web UI: http://localhost:4040"
    echo "🏠 Local Access: http://localhost:8080"
else
    echo "❌ Failed to create tunnel"
    exit 1
fi

Usage

1
2
3
4
5
6
7
8
9
10
# Make script executable
chmod +x tunnel.sh

# Start tunnel
./tunnel.sh

# Stop everything
docker compose down
pkill ngrok
pkill caddy

Common Issues and Solutions

Mixed Content Errors

Problem: Browser blocks HTTP requests from HTTPS pages.

Solution: Use relative URLs and smart protocol detection:

1
2
3
4
5
// ❌ Bad - hardcoded HTTP
const apiUrl = 'http://192.168.1.100:8000/api';

// ✅ Good - smart detection
const apiUrl = window.location.protocol === 'https:' ? '/api' : 'http://localhost:8000/api';

Media Files Not Loading

Problem: Images return HTML instead of actual files.

Solution: Ensure Caddy routes media files to Django:

# Caddyfile
:8080 {
    reverse_proxy /media/* localhost:8000 {  # ← This is crucial
        header_up X-Forwarded-Proto {scheme}
    }
    reverse_proxy /api/* localhost:8000
    reverse_proxy /* localhost:5173
}

CORS Issues

Problem: Frontend can’t access backend API.

Solution: Configure Django CORS properly:

1
2
3
# settings.py
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True

Security Considerations

Development Only

This setup is for development and demos only. For production:

  • Use proper domain names
  • Set up SSL certificates
  • Configure proper CORS policies
  • Use environment-specific settings

ngrok Security

  • Free tier URLs change on restart
  • Consider ngrok Pro for custom domains
  • Be mindful of what you expose

Final Thoughts

Tunneling your full-stack project to the internet opens up incredible possibilities for collaboration, demos, and testing. The key is proper configuration of your reverse proxy and smart URL detection in your frontend.

Remember:

  • Security first: This is for development only
  • Smart URLs: Detect protocol, don’t hardcode
  • Proper routing: Media files go to backend, not frontend
  • Monitor everything: Use the ngrok web interface

Now go forth and share your amazing projects with the world! 🚀

What’s your experience with tunneling? Any tips or tricks to share? Let me know in the comments!

This post is licensed under CC BY 4.0 by the author.