Reverse Proxy Configuration β
A reverse proxy sits in front of MeshMonitor to handle SSL/TLS termination, load balancing, caching, and provide additional security. This guide covers popular reverse proxy solutions.
Why Use a Reverse Proxy? β
Benefits of using a reverse proxy:
- SSL/TLS Termination: Handle HTTPS encryption at the proxy level
- Security: Hide internal network topology, add security headers
- Load Balancing: Distribute traffic across multiple instances
- Caching: Cache static assets for better performance
- Centralized Logging: Single point for access logs
- Multiple Services: Host multiple applications on one server/domain
β οΈ Critical: Required Environment Variables β
When deploying MeshMonitor behind a reverse proxy with HTTPS, you MUST set these environment variables:
NODE_ENV=production # Enable production mode with full CSP
TRUST_PROXY=true # Trust proxy headers
COOKIE_SECURE=true # Enable secure cookies for HTTPS
ALLOWED_ORIGINS=https://meshmonitor.example.com # Allow CORS from your domainWithout NODE_ENV=production, you will get:
- Map tiles won't load (gray/blank tiles) for satellite, dark mode, light mode, and topographic maps
- Browser console shows CSP errors about refused connections to tile servers
- Only OpenStreetMap tiles will work
Without ALLOWED_ORIGINS, you will get:
- Blank white pages
- 500 errors on JavaScript files
- CORS errors in browser console: "Access to fetch at '...' has been blocked by CORS policy"
This happens because when using HTTPS, the browser considers the frontend and backend as different origins and blocks API requests for security. Setting ALLOWED_ORIGINS tells MeshMonitor to allow requests from your HTTPS domain.
NGINX β
NGINX is a popular, high-performance reverse proxy and web server.
Basic Configuration β
Create /etc/nginx/sites-available/meshmonitor:
server {
listen 80;
server_name meshmonitor.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name meshmonitor.example.com;
# SSL Configuration
ssl_certificate /etc/ssl/certs/meshmonitor.example.com.crt;
ssl_certificate_key /etc/ssl/private/meshmonitor.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Proxy Configuration
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts for long-running requests
proxy_read_timeout 90s;
proxy_connect_timeout 90s;
proxy_send_timeout 90s;
}
# Logging
access_log /var/log/nginx/meshmonitor-access.log;
error_log /var/log/nginx/meshmonitor-error.log;
}Enable the site:
sudo ln -s /etc/nginx/sites-available/meshmonitor /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxWith Let's Encrypt SSL β
Use Certbot for free SSL certificates:
# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d meshmonitor.example.com
# Auto-renewal is configured automaticallyDocker Compose with NGINX β
Run NGINX alongside MeshMonitor:
version: '3.8'
services:
meshmonitor:
image: ghcr.io/yeraze/meshmonitor:latest
environment:
- MESHTASTIC_NODE_IP=192.168.1.100
- SESSION_SECRET=your-secure-random-string
- NODE_ENV=production # Required for full CSP and map tiles
- TRUST_PROXY=true # Required when behind a reverse proxy
- COOKIE_SECURE=true # Enable secure cookies for HTTPS
- ALLOWED_ORIGINS=https://meshmonitor.example.com # REQUIRED for HTTPS!
- ACCESS_LOG_ENABLED=true # Optional: Enable for fail2ban
volumes:
- meshmonitor-data:/data
- ./meshmonitor-logs:/data/logs:rw # Optional: For fail2ban access
expose:
- "8080"
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/ssl:ro
depends_on:
- meshmonitor
networks:
- app-network
volumes:
meshmonitor-data:
networks:
app-network:
driver: bridgeApache β
Apache HTTP Server with mod_proxy.
Configuration β
Create /etc/apache2/sites-available/meshmonitor.conf:
<VirtualHost *:80>
ServerName meshmonitor.example.com
Redirect permanent / https://meshmonitor.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName meshmonitor.example.com
# SSL Configuration
SSLEngine on
SSLCertificateFile /etc/ssl/certs/meshmonitor.example.com.crt
SSLCertificateKeyFile /etc/ssl/private/meshmonitor.example.com.key
SSLCertificateChainFile /etc/ssl/certs/ca-bundle.crt
# Security Headers
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
# Proxy Configuration
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
# WebSocket Support
ProxyPass /ws ws://localhost:8080/ws
ProxyPassReverse /ws ws://localhost:8080/ws
# Logging
ErrorLog ${APACHE_LOG_DIR}/meshmonitor-error.log
CustomLog ${APACHE_LOG_DIR}/meshmonitor-access.log combined
</VirtualHost>Enable required modules and the site:
sudo a2enmod proxy proxy_http proxy_wstunnel ssl headers
sudo a2ensite meshmonitor
sudo apache2ctl configtest
sudo systemctl reload apache2Traefik β
Traefik is a modern reverse proxy designed for containerized environments.
Docker Compose with Traefik β
version: '3.8'
services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- app-network
meshmonitor:
image: ghcr.io/yeraze/meshmonitor:latest
environment:
- MESHTASTIC_NODE_IP=192.168.1.100
- SESSION_SECRET=your-secure-random-string
- NODE_ENV=production # Required for full CSP and map tiles
- TRUST_PROXY=true # Required when behind a reverse proxy
- COOKIE_SECURE=true # Enable secure cookies for HTTPS
- ALLOWED_ORIGINS=https://meshmonitor.example.com # REQUIRED for HTTPS!
- ACCESS_LOG_ENABLED=true # Optional: Enable for fail2ban
volumes:
- meshmonitor-data:/data
- ./meshmonitor-logs:/data/logs:rw # Optional: For fail2ban access
labels:
- "traefik.enable=true"
- "traefik.http.routers.meshmonitor.rule=Host(`meshmonitor.example.com`)"
- "traefik.http.routers.meshmonitor.entrypoints=websecure"
- "traefik.http.routers.meshmonitor.tls.certresolver=letsencrypt"
- "traefik.http.services.meshmonitor.loadbalancer.server.port=8080"
- "traefik.http.routers.meshmonitor-http.rule=Host(`meshmonitor.example.com`)"
- "traefik.http.routers.meshmonitor-http.entrypoints=web"
- "traefik.http.routers.meshmonitor-http.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
networks:
- app-network
volumes:
meshmonitor-data:
networks:
app-network:
driver: bridgeCaddy β
Caddy automatically handles HTTPS with Let's Encrypt.
Caddyfile β
Create a Caddyfile:
meshmonitor.example.com {
reverse_proxy localhost:8080
# Security headers (automatic)
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
X-XSS-Protection "1; mode=block"
}
# Logging
log {
output file /var/log/caddy/meshmonitor.log
format json
}
}Run Caddy:
caddy run --config CaddyfileDocker Compose with Caddy β
version: '3.8'
services:
caddy:
image: caddy:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- app-network
meshmonitor:
image: ghcr.io/yeraze/meshmonitor:latest
environment:
- MESHTASTIC_NODE_IP=192.168.1.100
- SESSION_SECRET=your-secure-random-string
- NODE_ENV=production # Required for full CSP and map tiles
- TRUST_PROXY=true # Required when behind a reverse proxy
- COOKIE_SECURE=true # Enable secure cookies for HTTPS
- ALLOWED_ORIGINS=https://meshmonitor.example.com # REQUIRED for HTTPS!
- ACCESS_LOG_ENABLED=true # Optional: Enable for fail2ban
volumes:
- meshmonitor-data:/data
- ./meshmonitor-logs:/data/logs:rw # Optional: For fail2ban access
expose:
- "8080"
networks:
- app-network
volumes:
caddy_data:
caddy_config:
meshmonitor-data:
networks:
app-network:
driver: bridgeKubernetes Ingress β
For Kubernetes deployments, use an Ingress controller.
NGINX Ingress β
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: meshmonitor-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- meshmonitor.example.com
secretName: meshmonitor-tls
rules:
- host: meshmonitor.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: meshmonitor
port:
number: 8080Security Considerations β
Security Headers β
Always include these security headers:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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;Rate Limiting β
Protect against abuse with rate limiting:
NGINX:
limit_req_zone $binary_remote_addr zone=meshmonitor:10m rate=10r/s;
server {
location / {
limit_req zone=meshmonitor burst=20 nodelay;
proxy_pass http://localhost:8080;
}
}IP Whitelisting β
Restrict access to specific IPs if needed:
location / {
allow 192.168.1.0/24;
allow 10.0.0.0/8;
deny all;
proxy_pass http://localhost:8080;
}Troubleshooting β
Blank White Page / 500 Errors on JavaScript Files β
Symptoms:
- White blank page when accessing MeshMonitor
- 500 Internal Server Error on
/assets/index-*.js - Browser console shows CORS errors:
Access to fetch at 'https://meshmonitor.example.com/api/...' has been blocked by CORS policy
Cause: Missing ALLOWED_ORIGINS environment variable
Solution: Add ALLOWED_ORIGINS to your docker-compose.yml or environment:
ALLOWED_ORIGINS=https://meshmonitor.example.comMultiple domains: Separate with commas:
ALLOWED_ORIGINS=https://meshmonitor.example.com,https://mesh.example.orgAfter adding, restart MeshMonitor:
docker compose down
docker compose up -dUnderstanding CORS Errors β
What is CORS? Cross-Origin Resource Sharing (CORS) is a browser security feature that blocks JavaScript from making requests to a different origin (domain, protocol, or port) than where the page was loaded from.
Why does it happen with HTTPS? When you access MeshMonitor via https://meshmonitor.example.com, but the API calls go to the backend, the browser considers them different origins and blocks the requests for security.
How ALLOWED_ORIGINS fixes it: Setting ALLOWED_ORIGINS=https://meshmonitor.example.com tells the MeshMonitor backend to send proper CORS headers that allow the browser to make API requests from that domain.
Checking CORS in browser console:
- Open browser DevTools (F12)
- Go to Console tab
- Look for errors containing "CORS policy" or "Access-Control-Allow-Origin"
- If you see these,
ALLOWED_ORIGINSis missing or incorrect
502 Bad Gateway β
Cause: Backend not reachable
Solution:
- Verify MeshMonitor is running:
docker psorsystemctl status meshmonitor - Check firewall rules
- Verify proxy_pass URL is correct
SSL Certificate Errors β
Cause: Invalid or expired certificate
Solution:
- Check certificate validity:
openssl x509 -in cert.crt -text -noout - Renew Let's Encrypt:
sudo certbot renew - Verify certificate chain is complete
WebSocket Connection Failed β
Cause: Proxy not configured for WebSocket upgrade
Solution: Ensure these headers are set:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';Login Works But Immediately Logs Out β
Cause: Cookie security mismatch
Solution: Ensure both are set when using HTTPS:
TRUST_PROXY=true
COOKIE_SECURE=trueIf using HTTP (not recommended), you must explicitly set:
COOKIE_SECURE=falseMap Tiles Not Loading (CSP Errors) β
Symptoms:
- Map displays but tiles don't load (gray/blank tiles)
- Browser console shows CSP errors:
Refused to connect to 'https://server.arcgisonline.com/...' because it violates the following Content Security Policy directive: "connect-src 'self' https://*.tile.openstreetmap.org" - Only OpenStreetMap tiles work, but satellite/dark/light/topo maps don't load
Cause: MeshMonitor is not running in production mode with secure cookies enabled. The development CSP only allows OpenStreetMap tiles for security reasons.
Solution: Set BOTH of these environment variables:
NODE_ENV=production
COOKIE_SECURE=true
TRUST_PROXY=true # Also required when behind a reverse proxyWhy this happens:
- MeshMonitor uses different Content Security Policy (CSP) headers based on deployment mode
- Development/HTTP mode: Restrictive CSP that only allows OpenStreetMap (to prevent accidental exposure)
- Production/HTTPS mode: Full CSP that allows all tile providers (OpenStreetMap, CartoDB, OpenTopoMap, Esri)
Verification: Check the CSP header in your browser DevTools:
- Open DevTools (F12)
- Go to Network tab
- Click on the main HTML document request
- Look at Response Headers > Content-Security-Policy
- The
connect-srcdirective should include:connect-src 'self' https://*.tile.openstreetmap.org https://*.basemaps.cartocdn.com https://*.tile.opentopomap.org https://server.arcgisonline.com
After changing, restart MeshMonitor:
docker compose down
docker compose up -dNext Steps β
- Configure HTTP vs HTTPS properly
- Set up production deployment with monitoring
- Configure SSO with proper redirect URIs