Obsidian Container Tunnel Integration
Date: 2025-04-03 Duration: About 30 minutes Issue: Obsidian container accessible internally, 502 externally Root Cause: XPRA configuration + WebSocket handling through Cloudflare tunnel
The Goal
Run Obsidian in a container. Access it from anywhere via browser. No local installation needed.
XPRA handles this: it streams X11 applications over HTML5. Wrap Obsidian in XPRA, route it through the protected browser path, and keep local installation optional.
The Setup
The container was running. XPRA was active, and the Kubernetes service was reachable through an internal test path.
Internal test via port-forward: worked fine.
External access via Cloudflare: 502 Bad Gateway.
The Clue
The process list showed something unexpected:
obsidian 54 xmessage -center -file -
xmessage is X11's way of displaying error dialogs. Something was showing an error message instead of actually running Obsidian. But the XPRA session was alive, and internally it worked.
The issue wasn't Obsidian itself. It was how XPRA handled connections coming through the Cloudflare tunnel.
The Problem
XPRA's HTML5 interface uses WebSockets. Cloudflare tunnels support WebSockets, but they need specific handling. The default XPRA configuration wasn't playing nice with the tunnel's connection proxying.
Also, the --start-child flag was subtly wrong. XPRA 3.x uses --start for some configurations. The distinction matters for how the child process inherits the display.
The Fix
Updated the Dockerfile command to use the right XPRA startup mode for the containerized browser session.
Key changes:
- HTML timeout disabled for long-running browser sessions
--start=obsidianinstead of--start-child=obsidian --no-sandbox— cleaner process spawning- Authentication remains handled by the protected access layer in front of the tunnel
--no-pulseaudio— audio isn't needed and reduces complexity--notifications=no— disables desktop notifications that can cause issues in containers
Rebuilt and redeployed:
docker build -t obsidian-xpra:latest .
docker save obsidian-xpra:latest | sudo ctr -n k8s.io images import -
kubectl rollout restart deployment obsidian-xpra
The Cloudflare Side
Also adjusted the tunnel configuration so WebSocket traffic reached the internal service through the protected route without publishing raw internal addresses or ports.
Restarted the tunnel:
kubectl rollout restart deployment cloudflared
The Test
kubectl port-forward svc/obsidian-xpra-service 5800:5800
curl http://localhost:5800
Got HTML back. Internal: working.
Visited https://obsidian.argobox.com.
Got the XPRA HTML5 client. Obsidian rendered in the browser. Full application, accessible from anywhere.
Why Internal Worked But External Didn't
Internal port-forwarding goes through kubectl, which handles the raw TCP connection directly. No proxying, no WebSocket negotiation, no tunnel.
External access goes through:
- Cloudflare edge
- Cloudflare tunnel
cloudflareddaemon- Kubernetes service
- Pod
Each hop can interpret or modify the WebSocket connection. XPRA's default settings weren't tolerant enough of these intermediaries. The --html-timeout=0 and explicit flags made it more resilient.
The Result
Obsidian in a browser. Vault synced via the container's volume mount. Accessible from any device with a browser and Cloudflare access.
Is it practical? Maybe not for everyday use — there's latency. But for accessing notes from a phone or a machine where I can't install Obsidian? Works fine.
What I Learned
XPRA flags matter. The difference between --start and --start-child isn't just syntax. The process hierarchy affects how displays are attached.
WebSocket apps need tunnel awareness. Anything that relies on WebSockets through a reverse proxy or tunnel needs explicit timeout and connection handling.
Disable what you don't need. Audio, notifications, authentication — each one is a potential failure point in a containerized environment. Strip it down to essentials.
Test through the full path. Internal port-forward testing doesn't catch proxy issues. Always test the actual production path.
Obsidian in a container, streamed over the internet. Because sometimes "just install the app" isn't an option.