Reverse Proxy
A reverse proxy should be placed in front of the OpenVLE backend and frontend. It handles SSL termination, routes requests to the correct services, and can optionally provide load balancing.
Prerequisites
- Three services must be accessible through the reverse proxy:
- Backend API (Port 8000) — Path
/v1/ - Frontend (Port 80) — all other paths
- Apache Guacamole (Port 8080) — separate subdomain (e.g.,
desktop.example.com) (optional, only if Guacamole should be accessible through this reverse proxy)
- Backend API (Port 8000) — Path
- Two hostnames must be configured (e.g.,
openvle.example.comanddesktop.example.com) — without Guacamole, a single hostname is sufficient - SSL certificates for both hostnames (e.g., via Let's Encrypt)
- WebSocket support for Guacamole (required for remote desktop connections) (only when connecting Guacamole)
The Guacamole configuration in the following examples is optional. If you are not using Guacamole or are running Guacamole behind its own reverse proxy on a separate server, you can omit the Guacamole-related sections (subdomain desktop.example.com, port 8080).
Configuration
We recommend Traefik, as it runs natively as a Docker container and can be dynamically configured via Docker labels — ideal for a Docker Compose-based setup like OpenVLE. Alternatively, HAProxy, Caddy, Nginx, or Apache are also possible.
- Traefik (recommended)
- HAProxy
- Caddy
- Nginx
Traefik can be operated as an additional service directly within the Docker Compose stack. Configuration is done via labels on individual services — no separate configuration files are needed.
This is a docker-compose.override.yml — the actual docker-compose.yml for the backend and frontend is still required.
---
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
ports:
- 0.0.0.0:80:80
- 0.0.0.0:443:443
command:
# Provider
- --providers.docker=true
- --providers.docker.exposedbydefault=false
# EntryPoints
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# HTTP → HTTPS Redirect
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
# ACME (Let's Encrypt)
- --certificatesresolvers.letsencrypt.acme.email=admin@example.com
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/letsencrypt
labels:
- traefik.enable=true
backend:
labels:
- traefik.enable=true
- traefik.http.routers.backend.rule=Host(`openvle.example.com`) && PathPrefix(`/v1/`)
- traefik.http.routers.backend.entrypoints=websecure
- traefik.http.routers.backend.tls=true
- traefik.http.routers.backend.tls.certresolver=letsencrypt
- traefik.http.routers.backend.priority=200
- traefik.http.services.backend.loadbalancer.server.port=8000
frontend:
labels:
- traefik.enable=true
- traefik.http.routers.frontend.rule=Host(`openvle.example.com`) && PathPrefix(`/`)
- traefik.http.routers.frontend.entrypoints=websecure
- traefik.http.routers.frontend.tls=true
- traefik.http.routers.frontend.tls.certresolver=letsencrypt
- traefik.http.routers.frontend.priority=100
- traefik.http.services.frontend.loadbalancer.server.port=80
# Optional: Only required if Guacamole should be accessible through this reverse proxy
guacamole:
labels:
- traefik.enable=true
- traefik.http.routers.guacamole.rule=Host(`desktop.example.com`)
- traefik.http.routers.guacamole.entrypoints=websecure
- traefik.http.routers.guacamole.tls=true
- traefik.http.routers.guacamole.tls.certresolver=letsencrypt
- traefik.http.services.guacamole.loadbalancer.server.port=8080
- Replace
openvle.example.comanddesktop.example.comwith your hostnames - Replace
admin@example.comwith your email address for Let's Encrypt - The
priorityof the backend router must be higher than the frontend's, so that/v1/requests are routed correctly
This HAProxy configuration sets up a reverse proxy that performs SSL termination, redirects HTTP requests to HTTPS, and routes requests to the three services based on hostnames and URL paths.
- Replace
/your-ssl-certs.certlistwith the path to your SSL certificate list - Replace
desktop.example.comandopenvle.example.comwith your hostnames - Replace
10.1.1.10with the IP address of your OpenVLE server
global
[...]
defaults
[...]
frontend frontend_http
bind 0.0.0.0:80 name 0.0.0.0:80
mode http
option http-keep-alive
option httplog
http-request redirect code 301 location https://%[hdr(host)]%[capture.req.uri]
frontend frontend_https
bind 0.0.0.0:443 name 0.0.0.0:443 ssl alpn h2,http/1.1 crt-list /your-ssl-certs.certlist
mode http
option http-keep-alive
option httplog
acl condition_path_openvle_backend_v1 path_beg -i /v1/
acl condition_path_openvle_frontend_v1 path_beg -i /v1/
# Optional: Only required if Guacamole should be accessible through this reverse proxy
acl condition_host_guacamole hdr(host) -i desktop.example.com
acl condition_host_openvle hdr(host) -i openvle.example.com
http-request set-header X-Forwarded-Proto https
use_backend openvle-backend if condition_host_openvle condition_path_openvle_backend_v1
use_backend openvle-frontend if condition_host_openvle !condition_path_openvle_frontend_v1
use_backend openvle-guacamole if condition_host_guacamole
backend openvle-backend
mode http
balance source
stick-table type ip size 50k expire 30m
stick on src
http-reuse safe
option forwardfor
server openvle-backend01 10.1.1.10:8000
backend openvle-frontend
option httpchk
http-check send meth OPTIONS uri / ver HTTP/1.1 hdr Host localhost
mode http
balance source
stick-table type ip size 50k expire 30m
stick on src
http-reuse safe
option forwardfor
server openvle-frontend01 10.1.1.10:80 check inter 10s
# Optional: Only required if Guacamole should be accessible through this reverse proxy
backend openvle-guacamole
option httpchk
http-check send meth OPTIONS uri / ver HTTP/1.1 hdr Host localhost
mode http
balance source
stick-table type ip size 50k expire 30m
stick on src
http-reuse safe
option forwardfor
server openvle-guacamole01 10.1.1.10:8080 check inter 10s
Caddy handles SSL termination automatically via Let's Encrypt — no manual certificate configuration is needed.
- Replace
openvle.example.comanddesktop.example.comwith your hostnames - Replace
10.1.1.10with the IP address of your OpenVLE server
openvle.example.com {
handle /v1/* {
reverse_proxy 10.1.1.10:8000
}
handle {
reverse_proxy 10.1.1.10:80
}
}
# Optional: Only required if Guacamole should be accessible through this reverse proxy
desktop.example.com {
reverse_proxy 10.1.1.10:8080
}
This Nginx configuration sets up a reverse proxy with SSL termination and routes requests to the three services.
- Replace the paths under
ssl_certificateandssl_certificate_keywith your certificate paths (e.g., from Let's Encrypt) - Replace
openvle.example.comanddesktop.example.comwith your hostnames - Replace
10.1.1.10with the IP address of your OpenVLE server
# HTTP → HTTPS Redirect
server {
listen 80;
server_name openvle.example.com desktop.example.com;
return 301 https://$host$request_uri;
}
# OpenVLE (Backend + Frontend)
server {
listen 443 ssl;
server_name openvle.example.com;
ssl_certificate /etc/letsencrypt/live/openvle.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/openvle.example.com/privkey.pem;
# Backend API
location /v1/ {
proxy_pass http://10.1.1.10:8000;
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;
}
# Frontend
location / {
proxy_pass http://10.1.1.10:80;
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;
}
}
# Optional: Only required if Guacamole should be accessible through this reverse proxy
server {
listen 443 ssl;
server_name desktop.example.com;
ssl_certificate /etc/letsencrypt/live/desktop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/desktop.example.com/privkey.pem;
location / {
proxy_pass http://10.1.1.10:8080;
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;
# WebSocket support (required for Apache Guacamole)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Verification
After configuring the reverse proxy:
- Check HTTPS access —
https://openvle.example.comshould display the frontend - Check API access —
https://openvle.example.com/v1/system/settingsshould return a JSON response with the current system settings. An empty or erroneous response indicates an incorrect routing configuration. - Check Guacamole access (if configured) —
https://desktop.example.comshould display the Guacamole login page - Check HTTP redirect —
http://openvle.example.comshould redirect to HTTPS
Troubleshooting
| Problem | Solution |
|---|---|
| SSL certificate error | Make sure the certificate paths are correctly configured and the certificates are valid. For Let's Encrypt: check whether port 80 is reachable from the outside. |
| 502 Bad Gateway | The target service is not reachable. Check whether the containers are running (docker ps -a) and the IP addresses/ports in the proxy configuration are correct. |
| WebSocket error with Guacamole | Make sure the WebSocket headers (Upgrade, Connection) are set in the proxy configuration (see Nginx example). |
| Frontend reachable, API calls fail | Check whether the routing rules for /v1/ correctly forward to the backend and not to the frontend. For Traefik: the priority of the backend router must be higher than the frontend's. |