The Obsidian Container That Wouldnât Connect
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, expose port 8080, route through Cloudflare tunnel. Should be straightforward.
The Setup
The container was running. XPRA was active:
kubectl exec -it obsidian-xpra-pod -- ps aux | grep xpra
obsidian 7 /usr/bin/python3 /usr/bin/xpra start --bind-tcp=0.0.0.0:8080 --html=on --start-child=obsidian --no-sandbox --exit-with-children --daemon=no
The Kubernetes service mapped port 5800 externally to 8080 internally. The Cloudflare tunnel pointed obsidian.argobox.com to the service.
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 CMD:
CMD xpra start --bind-tcp=0.0.0.0:8080 --html=on --html-timeout=0 --start=obsidian --no-sandbox --exit-with-children --daemon=no --auth=none --no-pulseaudio --notifications=no
Key changes:
--html-timeout=0â prevents XPRA from timing out the HTML connection--start=obsidianinstead of--start-child=obsidian --no-sandboxâ cleaner process spawning--auth=noneâ removes authentication (Cloudflare handles access control)--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 added WebSocket handling to the tunnel config:
- hostname: obsidian.argobox.com
service: http://10.43.60.48:5800
originRequest:
noTLSVerify: true
The noTLSVerify: true is safe here because weâre connecting to an internal service. TLS terminates at Cloudflare; internal traffic doesnât need it again.
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.