A complete WebRTC video calling application with signaling server, HTTP backend, and Next.js frontend. This guide covers local development with Cloudflare Tunnel for HTTPS testing.
- Prerequisites
- Project Structure
- Environment Setup
- Installation
- Docker Setup
- Cloudflare Tunnel Setup
- Running the Application
- Usage Guide
- Troubleshooting
- Node.js (v18 or higher)
- Docker and Docker Compose
- MongoDB (or use MongoDB Atlas)
- Cloudflare Account (free tier works)
- Metered.ca Account (for TURN/STUN servers)
.
βββ apps/
β βββ web/ # Next.js frontend
β βββ http-server/ # Express backend API
β βββ signaling-server/ # WebSocket signaling server
βββ docker-compose.dev.yaml
βββ docker-compose.tunnel.yaml
βββ README.md
- Sign up at metered.ca
- Create a new application
- Copy your STUN/TURN server credentials
# Cloudflare Tunnel URLs (update after tunnel creation)
NEXT_PUBLIC_SOCKET_URL=https://your-signaling-tunnel.trycloudflare.com
NEXT_PUBLIC_BACKEND_URL=https://your-backend-tunnel.trycloudflare.com
# Metered.ca STUN/TURN Servers
NEXT_PUBLIC_STUN_URL=stun:stun.relay.metered.ca:80
NEXT_PUBLIC_TURN_URL=turn:turn.relay.metered.ca:80
NEXT_PUBLIC_TURN_USERNAME=your_metered_username
NEXT_PUBLIC_TURN_PASSWORD=your_metered_password# Frontend URL (Cloudflare Tunnel)
NEXT_PUBLIC_FRONTEND_URL=https://your-web-tunnel.trycloudflare.com
# JWT Secret (generate a random string)
JWT_SECRET=your_super_secret_jwt_key_change_this
# MongoDB Connection
MONGO_URI=mongodb://localhost:27017/webrtc_caller
# Or use MongoDB Atlas:
# MONGO_URI=mongodb+srv://username:password@cluster.mongodb.net/webrtc_caller# Frontend URL (Cloudflare Tunnel)
NEXT_PUBLIC_FRONTEND_URL=https://your-web-tunnel.trycloudflare.com
# JWT Secret (must match http-server)
JWT_SECRET=your_super_secret_jwt_key_change_thisWindows (PowerShell):
iwr https://get.pnpm.io/install.ps1 -useb | iexmacOS/Linux:
curl -fsSL https://get.pnpm.io/install.sh | sh -Or via npm:
npm install -g pnpmVerify installation:
pnpm --version# Install all workspace dependencies
pnpm installFROM node:18-alpine AS base
# Install pnpm
RUN npm install -g pnpm
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY apps/web/package.json ./apps/web/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy application code
COPY apps/web ./apps/web
# Set working directory to web app
WORKDIR /app/apps/web
# Build the application
RUN pnpm build
# Expose port
EXPOSE 3000
# Start the application
CMD ["pnpm", "start"]FROM node:18-alpine
# Install pnpm
RUN npm install -g pnpm
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY apps/http-server/package.json ./apps/http-server/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy application code
COPY apps/http-server ./apps/http-server
# Set working directory to http-server
WORKDIR /app/apps/http-server
# Expose port
EXPOSE 3002
# Start the application
CMD ["pnpm", "start"]FROM node:18-alpine
# Install pnpm
RUN npm install -g pnpm
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY apps/signaling-server/package.json ./apps/signaling-server/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy application code
COPY apps/signaling-server ./apps/signaling-server
# Set working directory to signaling-server
WORKDIR /app/apps/signaling-server
# Expose port
EXPOSE 8080
# Start the application
CMD ["pnpm", "start"]version: '3.8'
services:
mongodb:
image: mongo:7.0
container_name: webrtc_mongodb
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
environment:
- MONGO_INITDB_DATABASE=webrtc_caller
networks:
- webrtc_network
http-server:
build:
context: .
dockerfile: apps/http-server/Dockerfile
container_name: webrtc_http_server
ports:
- "3002:3002"
environment:
- NODE_ENV=development
- PORT=3002
- MONGO_URI=mongodb://mongodb:27017/webrtc_caller
env_file:
- apps/http-server/.env
depends_on:
- mongodb
networks:
- webrtc_network
restart: unless-stopped
signaling-server:
build:
context: .
dockerfile: apps/signaling-server/Dockerfile
container_name: webrtc_signaling_server
ports:
- "8080:8080"
environment:
- NODE_ENV=development
- PORT=8080
env_file:
- apps/signaling-server/.env
networks:
- webrtc_network
restart: unless-stopped
web:
build:
context: .
dockerfile: apps/web/Dockerfile
container_name: webrtc_web
ports:
- "3000:3000"
environment:
- NODE_ENV=development
env_file:
- apps/web/.env
depends_on:
- http-server
- signaling-server
networks:
- webrtc_network
restart: unless-stopped
networks:
webrtc_network:
driver: bridge
volumes:
mongodb_data:version: '3.8'
services:
mongodb:
image: mongo:7.0
container_name: webrtc_mongodb
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
environment:
- MONGO_INITDB_DATABASE=webrtc_caller
networks:
- webrtc_network
http-server:
build:
context: .
dockerfile: apps/http-server/Dockerfile
container_name: webrtc_http_server
ports:
- "3002:3002"
environment:
- NODE_ENV=production
- PORT=3002
- MONGO_URI=mongodb://mongodb:27017/webrtc_caller
env_file:
- apps/http-server/.env
depends_on:
- mongodb
networks:
- webrtc_network
restart: unless-stopped
signaling-server:
build:
context: .
dockerfile: apps/signaling-server/Dockerfile
container_name: webrtc_signaling_server
ports:
- "8080:8080"
environment:
- NODE_ENV=production
- PORT=8080
env_file:
- apps/signaling-server/.env
networks:
- webrtc_network
restart: unless-stopped
web:
build:
context: .
dockerfile: apps/web/Dockerfile
container_name: webrtc_web
ports:
- "3000:3000"
environment:
- NODE_ENV=production
env_file:
- apps/web/.env
depends_on:
- http-server
- signaling-server
networks:
- webrtc_network
restart: unless-stopped
# Cloudflare Tunnel for Web Frontend
cloudflared-web:
image: cloudflare/cloudflared:latest
container_name: cloudflared_web
command: tunnel --no-autoupdate --url http://web:3000
depends_on:
- web
networks:
- webrtc_network
restart: unless-stopped
# Cloudflare Tunnel for HTTP Server
cloudflared-http:
image: cloudflare/cloudflared:latest
container_name: cloudflared_http
command: tunnel --no-autoupdate --url http://http-server:3002
depends_on:
- http-server
networks:
- webrtc_network
restart: unless-stopped
# Cloudflare Tunnel for Signaling Server
cloudflared-signaling:
image: cloudflare/cloudflared:latest
container_name: cloudflared_signaling
command: tunnel --no-autoupdate --url http://signaling-server:8080
depends_on:
- signaling-server
networks:
- webrtc_network
restart: unless-stopped
networks:
webrtc_network:
driver: bridge
volumes:
mongodb_data:Cloudflare Tunnel creates a secure outbound connection from your local machine to Cloudflare's network:
User β Cloudflare Edge β Tunnel β localhost:3000
Benefits:
- β No direct connections to your machine
- β Your IP stays hidden
- β WebSockets work automatically
- β Automatic HTTPS
Windows:
winget install --id Cloudflare.cloudflaredmacOS:
brew install cloudflare/cloudflare/cloudflaredLinux:
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/binVerify:
cloudflared --versionThis is the fastest way to get HTTPS URLs for testing:
# Terminal 1 - Web Frontend
cloudflared tunnel --url http://localhost:3000
# Terminal 2 - HTTP Server
cloudflared tunnel --url http://localhost:3002
# Terminal 3 - Signaling Server
cloudflared tunnel --url http://localhost:8080Each command will output a URL like:
https://random-name-xyz.trycloudflare.com
π Important: Copy these URLs and update your .env files accordingly!
For production use with a custom domain:
- Login to Cloudflare:
cloudflared tunnel login- Create a tunnel:
cloudflared tunnel create webrtc-caller- Create config file
~/.cloudflared/config.yml:
tunnel: <tunnel-id>
credentials-file: /path/to/credentials.json
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
- hostname: api.yourdomain.com
service: http://localhost:3002
- hostname: signal.yourdomain.com
service: http://localhost:8080
- service: http_status:404- Route DNS:
cloudflared tunnel route dns webrtc-caller app.yourdomain.com
cloudflared tunnel route dns webrtc-caller api.yourdomain.com
cloudflared tunnel route dns webrtc-caller signal.yourdomain.com- Run tunnel:
cloudflared tunnel run webrtc-caller# Start MongoDB locally or use MongoDB Atlas
# Terminal 1 - HTTP Server
cd apps/http-server
pnpm install
pnpm dev
# Terminal 2 - Signaling Server
cd apps/signaling-server
pnpm install
pnpm dev
# Terminal 3 - Web Frontend
cd apps/web
pnpm install
pnpm dev
# Terminal 4, 5, 6 - Cloudflare Tunnels (run after services start)
cloudflared tunnel --url http://localhost:3000
cloudflared tunnel --url http://localhost:3002
cloudflared tunnel --url http://localhost:8080# Build and start all services
docker-compose -f docker-compose.dev.yml up --build
# In separate terminals, create tunnels
cloudflared tunnel --url http://localhost:3000
cloudflared tunnel --url http://localhost:3002
cloudflared tunnel --url http://localhost:8080# Start all services with integrated Cloudflare tunnels
docker-compose -f docker-compose.tunnel.yml up --build
# Check logs for tunnel URLs
docker logs cloudflared_web
docker logs cloudflared_http
docker logs cloudflared_signaling.env files and restart services.
-
Account 1:
- Open the web app:
https://your-web-tunnel.trycloudflare.com - Click "Sign Up"
- Enter username and password
- Complete registration
- Open the web app:
-
Account 2:
- Open app in incognito window or different browser
- Repeat registration process with different credentials
- Login with Account 1
- Navigate to
/chatRoomroute - Click "Create Room" button
- Copy the generated Room ID (e.g.,
room-abc123)
- Login with Account 2 (in separate browser/incognito)
- Navigate to "Join Room" page
- Paste the Room ID from Step 2
- Click "Join Room"
- Both users should now see each other's video feeds
- Camera and microphone will be requested for permission
- Use the controls to:
- π€ Mute/unmute microphone
- πΉ Enable/disable video
- π End call
Problem: Video not connecting or black screen
Solutions:
- Verify TURN/STUN credentials in
apps/web/.env - Check Cloudflare tunnels are running and accessible
- Open browser console (F12) and check for ICE connection errors
- Ensure browser has camera/microphone permissions
- Try different browsers (Chrome, Firefox, Safari)
Problem: CORS policy blocking requests
Solutions:
- Ensure
NEXT_PUBLIC_FRONTEND_URLexactly matches your web tunnel URL - Check that all services use HTTPS URLs from Cloudflare (not HTTP)
- Verify no trailing slashes in environment variables
- Restart all services after changing CORS settings
Problem: Cannot connect to MongoDB
Solutions:
- Check MongoDB is running:
docker ps | grep mongo - Verify
MONGO_URIin environment variables - For local MongoDB: ensure port 27017 is not in use
- For MongoDB Atlas:
- Whitelist all IPs (0.0.0.0/0) during development
- Check username/password are correct
- Verify cluster is running
Problem: Services can't read environment variables
Solutions:
- Ensure
.envfiles exist in correct directories:apps/web/.envapps/http-server/.envapps/signaling-server/.env
- Restart Docker containers after changing
.env - Check for typos in variable names (case-sensitive)
- Verify no spaces around
=in.envfiles
Problem: Tunnel URLs stop working or become inaccessible
Solutions:
- Quick tunnels are temporary - URLs change on restart
- Use permanent tunnels for stable testing
- Check
cloudflaredlogs for errors - Ensure stable internet connection
- Try restarting the tunnel
Problem: Token validation fails
Solutions:
- Ensure
JWT_SECRETis identical in both:apps/http-server/.envapps/signaling-server/.env
- Clear browser cookies and local storage
- Re-login to generate new tokens
- Use Docker for consistency - Same environment across all team members
- MongoDB Atlas for production - Easier than managing local MongoDB
- Permanent tunnels for team testing - Stable URLs for shared development
- Environment variable checklist - Double-check all
.envfiles before starting - Browser DevTools - Monitor WebRTC connections in Chrome's
chrome://webrtc-internals - Test with multiple browsers - Verify cross-browser compatibility
# Start services
docker-compose -f docker-compose.dev.yml up
# Start in background
docker-compose -f docker-compose.dev.yml up -d
# Stop all containers
docker-compose -f docker-compose.dev.yml down
# Remove all volumes (fresh start)
docker-compose -f docker-compose.dev.yml down -v
# Rebuild specific service
docker-compose -f docker-compose.dev.yml up --build web
# View logs
docker logs webrtc_web -f
docker logs webrtc_http_server -f
docker logs webrtc_signaling_server -f
# Check running containers
docker ps
# Execute command in container
docker exec -it webrtc_web sh# Install dependencies
pnpm install
# Run specific service
cd apps/web && pnpm dev
cd apps/http-server && pnpm dev
cd apps/signaling-server && pnpm dev
# Build for production
pnpm build
# Clean install
rm -rf node_modules
pnpm install --frozen-lockfile- Never commit
.envfiles - Add them to.gitignore - Use strong JWT secrets - Generate random strings (32+ characters)
- MongoDB security - Use authentication in production
- TURN server credentials - Rotate regularly
- Rate limiting - Implement on signaling server for production
βββββββββββββββββββ
β Web Browser β
β (Next.js) β
ββββββββββ¬βββββββββ
β
βββββ HTTP βββββΊ ββββββββββββββββββββ
β β HTTP Server β
β β (Express API) β
β ββββββββββ¬ββββββββββ
β β
βββ WebSocket βββΊ βββββββββΌββββββββββ
β Signaling Serverβ
β (Socket.io) β
ββββββββββ¬βββββββββ
β
βββββββββΌβββββββββ
β MongoDB β
ββββββββββββββββββ
- WebRTC Documentation
- Cloudflare Tunnel Docs
- Socket.io Documentation
- Next.js Documentation
- pnpm Documentation
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Open a Pull Request