<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yahav tzukerman</title>
    <description>The latest articles on DEV Community by yahav tzukerman (@yahavtz).</description>
    <link>https://dev.to/yahavtz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3864553%2F5bcd0258-9106-4615-a39a-cbe4c353739c.jpg</url>
      <title>DEV Community: yahav tzukerman</title>
      <link>https://dev.to/yahavtz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yahavtz"/>
    <language>en</language>
    <item>
      <title>Running a nested Proxmox homelab and Docker development on the same Windows machine</title>
      <dc:creator>yahav tzukerman</dc:creator>
      <pubDate>Sat, 12 Sep 2026 21:35:44 +0000</pubDate>
      <link>https://dev.to/yahavtz/running-a-nested-proxmox-homelab-and-docker-development-on-the-same-windows-machine-44c8</link>
      <guid>https://dev.to/yahavtz/running-a-nested-proxmox-homelab-and-docker-development-on-the-same-windows-machine-44c8</guid>
      <description>&lt;p&gt;&lt;em&gt;Tags: docker, proxmox, homelab, windows&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I sat down to start a new project and Docker Desktop wouldn't run. I knew why immediately, because I'd already paid this bill once — in the opposite direction.&lt;/p&gt;

&lt;p&gt;When I first set up Proxmox nested in VMware, VMware wouldn't run either. I fixed it by turning the Windows hypervisor off so the nested VM could reach VT-x/EPT directly. It worked. What I didn't think about at the time was the other half: Docker Desktop needs that same hypervisor. So I had a working homelab and no development environment, and I'd done it to myself months earlier without noticing.&lt;/p&gt;

&lt;p&gt;The two are mutually exclusive on one machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hypervisor on:&lt;/strong&gt; Docker Desktop works, nested Proxmox fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypervisor off:&lt;/strong&gt; Proxmox works, Docker Desktop has no engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The switch itself is one line in an elevated prompt, and it needs a reboot either way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# nested Proxmox works, Docker Desktop and WSL2 do not&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;bcdedit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hypervisorlaunchtype&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# back to the other side&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;bcdedit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hypervisorlaunchtype&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't just want them to stop fighting, either. I wanted the new development setup to be able to reach PVE, not merely coexist with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;p&gt;I went to ChatGPT first. The answers weren't good enough — they were variations on "pick one", and I wanted both.&lt;/p&gt;

&lt;p&gt;The idea that actually solved it was mine: build a separate VM whose only job is to be the Docker engine for development. Then PVE keeps direct hardware access, development gets a real Docker daemon, and neither one is Windows' problem any more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DEV01&lt;/strong&gt; — Ubuntu 24.04.5 in VMware, 4 vCPU, ~8 GB RAM, 100 GB disk. Docker Engine 29.8.0, Compose v5.5.1, containerd 2.3.5.&lt;/p&gt;

&lt;p&gt;The Windows Docker CLI talks to it through a context pointing at &lt;code&gt;ssh://dev&lt;/code&gt;. Docker Desktop stays installed for the CLI only, and never runs.&lt;/p&gt;

&lt;p&gt;That's the whole trick. The daemon isn't on Windows, so it doesn't care what Windows did to its hypervisor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Passwordless SSH first
&lt;/h3&gt;

&lt;p&gt;The Docker context runs every command over SSH, so key auth has to work before anything else does. From Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# create a key if you don't have one&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;ssh-keygen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ed25519&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# copy the public key to the VM (this is the last time it asks for a password)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;scp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\.ssh\id_ed25519.pub"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;@&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;dev01-ip&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;/tmp/master.pub&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then on the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;touch&lt;/span&gt; ~/.ssh/authorized_keys
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qxFf&lt;/span&gt; /tmp/master.pub ~/.ssh/authorized_keys &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/master.pub &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/authorized_keys
&lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/master.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Then an alias, so the IP appears exactly once
&lt;/h3&gt;

&lt;p&gt;Add this to &lt;code&gt;%USERPROFILE%\.ssh\config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; dev
    &lt;span class="k"&gt;HostName&lt;/span&gt; &amp;lt;dev01-ip&amp;gt;
    &lt;span class="k"&gt;User&lt;/span&gt; &amp;lt;user&amp;gt;
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; C:/Users/&amp;lt;you&amp;gt;/.ssh/id_ed25519
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="k"&gt;ServerAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ssh dev&lt;/code&gt; should now drop you straight into a shell with no password and no IP. &lt;code&gt;ServerAliveInterval&lt;/code&gt; matters more than it looks: without it, a long build over a quiet connection can drop halfway.&lt;/p&gt;

&lt;h3&gt;
  
  
  And finally the context
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"host=ssh://dev"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEV01 Docker Engine"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;docker info&lt;/code&gt; prints the VM's engine version, you're done. Everything below is what that costs you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The side effect nobody mentions
&lt;/h2&gt;

&lt;p&gt;WSL2 stops existing too.&lt;/p&gt;

