Self-Hosted XR Development Environment with Docker & Traefik
Self-Hosted XR Development Environment with Docker & Traefik
Creating a professional XR (Extended Reality) development environment can be complex, but containerization with Docker and reverse proxying with Traefik makes it manageable and scalable. This comprehensive guide walks you through setting up a complete, production-ready XR development stack that you can self-host with full control.
Introduction
Why Self-Host XR Development?
Modern XR development requires multiple services working together: development servers, build tools, backend APIs, real-time communication servers, and often database systems. Self-hosting gives you:
- Complete control over your development environment
- No vendor lock-in - your tools, your way
- Privacy and security - your code never leaves your infrastructure
- Cost efficiency - no subscription fees for development tools
- Customization - tailor every component to your specific needs
Benefits of Containerized XR Workflows:
- Reproducibility - identical environments across team members
- Isolation - separate projects don't interfere with each other
- Scalability - easily add more resources when needed
- Easy deployment - move from development to production with confidence
- Team collaboration - share consistent development environments
Prerequisites
Hardware Requirements
Minimum Requirements:
- CPU: 4 cores (Intel i5 or equivalent)
- RAM: 8GB (16GB recommended)
- Storage: 50GB SSD
- GPU: Integrated graphics acceptable for basic 3D work
Recommended Requirements:
- CPU: 8+ cores (Intel i7/i9, AMD Ryzen 7/9)
- RAM: 32GB+
- Storage: 500GB+ NVMe SSD
- GPU: NVIDIA RTX 3060 or equivalent with 6GB+ VRAM
- Network: Gigabit Ethernet (for team collaboration)
Software Dependencies
Required Software:
- Docker Engine 24.0+
- Docker Compose v2.20+
- NVIDIA Container Toolkit (for GPU acceleration)
- Git
- Node.js 18+ (for some XR frameworks)
- Modern web browser with WebXR support
Network Setup Requirements:
- Static IP or dynamic DNS service
- Domain name (optional but recommended for SSL)
- Open ports: 80, 443, additional ports for development tools
- Firewall configuration for Traefik
Infrastructure Setup
Base Docker Configuration
Enable GPU Support (NVIDIA):
First, install the NVIDIA Container Toolkit:
# Add NVIDIA repository and install toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
# Configure Docker to use NVIDIA runtime
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# Verify GPU access
docker run --rm --gpus all nvidia/cuda:11.0.3-base-ubuntu20.04 nvidia-smi
Optimize Docker Daemon:
Create or edit /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
},
"default-runtime": "nvidia"
}
Restart Docker:
sudo systemctl restart docker
Traefik Reverse Proxy Configuration
Create Traefik Directory Structure:
mkdir -p ~/xr-dev-env/{traefik,projects,shared}
cd ~/xr-dev-env/traefik
Create Traefik Configuration:
traefik.yml:
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
letsencrypt:
acme:
email: your-email@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
Create Docker Compose for Traefik:
docker-compose.yml:
version: '3.8'
services:
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
command:
- "--configFile=/etc/traefik/traefik.yml"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml
- ./letsencrypt:/letsencrypt
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`traefik.your-domain.com`)"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_USERS}"
environment:
- TRAEFIK_USERS=admin:$$apr1$$hashhere # Generate with: htpasswd -nb admin password
networks:
- proxy
networks:
proxy:
external: true
Create the external network:
docker network create proxy
Start Traefik:
cd ~/xr-dev-env/traefik
docker-compose up -d
Traefik Middlewares for XR:
Create dynamic-config.yml:
http:
middlewares:
cors:
headers:
accesscontrolallowmethods: GET,POST,OPTIONS,PUT,DELETE
accesscontrolallowheaders: Authorization,Content-Type,Depth,User-Agent,X-File-Size,X-Requested-With,X-Requested-By,If-Modified-Since,X-File-Name,Cache-Control
accesscontrolalloworiginlist: "*"
accesscontrolmaxage: 100
addvaryheader: true
websocket-headers:
headers:
customrequestheaders:
X-Forwarded-Proto: "https"
accesscontrolallowheaders: Authorization,Content-Type,Depth,User-Agent,X-File-Size,X-Requested-With,X-Requested-By,If-Modified-Since,X-File-Name,Cache-Control,Upgrade,Connection,Sec-WebSocket-Key,Sec-WebSocket-Version,Sec-WebSocket-Extensions,Sec-WebSocket-Accept,Sec-WebSocket-Protocol
accesscontrolallowmethods: GET,POST,OPTIONS
accesscontrolalloworiginlist: "*"
compress:
compress: true
ratelimit:
rateLimit:
average: 100
burst: 50
XR Development Stack Components
Three.js Development Server
Create Development Server Container:
Create directory ~/xr-dev-env/projects/threejs-dev/:
docker-compose.yml:
version: '3.8'
services:
dev-server:
image: node:18-alpine
container_name: threejs-dev-server
restart: unless-stopped
working_dir: /app
command: sh -c "npm install && npm run dev"
volumes:
- ./src:/app/src
- ./public:/app/public
- /app/node_modules
labels:
- "traefik.enable=true"
- "traefik.http.routers.threejs-dev.rule=Host(`dev.your-domain.com`)"
- "traefik.http.routers.threejs-dev.tls=true"
- "traefik.http.routers.threejs-dev.tls.certresolver=letsencrypt"
- "traefik.http.routers.threejs-dev.middlewares=websocket-headers,cors"
- "traefik.http.services.threejs-dev.loadbalancer.server.port=3000"
environment:
- NODE_ENV=development
networks:
- proxy
networks:
proxy:
external: true
package.json:
{
"name": "threejs-xr-dev",
"version": "1.0.0",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3000",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"three": "^0.160.0",
"@types/three": "^0.160.0"
},
"devDependencies": {
"vite": "^5.0.0"
}
}
Simple Three.js + WebXR Example:
src/main.js:
import * as THREE from 'three';
let camera, scene, renderer;
let controller;
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x808080);
camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.01,
20
);
// Create a simple cube
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 1.6, -1);
scene.add(cube);
// Lighting
const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1);
light.position.set(0.5, 1, 0.25);
scene.add(light);
// Renderer with WebXR support
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
// XR Controller
controller = renderer.xr.getController(0);
scene.add(controller);
// Handle resize
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
renderer.render(scene, camera);
}
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js XR Development</title>
<style>
body { margin: 0; overflow: hidden; }
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div id="info">Three.js + WebXR Development Environment</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
WebXR Backend Services
Real-Time Multiplayer Server (WebSocket):
Create directory ~/xr-dev-env/projects/webxr-backend/:
docker-compose.yml:
version: '3.8'
services:
websocket-server:
image: node:18-alpine
container_name: webxr-websocket
restart: unless-stopped
working_dir: /app
command: sh -c "npm install && npm start"
volumes:
- ./src:/app/src
- /app/node_modules
labels:
- "traefik.enable=true"
- "traefik.http.routers.ws-server.rule=Host(`ws.your-domain.com`)"
- "traefik.http.routers.ws-server.tls=true"
- "traefik.http.routers.ws-server.tls.certresolver=letsencrypt"
- "traefik.http.routers.ws-server.middlewares=websocket-headers"
- "traefik.http.services.ws-server.loadbalancer.server.port=8080"
networks:
- proxy
redis:
image: redis:7-alpine
container_name: webxr-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- proxy
volumes:
redis-data:
networks:
proxy:
external: true
package.json:
{
"name": "webxr-backend",
"version": "1.0.0",
"scripts": {
"start": "node src/server.js"
},
"dependencies": {
"ws": "^8.16.0",
"redis": "^4.6.0",
"express": "^4.18.0"
}
}
src/server.js:
const WebSocket = require('ws');
const Redis = require('redis');
// Redis client for session management
const redisClient = Redis.createClient({
url: 'redis://redis:6379'
});
redisClient.connect().catch(console.error);
// WebSocket server
const wss = new WebSocket.Server({ port: 8080 });
const sessions = new Map();
wss.on('connection', (ws) => {
const sessionId = generateSessionId();
sessions.set(sessionId, ws);
console.log(`New client connected: ${sessionId}`);
ws.on('message', async (message) => {
try {
const data = JSON.parse(message);
switch (data.type) {
case 'join':
await handleJoin(ws, data);
break;
case 'position':
await handlePositionUpdate(sessionId, data);
break;
case 'chat':
await handleChat(sessionId, data);
break;
default:
console.log('Unknown message type:', data.type);
}
} catch (error) {
console.error('Error handling message:', error);
}
});
ws.on('close', () => {
sessions.delete(sessionId);
broadcastUserLeft(sessionId);
console.log(`Client disconnected: ${sessionId}`);
});
});
function generateSessionId() {
return 'session_' + Math.random().toString(36).substr(2, 9);
}
async function handleJoin(ws, data) {
const userId = data.userId || 'user_' + Math.random().toString(36).substr(2, 9);
await redisClient.hSet('users', userId, JSON.stringify({
userId,
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 },
joinedAt: Date.now()
}));
ws.send(JSON.stringify({
type: 'joined',
userId,
sessionId: generateSessionId()
}));
broadcastUserList();
}
async function handlePositionUpdate(sessionId, data) {
const userId = data.userId;
const position = data.position;
const rotation = data.rotation;
await redisClient.hSet('users', userId, JSON.stringify({
userId,
position,
rotation
}));
broadcast({
type: 'positionUpdate',
userId,
position,
rotation
}, sessionId);
}
async function handleChat(sessionId, data) {
const message = {
type: 'chat',
userId: data.userId,
text: data.text,
timestamp: Date.now()
};
broadcast(message);
}
function broadcast(message, excludeSessionId = null) {
const data = JSON.stringify(message);
sessions.forEach((ws, sessionId) => {
if (sessionId !== excludeSessionId && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
});
}
function broadcastUserLeft(userId) {
broadcast({
type: 'userLeft',
userId
});
}
async function broadcastUserList() {
const users = await redisClient.hGetAll('users');
const userList = Object.values(users).map(JSON.parse);
broadcast({
type: 'userList',
users: userList
});
}
console.log('WebSocket server running on port 8080');
Development Tools
VS Code Remote Containers Setup:
Create .devcontainer/devcontainer.json:
{
"name": "XR Development Environment",
"image": "mcr.microsoft.com/devcontainers/javascript-node:18",
"features": {
"ghcr.io/devcontainers/features/node:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"formulahendry.auto-rename-tag",
"eamodio.gitlens",
"ms-vscode.vscode-typescript-next"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"editor.formatOnSave": true
}
}
},
"mounts": [
"source=${localWorkspaceFolder},target=/workspace,type=bind",
"source=vscode-extensions,target=/root/.vscode-server/extensions,type=volume"
],
"postCreateCommand": "npm install"
}
Performance Monitoring Dashboard (Grafana + Prometheus):
Create directory ~/xr-dev-env/monitoring/:
docker-compose.yml:
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: xr-prometheus
restart: unless-stopped
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
labels:
- "traefik.enable=true"
- "traefik.http.routers.prometheus.rule=Host(`prometheus.your-domain.com`)"
- "traefik.http.routers.prometheus.tls=true"
- "traefik.http.routers.prometheus.tls.certresolver=letsencrypt"
- "traefik.http.routers.prometheus.middlewares=auth"
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
networks:
- proxy
grafana:
image: grafana/grafana:latest
container_name: xr-grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`grafana.your-domain.com`)"
- "traefik.http.routers.grafana.tls=true"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.routers.grafana.middlewares=auth"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
depends_on:
- prometheus
networks:
- proxy
volumes:
prometheus-data:
grafana-data:
networks:
proxy:
external: true
prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8080']
- job_name: 'docker'
static_configs:
- targets: ['172.17.0.1:9323']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
Project Templates
Basic WebXR Project
Complete Project Structure:
~/xr-dev-env/projects/basic-webxr/
├── docker-compose.yml
├── package.json
├── index.html
└── src/
├── main.js
├── xr.js
└── utils.js
docker-compose.yml:
version: '3.8'
services:
basic-webxr:
image: node:18-alpine
container_name: basic-webxr
restart: unless-stopped
working_dir: /app
command: sh -c "npm install && npm run dev"
volumes:
- .:/app
- /app/node_modules
labels:
- "traefik.enable=true"
- "traefik.http.routers.basic-webxr.rule=Host(`basic-webxr.your-domain.com`)"
- "traefik.http.routers.basic-webxr.tls=true"
- "traefik.http.routers.basic-webxr.tls.certresolver=letsencrypt"
- "traefik.http.routers.basic-webxr.middlewares=websocket-headers,cors"
- "traefik.http.services.basic-webxr.loadbalancer.server.port=5173"
networks:
- proxy
networks:
proxy:
external: true
Performance Optimization
GPU Passthrough Configuration
Configure Docker Compose for GPU Access:
services:
xr-app:
image: your-xr-app
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu, utility, compute]
Network Latency Optimization
Optimize Docker Network:
Create custom network with MTU optimization:
docker network create \
--driver bridge \
--opt com.docker.network.driver.mtu=1450 \
--opt "com.docker.network.bridge.enable_icc=true" \
--opt "com.docker.network.bridge.enable_ip_masquerade=true" \
xr-network
Traefik Performance Tuning:
Add to traefik.yml:
global:
checknewversion: false
sendanonymoususage: false
serversTransport:
insecureSkipVerify: true
maxIdleConnsPerHost: 100
forwardingTimeouts:
dialTimeout: 30s
responseHeaderTimeout: 10s
Container Resource Limits
services:
xr-app:
deploy:
resources:
limits:
cpus: '4'
memory: 4G
reservations:
cpus: '2'
memory: 2G
Security Considerations
Traefik Middlewares for XR Endpoints
Security Headers:
http:
middlewares:
security-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
sslRedirect: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"
Authentication for Development:
middlewares:
dev-auth:
basicAuth:
users:
- "admin:$$apr1$$hashhere"
realm: "XR Development Environment"
CrowdSec Security Integration
Install CrowdSec Agent:
curl -s https://install.crowdsec.net | sudo sh
sudo cscli collection install crowdsecurity/traefik
sudo cscli hub update
sudo systemctl restart crowdsec
Configure CrowdSec Remediation:
middlewares:
crowdsec-bouncer:
plugin:
pluginName: ""
pluginConf:
crowdsec:
enabled: true
machineID: ""
crowdsecLapiKey: ""
updateFrequencySeconds: 10
Deployment Workflow
CI/CD Pipeline Setup
.gitlab-ci.yml:
stages:
- test
- build
- deploy
test:
stage: test
image: node:18
script:
- npm install
- npm run test
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
deploy:
stage: deploy
image: docker:24
services:
- docker:24-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker-compose pull
- docker-compose up -d
only:
- main
Automated Testing with XR Emulators
Test XR Functionality:
// tests/xr-test.js
const { expect } = require('chai');
describe('WebXR Functionality', () => {
it('should support VR sessions', async () => {
const xr = navigator.xr;
const isSupported = await xr.isSessionSupported('immersive-vr');
expect(isSupported).to.be.true;
});
it('should initialize Three.js scene', () => {
const scene = new THREE.Scene();
expect(scene).to.be.instanceOf(THREE.Scene);
});
});
Troubleshooting
Common GPU Container Issues
Issue: GPU not detected in container
Solution:
# Verify NVIDIA driver
nvidia-smi
# Check NVIDIA runtime
docker info | grep nvidia
# Test GPU access
docker run --rm --gpus all nvidia/cuda:11.0.3-base-ubuntu20.04 nvidia-smi
Issue: Out of GPU memory
Solution:
- Reduce model complexity
- Use texture compression
- Implement LOD (Level of Detail)
- Optimize shader code
WebXR Browser Compatibility
Browser Support Matrix:
| Browser | VR | AR | Notes |
|---|---|---|---|
| Chrome 84+ | ✅ | ✅ (Android) | Full WebXR support |
| Firefox 88+ | ✅ | ❌ | VR only |
| Edge 84+ | ✅ | ✅ (Android) | Chromium-based |
| Safari 15+ | ✅ | ❌ | Limited support |
WebSocket Connection Problems
Debug WebSocket Connections:
const ws = new WebSocket('wss://ws.your-domain.com');
ws.onopen = () => {
console.log('WebSocket connected');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
};
Appendix
Sample Docker Compose Files
Complete XR Development Stack:
version: '3.8'
services:
# Traefik reverse proxy
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
# ... (see above for full config)
# Three.js development server
threejs-dev:
image: node:18-alpine
container_name: threejs-dev-server
restart: unless-stopped
# ... (see above for full config)
# WebSocket backend
websocket-server:
image: node:18-alpine
container_name: webxr-websocket
restart: unless-stopped
# ... (see above for full config)
# Redis for session management
redis:
image: redis:7-alpine
container_name: webxr-redis
restart: unless-stopped
# ... (see above for full config)
# Monitoring
prometheus:
image: prom/prometheus:latest
container_name: xr-prometheus
restart: unless-stopped
# ... (see above for full config)
grafana:
image: grafana/grafana:latest
container_name: xr-grafana
restart: unless-stopped
# ... (see above for full config)
Complete Traefik Configuration
See above sections for complete traefik.yml and middleware configurations.
Troubleshooting Checklist
- Verify Docker daemon is running
- Check NVIDIA driver installation
- Confirm GPU runtime is available
- Verify Traefik is accessible
- Check SSL certificates are valid
- Test WebSocket connections
- Monitor container logs
- Verify network connectivity
- Check resource usage
- Review CrowdSec security logs
Conclusion
This comprehensive XR development environment provides everything you need to build modern WebXR applications with complete control over your infrastructure. By leveraging Docker for containerization and Traefik for reverse proxying, you get:
- Scalable architecture that grows with your projects
- Professional tooling for team collaboration
- Security features to protect your development environment
- Performance optimizations for XR workloads
- Monitoring and observability for production-ready deployments
The self-hosted approach ensures privacy, control, and the flexibility to customize every component to your specific needs. Whether you're building VR experiences, AR applications, or mixed reality solutions, this infrastructure provides a solid foundation for professional XR development.
Next Steps
- Start with the basic project template and gradually add complexity
- Implement CI/CD pipelines for automated testing and deployment
- Add monitoring dashboards to track performance and resource usage
- Experiment with GPU acceleration for compute-intensive XR applications
- Scale up as your projects and team grow
Happy XR development! 🚀