The problem: working from anywhere without paying for cloud instances #
I have a standard gaming laptop at home with an NVIDIA GPU. It is neither an enterprise server nor a powerhouse, but for my personal projects, it gets the job done. The issue is purely physical: it’s heavy, sits on my desk, and I can’t easily take it with me on the go.
The concept takes inspiration from 1970s mainframe computing: a central server doing the heavy lifting, with lightweight “dumb” terminals connecting into it. This allows me to carry a much lighter laptop around, or even use my parents’ PC whenever I travel back to Sicily.
Earlier this year, I read an article by Lucia Subatin explaining how she worked from anywhere using a cloud workstation accessible via VNC. The idea is great, but renting cloud instances with a dedicated GPU gets expensive quickly—especially for hobby work or intermittent personal side projects.
Hence the decision: replicate that exact convenience on my home machine. Two immediate advantages: zero recurring monthly costs, and all my code, datasets, and configs remain on my own disks, without being stored on third-party servers.
Why encapsulate everything in Docker #
I initially considered enabling standard remote desktop on Linux, but that left me tightly coupled to the physical monitor and its lock screen. I could have looked into third-party solutions like TeamViewer or RustDesk, but I wanted complete independence. More importantly, I didn’t actually need my personal desktop—I needed a dedicated development environment: a clean workspace accessible from anywhere.
Containerizing the entire workstation inside Docker solves two key problems:
- No clutter on the host: I can test experimental SDKs, language runtimes, extensions, and AI tools without polluting my main OS.
- Reproducible and persistent setup: the user home directory (
/home/deskharbor) and Homebrew live on dedicated Docker volumes, while repositories are mounted straight from the host filesystem. If something breaks, I can tear down the container and rebuild it cleanly without losing configurations, SSH keys, or project files.
I began assembling the Dockerfile step-by-step with Gemini’s assistance (which was what I had access to back then), naming the project Desk Harbor (now open source on GitHub).
Overcoming XFCE limitations: GNOME Flashback and NVIDIA GPU passthrough #
I ran the initial setup for a couple of months, but soon needed GPU access for another project. The container had to tap into my graphics card.
I discovered rawritude’s repository, which provided great inspiration for NVIDIA passthrough and running Antigravity inside a container. However, that setup relied on XFCE, and I ran into recurring friction: screen resolution quirks, unreliable dynamic viewport resizing, and intermittent graphical session instability.
I decided to refactor the project from the ground up with the help of Codex:
- Desktop Environment: switched to GNOME Flashback alongside TigerVNC, noVNC, and the Tilix terminal (configured with dark theme and Fira Code). Resolution now dynamically matches the viewport of the browser tab or native VNC client (Remmina).
- GPU Passthrough & CUDA: using NVIDIA Container Toolkit, the container accesses host drivers for CUDA compute workloads (essential for local model inference and agentic coding workflows), while X11 desktop display rendering is handled entirely in software (
LIBGL_ALWAYS_SOFTWARE=1) to avoid display server crashes and driver conflicts. - Management TUI: built an interactive menu-driven CLI (
./deskharbor) in Python/curses to manage configuration, diagnostics, lifecycle control, and modular tool installation (Codex, Antigravity, Sublime Text, Chrome, pyenv, Node).
Two access paths: Browser (VNC) and Smartphone (SSH) #
Depending on where I am and which device I have on hand, I use two primary channels:
- From laptop / tablet (Web VNC - Port 6080 or native VNC 5901): via the noVNC bridge, the desktop environment is accessible in any modern web browser. When I prefer a dedicated client, I connect through Remmina on port 5901. There is also an optional low-latency audio stream on port 6081 (via virtual PulseAudio and ffmpeg).
- From smartphone (SSH - Port 20222): when I’m out and just need to run a command, monitor a build, or tweak a configuration file, I SSH in from my phone using public key authentication (via terminal apps like Termux with tmux).
All container services bind to localhost: nothing is directly exposed to the public Internet, requiring either the VPN or a secure SSH tunnel. I also added an option to expose select services automatically from my laptop, eliminating the need for manual port forwarding.
Overall Architecture with Desk Harbor #
Here is how all the pieces connect together: remote access, homelab control, and the containerized workstation:
flowchart TD
laptop["💻 Travel Laptop
(Browser noVNC / VNC)"]
phone["📱 Smartphone
(Termux SSH / Telegram)"]
subgraph Homelab["🏠 Homelab"]
direction LR
bot["🤖 Telegram Bot
(WoL Management)"]
wg["🔒 WireGuard VPN
(Secure Tunnel)"]
end
subgraph Host["⚡ Gaming Laptop (Home)"]
direction LR
wol["🔌 NIC (WoL)"]
desk["📦 DeskHarbor
• Web Desktop :6080 / VNC :5901
• OpenSSH :20222 (tmux)
• AI: Codex & Antigravity"]
gpu["🎮 NVIDIA GPU (CUDA)"]
end
phone -.->|"1. /wake (Telegram)"| bot
bot -.->|"Magic Packet WoL"| wol
wol -.->|"Wake from Suspend"| desk
laptop ==>|"2. VPN Connection"| wg
phone ==>|"2. VPN Connection"| wg
wg ==>|"LAN Access"| desk
gpu -.->|"AI Compute"| desk
Remote connectivity: WireGuard on Proxmox #
Opening ports on a residential router pointing directly at a remote desktop machine is a terrible security idea. External access is routed entirely through the homelab VPN:
- A small home Proxmox node hosts a WireGuard VPN server.
- A custom Telegram bot allows me to toggle and monitor VPN access.
- Once connected, the Desk Harbor container is reachable directly on its private LAN IP (
192.168.x.x), exactly as if I were sitting on my home couch. - To avoid leaving the workstation running and drawing power 24/7 when idle, I put it into suspend mode and wake it on demand via Wake-on-LAN (using a command sent to the Telegram bot).
What works well and what doesn’t #
After several months of daily driving this setup, the overall experience has been very rewarding, with a few honest trade-offs:
- VNC Latency: on solid Wi-Fi or fast 5G, the web session is surprisingly smooth. If connection quality degrades, the graphical interface feels sluggish; in those situations, falling back to SSH and tmux is the only sensible way to stay productive.
- Browser vs. native VNC client: everything runs reliably, but it’s not identical to sitting in front of a native desktop. Browser-based VNC can also introduce slight clipboard friction (because the host doesn’t use SSL certificates, noVNC relies on its side drawer panel for clipboard exchange). In practice, I often develop inside the container even when working directly at the physical machine. You also have to be mindful of keyboard shortcuts so you don’t accidentally close or switch browser tabs.
This is my pragmatic approach to a self-hosted remote dev environment—it may not fit every possible use case, but it suits my workflow and delivers exactly what I need. You can inspect the full Dockerfile, TUI, and setup instructions in the DeskHarbor repository on GitHub. If you have suggestions or feedback to refine the setup, feel free to drop a comment below!