&lt;p&gt;The annoying part is that it doesn't &lt;em&gt;say&lt;/em&gt; so. &lt;code&gt;wsl --status&lt;/code&gt; still cheerfully reports &lt;code&gt;Default Distribution: Ubuntu, Default Version: 2&lt;/code&gt;, so everything looks registered and fine — but nothing can actually run. Anything you were hosting in WSL silently loses its home.&lt;/p&gt;

&lt;p&gt;For the real state, ask Windows whether a hypervisor is present at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_ComputerSystem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HypervisorPresent&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What breaks when the daemon isn't local
&lt;/h2&gt;

&lt;p&gt;A remote daemon behaves like a local one until it doesn't. Three things bit me.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Bind mounts resolve on the daemon host
&lt;/h3&gt;

&lt;p&gt;Not where you typed the command. &lt;code&gt;./data:/data&lt;/code&gt; quietly means "a folder on the VM".&lt;/p&gt;

&lt;p&gt;Worse: &lt;code&gt;-v /tmp/script.py:/x&lt;/code&gt; made Docker &lt;strong&gt;create a directory&lt;/strong&gt; named &lt;code&gt;/tmp/script.py&lt;/code&gt; on the VM and mount that, so the container failed with &lt;code&gt;can't find __main__ module&lt;/code&gt;. Nothing warns you. The path you meant is on Windows; the path Docker used is on the VM, and Docker invented it for you.&lt;/p&gt;

&lt;p&gt;Piping the file over stdin avoids the whole class of problem, because nothing has to exist on the daemon host at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# instead of -v script.py:/x&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;script.py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--rm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# same idea for a shell script&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;setup.sh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--rm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ubuntu:24.04&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;127.0.0.1&lt;/code&gt; in a port mapping is the VM's loopback now
&lt;/h3&gt;

&lt;p&gt;Published ports are invisible from Windows until you forward them. Everything starts, the health check inside the VM passes, and your browser on the workstation sees nothing.&lt;/p&gt;

&lt;p&gt;An SSH tunnel is the smallest fix, and it keeps both ends on loopback so nothing is exposed to the LAN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-N&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;yes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;127.0.0.1:5236:127.0.0.1:5236&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;127.0.0.1:8090:127.0.0.1:8090&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ExitOnForwardFailure=yes&lt;/code&gt; is the part worth copying. Without it, a port that's already taken fails silently and you get a tunnel that's half open — which looks exactly like the app being broken.&lt;/p&gt;

&lt;p&gt;I wrapped that in the same script that starts the stack, so the tunnel opens and closes with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The build context goes over SSH on every build
&lt;/h3&gt;

&lt;p&gt;Without a &lt;code&gt;.dockerignore&lt;/code&gt;, the local virtualenv is uploaded each time you build. With one, my context transfer was &lt;strong&gt;51.64 kB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the one item on the list that's a straight upgrade once you fix it: the feedback is immediate and visible in the build output.&lt;/p&gt;

&lt;h2&gt;
  
  
  HGFS: why the share looked empty
&lt;/h2&gt;

&lt;p&gt;I share the Windows folder into the VM with a VMware shared folder and bind it into the containers.&lt;/p&gt;

&lt;p&gt;First attempt: the share mounted fine as my login user, and was completely invisible to the containers. The bind failed, or the path just looked empty.&lt;/p&gt;

&lt;p&gt;The reason isn't a VMware quirk — it's FUSE's default. &lt;strong&gt;A FUSE filesystem mounted by an ordinary user is closed to every other uid, including root.&lt;/strong&gt; dockerd is root. Containers run as root. So they see nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;allow_other&lt;/code&gt; lifts it, with a catch: a non-root user can only pass &lt;code&gt;allow_other&lt;/code&gt; if &lt;code&gt;user_allow_other&lt;/code&gt; is uncommented in &lt;code&gt;/etc/fuse.conf&lt;/code&gt;, and it's commented out by default on Ubuntu 24.04. Mounting as root from &lt;code&gt;/etc/fstab&lt;/code&gt; sidesteps that and survives a reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;host&lt;/span&gt;:/&amp;lt;&lt;span class="n"&gt;share&lt;/span&gt;-&lt;span class="n"&gt;name&lt;/span&gt;&amp;gt; /&lt;span class="n"&gt;mnt&lt;/span&gt;/&amp;lt;&lt;span class="n"&gt;share&lt;/span&gt;-&lt;span class="n"&gt;name&lt;/span&gt;&amp;gt; &lt;span class="n"&gt;fuse&lt;/span&gt;.&lt;span class="n"&gt;vmhgfs&lt;/span&gt;-&lt;span class="n"&gt;fuse&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt;,&lt;span class="n"&gt;allow_other&lt;/span&gt;,&lt;span class="n"&gt;uid&lt;/span&gt;=&lt;span class="m"&gt;1000&lt;/span&gt;,&lt;span class="n"&gt;gid&lt;/span&gt;=&lt;span class="m"&gt;1000&lt;/span&gt;,&lt;span class="n"&gt;nofail&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nofail&lt;/code&gt; so a missing share never blocks boot.&lt;/p&gt;

&lt;p&gt;I recognised the symptom fast, but "looks empty" is a miserable error message to debug cold.&lt;/p&gt;

&lt;h3&gt;
  
  
  One guard worth having
&lt;/h3&gt;

&lt;p&gt;When the share isn't mounted, the mount point is just an empty directory. Containers happily write into the VM's local disk, everything looks healthy, and the files go nowhere.&lt;/p&gt;

&lt;p&gt;So don't check that the directory exists — check that a file you know is in the share is visible through the mount, before starting anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh dev &lt;span class="s1"&gt;'test -f /mnt/&amp;lt;share-name&amp;gt;/README.md'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"share is not mounted"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty directory and a working share are indistinguishable at a glance, and the failure mode is silent data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six things I proved before trusting it
&lt;/h2&gt;

&lt;p&gt;All of these ran as root inside a container with the share bound in, then were checked from Windows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Write a file, read it from Windows&lt;/td&gt;
&lt;td&gt;Works, owner is the Windows user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Replace a file by rename&lt;/td&gt;
&lt;td&gt;Works, both in Python and .NET, no temp files left&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Set a file's mtime&lt;/td&gt;
&lt;td&gt;Works, exact to the tick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Delete a file&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Write a 10 MB image&lt;/td&gt;
&lt;td&gt;Works, write + fsync in 0.014 s, SHA-256 identical both sides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;See a Windows-created file from the container&lt;/td&gt;
&lt;td&gt;No measurable delay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Measuring #6 honestly needs the clock offset first, and the way to get it is the same trick NTP uses: take a timestamp on Windows, ask the VM for its time, take a second timestamp on Windows, and compare the VM's answer against the midpoint of the two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$t1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'date -u --iso-8601=ns'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$t3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;FromUnixTimeMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUnixTimeMilliseconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$t3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUnixTimeMilliseconds&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalSeconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it three times and take the spread as your error bar. Mine came out at &lt;strong&gt;+0.39 s ± 0.1 s&lt;/strong&gt; — the VM runs ahead of the workstation — so any latency claim smaller than that is noise, not a measurement. It's worth doing this before you quote yourself a number you'll later repeat.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one real limitation
&lt;/h3&gt;

&lt;p&gt;Appending to a file that &lt;strong&gt;already existed&lt;/strong&gt; took ≈1.4 s to become visible in the container — the FUSE attribute cache. Creating, renaming and deleting are immediate.&lt;/p&gt;

&lt;p&gt;So: fine if every writer creates a new file or renames one into place. A problem if anything watches files, or re-reads one mid-edit.&lt;/p&gt;

&lt;p&gt;Also worth knowing: metadata over HGFS is synthetic. Files always report uid/gid 1000 and mode 0777, and &lt;code&gt;chown&lt;/code&gt;/&lt;code&gt;chmod&lt;/code&gt; succeed while changing nothing. If your container logic asserts on permissions, it's asserting on a fiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Desktop takes the context back
&lt;/h2&gt;

&lt;p&gt;Mid-session the active context flipped from my SSH context back to &lt;code&gt;desktop-linux&lt;/code&gt; on its own. Every command after that failed — Desktop had grabbed the context and then couldn't start, because the hypervisor is off.&lt;/p&gt;

&lt;p&gt;The fix is to stop relying on the active context at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;-c &amp;lt;context&amp;gt;&lt;/code&gt; in every script, or set &lt;code&gt;DOCKER_CONTEXT&lt;/code&gt; for the session. If the context is an implicit global, something else will eventually change it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this permanent?
&lt;/h2&gt;

&lt;p&gt;No. DEV01 gets deleted when PVE moves to dedicated hardware — but that's months away.&lt;/p&gt;

&lt;p&gt;PVE still has to prove itself first. The plan is to buy smart-home hardware, let PVE run that, and see whether it earns a server of its own. Until then, this is a good enough answer for months of development, and "good enough for months" is a real category.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone hitting this
&lt;/h2&gt;

&lt;p&gt;Be creative. If Windows can't solve a problem, an extra VM is a legitimate answer, not a defeat.&lt;/p&gt;

&lt;p&gt;The framing I was handed everywhere — including by the chatbot — was &lt;em&gt;choose one&lt;/em&gt;. The constraint was real; the conclusion wasn't. Moving the daemon out of the argument entirely cost me one VM, one &lt;code&gt;.dockerignore&lt;/code&gt;, one fstab line, and an afternoon of proving the shared folder does what I think it does.&lt;/p&gt;




&lt;p&gt;I'm Yahav Tzukerman, a full-stack developer (Angular + .NET). I build things I need and write about what broke along the way — most of it lives in my homelab.&lt;/p&gt;

&lt;p&gt;I also build automation for small businesses: Telegram bots, document workflows, and AI agents that handle the repetitive parts. If you've got a process that's eating your week, I'm happy to talk about it.&lt;/p&gt;

&lt;p&gt;Find me on &lt;a href="https://dev.to/yahavtz"&gt;Dev.to&lt;/a&gt; — or drop a comment below, I answer all of them.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>infrastructure</category>
    </item>
  </channel>
</rss>
