<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>nixos-vm-template on blog.rymcg.tech</title>
    <link>https://blog.rymcg.tech/tags/nixos-vm-template/</link>
    <description>Recent content in nixos-vm-template on blog.rymcg.tech</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>Copyright © 2020-2026, EnigmaCurry</copyright><atom:link href="https://blog.rymcg.tech/tags/nixos-vm-template/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>NixOS VMs part 4: Managing VMs with home-manager and sway-home</title>
      <link>https://blog.rymcg.tech/blog/linux/nixos-vm-home-manager/</link>
      <pubDate>Thu, 29 Jan 2026 00:00:01 -0600</pubDate>
      
      <guid>https://blog.rymcg.tech/blog/linux/nixos-vm-home-manager/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part 4 of a series on &lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/code-agent-vm/&#34;&gt;Running code agents in an immutable NixOS VM&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/nixos-proxmox-vm/&#34;&gt;Bootstrapping a Docker server with immutable NixOS on Proxmox&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/mutable-vms/&#34;&gt;Mutable VMs are cool too&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Managing VMs with home-manager and sway-home (this post)&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;The previous posts covered creating and managing VMs with
&lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;. But
how do you actually integrate it into your daily workflow on your
workstation? This post covers using
&lt;a href=&#34;https://github.com/nix-community/home-manager&#34;&gt;home-manager&lt;/a&gt; to set up
the VM tooling, create shell aliases with tab completion, and manage
updates through the Nix ecosystem.&lt;/p&gt;
&lt;h2 id=&#34;what-is-home-manager&#34;&gt;What is home-manager?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/nix-community/home-manager&#34;&gt;Home Manager&lt;/a&gt; is a tool
for managing user environments using the Nix package manager. Instead of
manually installing programs and editing dotfiles, you declare what you
want in a Nix configuration file, and home-manager builds and activates
that environment.&lt;/p&gt;
&lt;p&gt;The key benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Declarative&lt;/strong&gt;: Your entire user environment is defined in code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducible&lt;/strong&gt;: The same configuration produces the same result on
any machine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback&lt;/strong&gt;: Every configuration change creates a new &amp;ldquo;generation&amp;rdquo;
you can switch back to&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated&lt;/strong&gt;: Packages are installed in the Nix store, not globally&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Home-manager works on any Linux distribution - you don&amp;rsquo;t need NixOS. You
just need the Nix package manager installed.&lt;/p&gt;
&lt;h2 id=&#34;what-is-sway-home&#34;&gt;What is sway-home?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/EnigmaCurry/sway-home&#34;&gt;sway-home&lt;/a&gt; is a home-manager
configuration that sets up a complete development environment. It
includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shell configuration (bash, with aliases and completions)&lt;/li&gt;
&lt;li&gt;Editor setup (Emacs)&lt;/li&gt;
&lt;li&gt;Development tools (git, ripgrep, just, etc.)&lt;/li&gt;
&lt;li&gt;Window manager config (Sway, if you&amp;rsquo;re on Wayland)&lt;/li&gt;
&lt;li&gt;And relevantly: nixos-vm-template integration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you activate sway-home with home-manager, you get all of this
configured and ready to use. The configuration is modular - you can
enable or disable components as needed.&lt;/p&gt;
&lt;h2 id=&#34;the-hm--commands&#34;&gt;The hm-* commands&lt;/h2&gt;
&lt;p&gt;Sway-home provides shell aliases for common home-manager operations. These
are the commands you&amp;rsquo;ll use to manage your environment:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Command&lt;/th&gt;
          &lt;th&gt;What it does&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-switch&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Rebuild and activate your home-manager configuration&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-update&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Update flake.lock to fetch latest versions of all inputs&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-upgrade&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Update + switch in one step&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-generations&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;List your configuration history (each activation is a &amp;ldquo;generation&amp;rdquo;)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-rollback&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Revert to the previous generation if something breaks&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-metadata&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Show the git revisions of all flake inputs&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hm-pull&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Git pull the sway-home repository&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The typical workflow: edit your configuration, run &lt;code&gt;hm-switch&lt;/code&gt; to apply
it. If you want the latest upstream changes from sway-home and its
dependencies (including nixos-vm-template), run &lt;code&gt;hm-upgrade&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-nixos-vm-template-module&#34;&gt;The nixos-vm-template module&lt;/h2&gt;
&lt;p&gt;One of sway-home&amp;rsquo;s modules, &lt;code&gt;nixos-vm-template.nix&lt;/code&gt;, integrates the VM
tooling directly into your shell. It does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Symlinks the nixos-vm-template repository into &lt;code&gt;~/nixos-vm-template&lt;/code&gt;
(bound to the nix store, so it&amp;rsquo;s read-only but versioned)&lt;/li&gt;
&lt;li&gt;Creates a default environment file at
&lt;code&gt;~/.config/nixos-vm-template/env&lt;/code&gt; with XDG-compliant paths&lt;/li&gt;
&lt;li&gt;Sets up the &lt;code&gt;vm&lt;/code&gt; shell alias with tab completion&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After activating home-manager, you get a &lt;code&gt;vm&lt;/code&gt; command that works like
this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm list                    &lt;span style=&#34;color:#75715e&#34;&gt;# List all VMs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm create                  &lt;span style=&#34;color:#75715e&#34;&gt;# Create a VM (interactive wizard)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm ssh myvm                &lt;span style=&#34;color:#75715e&#34;&gt;# SSH in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm upgrade myvm            &lt;span style=&#34;color:#75715e&#34;&gt;# Upgrade to new image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;vm create&lt;/code&gt; command runs an interactive configuration wizard that
prompts for the VM name, profiles, memory, CPUs, disk size, and
network mode. After configuration, it builds the image, creates the
VM, and starts it automatically.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;vm&lt;/code&gt; command operates on the local machine&amp;rsquo;s libvirt backend.&lt;/p&gt;
&lt;p&gt;The alias is defined using the &lt;code&gt;_justfile_alias&lt;/code&gt; function, which wraps
&lt;code&gt;just&lt;/code&gt; with a specific Justfile and environment file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_justfile_alias vm &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/nixos-vm-template/Justfile&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/.config/nixos-vm-template/env&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This gives you full tab completion for recipes and machine names.
Tab completion works for commands like &lt;code&gt;vm ssh&lt;/code&gt;, &lt;code&gt;vm status&lt;/code&gt;, and
&lt;code&gt;vm upgrade&lt;/code&gt; that take a VM name as an argument.&lt;/p&gt;
&lt;h3 id=&#34;the-nix-store-binding&#34;&gt;The nix store binding&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s an important detail: &lt;code&gt;~/nixos-vm-template&lt;/code&gt; is a symlink into the
nix store, not a regular git clone. The module declares:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;home&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;file&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nixos-vm-template&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;source &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; inputs&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;nixos-vm-template;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This means the repository is read-only. You can&amp;rsquo;t edit files there
directly. The benefit is reproducibility - your VM tooling version is
pinned to a specific commit in the sway-home flake.lock.&lt;/p&gt;
&lt;p&gt;The downside is that changes to nixos-vm-template upstream won&amp;rsquo;t appear
until you update and rebuild. More on that below.&lt;/p&gt;
&lt;h3 id=&#34;where-things-live&#34;&gt;Where things live&lt;/h3&gt;
&lt;p&gt;The environment file sets up XDG-compliant paths:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Path&lt;/th&gt;
          &lt;th&gt;Purpose&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;~/.config/nixos-vm-template/machines/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Machine configs (identity files, profiles)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;~/.config/nixos-vm-template/libvirt/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Libvirt XML templates&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;~/.config/nixos-vm-template/env&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Backend configuration&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;~/.local/share/nixos-vm-template/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Built images and VM disks&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Your machine configs are in &lt;code&gt;~/.config&lt;/code&gt;, so they&amp;rsquo;re easy to back up and
track. The large image files go in &lt;code&gt;~/.local/share&lt;/code&gt; where they won&amp;rsquo;t
clutter your config backups.&lt;/p&gt;
&lt;h2 id=&#34;updating-nixos-vm-template&#34;&gt;Updating nixos-vm-template&lt;/h2&gt;
&lt;p&gt;Since &lt;code&gt;~/nixos-vm-template&lt;/code&gt; is bound to the nix store, you need to update
home-manager to get new versions:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hm-update    &lt;span style=&#34;color:#75715e&#34;&gt;# Update flake.lock (fetches latest nixos-vm-template)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hm-switch    &lt;span style=&#34;color:#75715e&#34;&gt;# Rebuild and activate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After this, &lt;code&gt;~/nixos-vm-template&lt;/code&gt; points to the new version and all &lt;code&gt;vm&lt;/code&gt;
commands use the updated code.&lt;/p&gt;
&lt;p&gt;To see what version you&amp;rsquo;re running:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hm-metadata
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This shows the git revisions of all flake inputs, including
nixos-vm-template.&lt;/p&gt;
&lt;h2 id=&#34;home-manager-inside-the-vm&#34;&gt;Home-manager inside the VM&lt;/h2&gt;
&lt;p&gt;So far we&amp;rsquo;ve talked about home-manager on your &lt;strong&gt;host workstation&lt;/strong&gt; -
managing your shell, your tools, and the nixos-vm-template integration.
But you can also run home-manager &lt;strong&gt;inside the VM&lt;/strong&gt; to manage the user
environment there.&lt;/p&gt;
&lt;p&gt;To get home-manager inside a VM, include the &lt;code&gt;home-manager&lt;/code&gt; profile when
creating it. Run &lt;code&gt;vm create&lt;/code&gt; and select the &lt;code&gt;home-manager&lt;/code&gt; profile
along with your other desired profiles (e.g., &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;home-manager&lt;/code&gt; profile installs sway-home&amp;rsquo;s configuration inside the
VM, giving you the same shell setup, editor config, and tools. This is
useful for development VMs where you want a consistent environment.&lt;/p&gt;
&lt;p&gt;The profile works in both immutable and mutable VMs, but with slightly
different behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In &lt;strong&gt;immutable VMs&lt;/strong&gt;, a custom activation service creates symlinks from
&lt;code&gt;/home/&amp;lt;user&amp;gt;&lt;/code&gt; to the nix store. This handles the complexity of the
read-only root and bind-mounted home directories.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In &lt;strong&gt;mutable VMs&lt;/strong&gt;, standard home-manager activation runs. The
filesystem is writable, so home-manager just does its normal thing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that the &lt;code&gt;dev&lt;/code&gt; profile includes basic development tools (neovim,
tmux, etc.) but does &lt;strong&gt;not&lt;/strong&gt; include home-manager. If you want the full
sway-home experience inside the VM, add the &lt;code&gt;home-manager&lt;/code&gt; profile
explicitly.&lt;/p&gt;
&lt;h2 id=&#34;creating-a-proxmox-alias&#34;&gt;Creating a Proxmox alias&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;vm&lt;/code&gt; alias points at libvirt by default. If you also have a Proxmox
server, you can create a separate alias for it.&lt;/p&gt;
&lt;p&gt;First, create an environment file for your Proxmox backend:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat &amp;gt; ~/.config/nixos-vm-template/env-pve &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;BACKEND=proxmox
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_HOST=pve
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_NODE=pve
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_STORAGE=local-zfs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_DISK_FORMAT=raw
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_BRIDGE=vmbr0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;OUTPUT_DIR=$HOME/.local/share/nixos-vm-template
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;MACHINES_DIR=$HOME/.config/nixos-vm-template/machines-pve
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;LIBVIRT_DIR=$HOME/.config/nixos-vm-template/libvirt
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note the separate &lt;code&gt;MACHINES_DIR&lt;/code&gt; - this keeps Proxmox machine configs
separate from libvirt ones, avoiding confusion.&lt;/p&gt;
&lt;p&gt;Then add the alias to your shell config (e.g., &lt;code&gt;~/.bashrc&lt;/code&gt; or the
sway-home &lt;code&gt;config/bash/alias.sh&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_justfile_alias pve &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/nixos-vm-template/Justfile&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/.config/nixos-vm-template/env-pve&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now you have two aliases:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm create       &lt;span style=&#34;color:#75715e&#34;&gt;# Creates on local libvirt (interactive)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pve create      &lt;span style=&#34;color:#75715e&#34;&gt;# Creates on Proxmox server (interactive)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both use the same nixos-vm-template codebase and run the same
interactive configuration wizard, but target different backends with
different configurations.&lt;/p&gt;
&lt;h2 id=&#34;disk-space-and-garbage-collection&#34;&gt;Disk space and garbage collection&lt;/h2&gt;
&lt;p&gt;VM images accumulate in &lt;code&gt;~/.local/share/nixos-vm-template/&lt;/code&gt;. Each profile
combination produces an image (typically 3-4 GB), and each VM has its own
&lt;code&gt;/var&lt;/code&gt; disk.&lt;/p&gt;
&lt;p&gt;To see disk usage:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;du -sh ~/.local/share/nixos-vm-template/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To clean up old images (keeping currently-used ones):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm clean
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the nix store itself, home-manager profiles accumulate over time. To
reclaim space:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nix-collect-garbage --delete-older-than 30d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This removes store paths that aren&amp;rsquo;t referenced by any generation newer
than 30 days. Be careful - this includes home-manager generations, so
you&amp;rsquo;ll lose the ability to roll back to older configs.&lt;/p&gt;
&lt;p&gt;To see how much space you&amp;rsquo;d reclaim:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nix-store --gc --print-dead
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-full-workflow&#34;&gt;The full workflow&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what a typical session looks like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Update everything&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hm-pull &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; hm-upgrade
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Check what version of nixos-vm-template you&amp;#39;re running&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hm-metadata | grep nixos-vm-template
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Create a VM for a new project (interactive wizard)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Enter: project-foo, claude/dev/docker/home-manager, 8192, 4, etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# The VM starts automatically after creation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Work on the project...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm ssh user@project-foo
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ... do things ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Later, upgrade the VM to pick up profile changes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm upgrade project-foo
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Deploy to production on Proxmox (interactive wizard)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pve create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Enter: project-foo, docker, etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# The VM starts automatically&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Clean up old images&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vm clean
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nix-collect-garbage --delete-older-than 7d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The sway-home integration turns nixos-vm-template into a first-class shell
tool. The &lt;code&gt;vm&lt;/code&gt; alias gives you quick access to all VM operations with tab
completion, and the home-manager binding ensures reproducible tooling
versions across machines. Adding backend-specific aliases like &lt;code&gt;pve&lt;/code&gt; lets
you manage VMs across multiple hypervisors from a single workflow.&lt;/p&gt;
&lt;p&gt;The key things to remember:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/nixos-vm-template&lt;/code&gt; is read-only; run &lt;code&gt;hm-upgrade&lt;/code&gt; to get updates&lt;/li&gt;
&lt;li&gt;Machine configs live in &lt;code&gt;~/.config/nixos-vm-template/machines/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;nix-collect-garbage&lt;/code&gt; periodically to reclaim disk space&lt;/li&gt;
&lt;li&gt;Create separate aliases and machine directories for each backend&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>NixOS VMs part 3: Mutable VMs are cool too</title>
      <link>https://blog.rymcg.tech/blog/linux/mutable-vms/</link>
      <pubDate>Thu, 29 Jan 2026 00:00:00 -0600</pubDate>
      
      <guid>https://blog.rymcg.tech/blog/linux/mutable-vms/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part 3 of a series on &lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/code-agent-vm/&#34;&gt;Running code agents in an immutable NixOS VM&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/nixos-proxmox-vm/&#34;&gt;Bootstrapping a Docker server with immutable NixOS on Proxmox&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Mutable VMs are cool too (this post)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/nixos-vm-home-manager/&#34;&gt;Managing VMs with home-manager and sway-home&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;
project has, so far, been only about immutable VMs: read-only root
filesystem, separate &lt;code&gt;/var&lt;/code&gt; disk for state, atomic upgrades from the
host. It&amp;rsquo;s a nice model. But sometimes you just want a normal NixOS
system that you can poke at from the inside.&lt;/p&gt;
&lt;p&gt;This post introduces mutable VM support - a single read-write disk with
the full nix toolchain available. Same profiles, same tooling, different
architecture.&lt;/p&gt;
&lt;h2 id=&#34;why-mutable&#34;&gt;Why mutable?&lt;/h2&gt;
&lt;p&gt;Immutable VMs are great for production-like workloads. The OS is
locked down, upgrades happen by replacing the boot image, and you can&amp;rsquo;t
accidentally &lt;code&gt;rm -rf /&lt;/code&gt; your way into trouble. But there are cases where
that rigidity gets in the way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NixOS experimentation&lt;/strong&gt;: You want to run &lt;code&gt;nixos-rebuild switch&lt;/code&gt;
inside the VM to test configuration changes interactively.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nix development&lt;/strong&gt;: You&amp;rsquo;re working on Nix expressions and need
&lt;code&gt;nix build&lt;/code&gt;, &lt;code&gt;nix develop&lt;/code&gt;, and friends to actually work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning NixOS&lt;/strong&gt;: You want a sandbox where you can break things and
fix them without rebuilding images on the host.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quick iterations&lt;/strong&gt;: Sometimes you just want to &lt;code&gt;nix profile install&lt;/code&gt;
something and see if it works.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Immutable VMs don&amp;rsquo;t support any of this. The nix toolchain requires a
writable &lt;code&gt;/nix/store&lt;/code&gt; with a valid database, and that&amp;rsquo;s fundamentally
incompatible with a read-only root. The old workaround was a &lt;code&gt;nix&lt;/code&gt;
profile that used overlayfs to make &lt;code&gt;/nix&lt;/code&gt; writable, but it was fragile
and didn&amp;rsquo;t survive reboots cleanly.&lt;/p&gt;
&lt;p&gt;Mutable VMs solve this properly: one disk, fully writable, standard
NixOS.&lt;/p&gt;
&lt;h2 id=&#34;the-tradeoffs&#34;&gt;The tradeoffs&lt;/h2&gt;
&lt;p&gt;Nothing is free. Here&amp;rsquo;s what you give up:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Feature&lt;/th&gt;
          &lt;th&gt;Immutable&lt;/th&gt;
          &lt;th&gt;Mutable&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Root filesystem&lt;/td&gt;
          &lt;td&gt;Read-only&lt;/td&gt;
          &lt;td&gt;Read-write&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Disk layout&lt;/td&gt;
          &lt;td&gt;Boot + var (two disks)&lt;/td&gt;
          &lt;td&gt;Single disk&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Thin provisioning&lt;/td&gt;
          &lt;td&gt;Yes (QCOW2 backing files)&lt;/td&gt;
          &lt;td&gt;No (full copy)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Host upgrades&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;just upgrade&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Not supported&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Corruption resistance&lt;/td&gt;
          &lt;td&gt;High (root can&amp;rsquo;t be modified)&lt;/td&gt;
          &lt;td&gt;Normal&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Nix commands&lt;/td&gt;
          &lt;td&gt;Limited&lt;/td&gt;
          &lt;td&gt;Full toolchain&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The big one is thin provisioning. With immutable VMs, multiple VMs
sharing the same profile share a single base image - each VM&amp;rsquo;s boot disk
is just a delta. With mutable VMs, each one gets a full copy of the
image. If your base image is 4GB, every mutable VM is at least 4GB on
disk.&lt;/p&gt;
&lt;p&gt;The other big one is upgrades. You can&amp;rsquo;t run &lt;code&gt;just upgrade&lt;/code&gt; on a mutable
VM. The whole point is that the system is managed from inside, so you
SSH in and run &lt;code&gt;nixos-rebuild switch&lt;/code&gt; like you would on any NixOS
machine.&lt;/p&gt;
&lt;h2 id=&#34;creating-a-mutable-vm&#34;&gt;Creating a mutable VM&lt;/h2&gt;
&lt;p&gt;Run the interactive create command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just create
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When prompted, enter the VM name and select your desired profiles
(e.g., &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;dev&lt;/code&gt;). The wizard will also ask whether you want a
mutable or immutable VM - select mutable mode when prompted.&lt;/p&gt;
&lt;p&gt;Alternatively, you can enable mutable mode on an existing machine
config:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just mutable myvm      &lt;span style=&#34;color:#75715e&#34;&gt;# prompts to enable/disable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just recreate myvm     &lt;span style=&#34;color:#75715e&#34;&gt;# applies the change&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;just mutable&lt;/code&gt; command shows you the current status, explains what
mutable mode does, and asks for confirmation. If you enable it, the next
&lt;code&gt;just create&lt;/code&gt; or &lt;code&gt;just recreate&lt;/code&gt; will build a mutable image.&lt;/p&gt;
&lt;p&gt;This works on both backends - just set &lt;code&gt;BACKEND=proxmox&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt;
file or environment before running &lt;code&gt;just create&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;whats-different-inside&#34;&gt;What&amp;rsquo;s different inside&lt;/h2&gt;
&lt;p&gt;A mutable VM looks like a normal NixOS system. The root filesystem is
ext4 on a single disk, &lt;code&gt;/nix&lt;/code&gt; is writable, and you can run any nix
command you want. But the VM still gets its identity from the machine
config files - hostname, SSH keys, firewall ports - just stored in
different locations.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;File&lt;/th&gt;
          &lt;th&gt;Immutable location&lt;/th&gt;
          &lt;th&gt;Mutable location&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Hostname&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/var/identity/hostname&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/etc/hostname&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Machine ID&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/var/identity/machine-id&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/etc/machine-id&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;SSH authorized keys&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/var/identity/*_authorized_keys&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/etc/ssh/authorized_keys.d/*&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Firewall ports&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/var/identity/tcp_ports&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/etc/firewall-ports/tcp_ports&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Root password hash&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/var/identity/root_password_hash&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;/etc/root_password_hash&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;On boot, systemd services read these files and apply them. The
&lt;code&gt;firewall-ports.service&lt;/code&gt; opens the configured ports; the
&lt;code&gt;root-password.service&lt;/code&gt; sets the root password from the hash file. Same
end result, different plumbing.&lt;/p&gt;
&lt;h2 id=&#34;upgrading-a-mutable-vm&#34;&gt;Upgrading a mutable VM&lt;/h2&gt;
&lt;p&gt;Since the system is writable, you upgrade it the normal NixOS way:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just ssh admin@myvm
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Inside the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo nixos-rebuild switch --upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or if you have a flake:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo nixos-rebuild switch --flake github:you/your-config#myvm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can also use &lt;code&gt;nix-env&lt;/code&gt; for user-level package management, though
that&amp;rsquo;s generally not recommended on NixOS.&lt;/p&gt;
&lt;p&gt;If you try &lt;code&gt;just upgrade&lt;/code&gt; on a mutable VM, it will error out and tell
you to upgrade from inside instead. This is intentional - the host
doesn&amp;rsquo;t own the system configuration anymore, the VM does.&lt;/p&gt;
&lt;h2 id=&#34;firewall-and-passwords&#34;&gt;Firewall and passwords&lt;/h2&gt;
&lt;p&gt;The machine config files (&lt;code&gt;machines/&amp;lt;name&amp;gt;/tcp_ports&lt;/code&gt;, &lt;code&gt;udp_ports&lt;/code&gt;,
&lt;code&gt;root_password_hash&lt;/code&gt;) are copied into the image at creation time. If you
want to change them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Edit the files in &lt;code&gt;machines/&amp;lt;name&amp;gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;just recreate &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Or just edit them directly inside the VM:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Add a new allowed port in the running VM:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;8080&amp;#34;&lt;/span&gt; | sudo tee -a /etc/firewall-ports/tcp_ports
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl restart firewall-ports
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Change root password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo passwd root
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Changes made inside the VM persist across reboots but won&amp;rsquo;t be reflected
in the machine config on the host. If you recreate the VM, you&amp;rsquo;ll get
whatever&amp;rsquo;s in the host-side config.&lt;/p&gt;
&lt;h2 id=&#34;when-to-use-which&#34;&gt;When to use which&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Use immutable VMs when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Running services that don&amp;rsquo;t need system-level changes&lt;/li&gt;
&lt;li&gt;You want atomic, host-driven upgrades&lt;/li&gt;
&lt;li&gt;Multiple VMs share the same base image&lt;/li&gt;
&lt;li&gt;You value the corruption resistance of a read-only root&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Use mutable VMs when:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Experimenting with NixOS configuration&lt;/li&gt;
&lt;li&gt;Developing Nix expressions&lt;/li&gt;
&lt;li&gt;Learning NixOS interactively&lt;/li&gt;
&lt;li&gt;You need &lt;code&gt;nix build&lt;/code&gt;, &lt;code&gt;nix develop&lt;/code&gt;, or &lt;code&gt;nixos-rebuild&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both types use the same profiles, the same machine config structure, and
the same &lt;code&gt;just&lt;/code&gt; commands for everything except upgrades. You can have
some VMs immutable and others mutable in the same setup.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Mutable VMs fill a gap that immutable VMs couldn&amp;rsquo;t: interactive NixOS
development and experimentation. You lose thin provisioning and
host-driven upgrades, but you gain a fully functional nix toolchain and
the ability to iterate on system configuration without rebuilding images.&lt;/p&gt;
&lt;p&gt;For production-like workloads, immutable is still the way to go. For
learning, development, and experimentation, mutable VMs are cool too.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>NixOS VMs part 2: Bootstrapping a Docker server with immutable NixOS on Proxmox</title>
      <link>https://blog.rymcg.tech/blog/linux/nixos-proxmox-vm/</link>
      <pubDate>Fri, 23 Jan 2026 23:00:00 -0600</pubDate>
      
      <guid>https://blog.rymcg.tech/blog/linux/nixos-proxmox-vm/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part 2 of a series on &lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/code-agent-vm/&#34;&gt;Running code agents in an immutable NixOS VM&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Bootstrapping a Docker server with immutable NixOS on Proxmox (this post)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/mutable-vms/&#34;&gt;Mutable VMs are cool too&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/nixos-vm-home-manager/&#34;&gt;Managing VMs with home-manager and sway-home&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;In the &lt;a href=&#34;http://blog.rymcg.tech/blog/linux/code-agent-vm/&#34;&gt;last
post&lt;/a&gt; I
described running AI code agents inside immutable NixOS VMs using
libvirt on a laptop. That setup works well for local development, but
sometimes you want to deploy VMs on actual infrastructure - a Proxmox
server sitting in a closet, a rack, or someone else&amp;rsquo;s datacenter.&lt;/p&gt;
&lt;p&gt;The
&lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;
project now supports multiple backends. You build the same NixOS
images locally on your workstation, you run the same commands as you
would with libvirt, and you get the same immutable VMs - the only
difference is where they land. This post walks through using the
Proxmox backend to bootstrap a Docker server VM.&lt;/p&gt;
&lt;h2 id=&#34;why-proxmox&#34;&gt;Why Proxmox?&lt;/h2&gt;
&lt;p&gt;Libvirt is fine for laptops and &amp;lsquo;sworkstations&amp;rsquo;. You run &lt;code&gt;just create&lt;/code&gt;, the VM shows up locally, and you SSH into it. But if you want
VMs running 24/7 on dedicated hardware, you probably have a hypervisor
already, and in the homelab world that hypervisor is usually Proxmox.&lt;/p&gt;
&lt;p&gt;The new Proxmox backend builds images locally with Nix, then ships
them to your PVE node over SSH. No Proxmox API tokens, no web UI
clicking, no cloud-init templates. Just NixOS config, SSH, and &lt;code&gt;qm&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-backend-abstraction&#34;&gt;The backend abstraction&lt;/h2&gt;
&lt;p&gt;The tool now has a &lt;code&gt;BACKEND&lt;/code&gt; variable. Set it to &lt;code&gt;libvirt&lt;/code&gt; or
&lt;code&gt;proxmox&lt;/code&gt; and the same Justfile recipes work against either target:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Local libvirt (the default)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Remote Proxmox&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;BACKEND&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;proxmox just create
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;just create&lt;/code&gt; command runs an interactive configuration wizard
that prompts for the VM name, profiles, memory, CPUs, disk size, and
network mode. After configuration, it builds the image, creates the
VM, and starts it automatically.&lt;/p&gt;
&lt;p&gt;You can also put &lt;code&gt;BACKEND=proxmox&lt;/code&gt; in a &lt;code&gt;.env&lt;/code&gt; file and forget about
it. The commands are identical - &lt;code&gt;just stop&lt;/code&gt;, &lt;code&gt;just upgrade&lt;/code&gt;, &lt;code&gt;just ssh&lt;/code&gt; - they just talk to different backends.&lt;/p&gt;
&lt;h2 id=&#34;set-up-the-proxmox-connection&#34;&gt;Set up the Proxmox connection&lt;/h2&gt;
&lt;p&gt;All you need is SSH access to your PVE node. Create a &lt;code&gt;.env&lt;/code&gt; file in
the project root:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;BACKEND&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;proxmox
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PVE_HOST&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;pve
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PVE_NODE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;pve
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PVE_STORAGE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;local-zfs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PVE_BRIDGE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;vmbr0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PVE_DISK_FORMAT&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;raw
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PVE_BACKUP_STORAGE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;pbs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few notes on these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BACKEND&lt;/code&gt; must be set to proxmox, otherwise libvirt is the default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PVE_HOST&lt;/code&gt; must be the name of the SSH config entry in your &lt;code&gt;~/.ssh/config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PVE_NODE&lt;/code&gt; must match your Proxmox node&amp;rsquo;s actual hostname. If your
node is called &lt;code&gt;pve&lt;/code&gt; in the web UI, put &lt;code&gt;pve&lt;/code&gt; here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PVE_STORAGE&lt;/code&gt; is which storage system the VM disks get stored on
(i.e., &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;local-zfs&lt;/code&gt;, &lt;code&gt;my-nfs&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PVE_BRIDGE&lt;/code&gt; is the default network bridge. You can override this
per-VM.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PVE_DISK_FORMAT&lt;/code&gt; use &lt;code&gt;raw&lt;/code&gt; format for ZFS or LVM-thin, &lt;code&gt;qcow2&lt;/code&gt; for
directory/NFS storage.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PVE_BACKUP_STORAGE&lt;/code&gt; is optional, for vzdump backups. Point it at a
PBS instance if you have one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure you can SSH to the PVE node. Use a key without a password,
or make sure that your SSH agent is loaded so you don&amp;rsquo;t need to type
the password:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh -i ~/.ssh/id_ed25519 root@192.168.1.100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that works, you&amp;rsquo;re good.&lt;/p&gt;
&lt;h2 id=&#34;build-and-create-the-docker-vm&#34;&gt;Build and create the Docker VM&lt;/h2&gt;
&lt;p&gt;The project ships with composable mixin profiles. The &lt;code&gt;docker&lt;/code&gt; profile
includes the Docker daemon and adds users to the &lt;code&gt;docker&lt;/code&gt; group. For a
dedicated Docker server, you don&amp;rsquo;t need the development tools or the
AI agents that we used in the last post. The &lt;code&gt;docker&lt;/code&gt; profile gives
you a minimal system with SSH, Docker, and not much else (the &lt;code&gt;core&lt;/code&gt;
profile is always implicitly included).&lt;/p&gt;
&lt;p&gt;Run the interactive create command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just create
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The wizard prompts you for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VM name&lt;/strong&gt;: e.g., &lt;code&gt;apps01&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Profiles&lt;/strong&gt;: select &lt;code&gt;docker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: e.g., &lt;code&gt;4096&lt;/code&gt; (4GB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPUs&lt;/strong&gt;: e.g., &lt;code&gt;2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Var disk size&lt;/strong&gt;: e.g., &lt;code&gt;50G&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network mode&lt;/strong&gt;: select &lt;code&gt;bridge&lt;/code&gt; and enter &lt;code&gt;vmbr0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After configuration, &lt;code&gt;just create&lt;/code&gt; builds the NixOS image with a
read-only root filesystem and Docker enabled, transfers it to your PVE
node via rsync, imports it as a Proxmox VM, and starts it
automatically.&lt;/p&gt;
&lt;h2 id=&#34;first-boot&#34;&gt;First boot&lt;/h2&gt;
&lt;p&gt;The VM starts automatically after &lt;code&gt;just create&lt;/code&gt; completes. Check its
status:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just status apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The status command queries the QEMU guest agent for the VM&amp;rsquo;s IP
address. Once it&amp;rsquo;s up:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just ssh admin@apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;re now SSH&amp;rsquo;d into a minimal NixOS system with Docker running.
Verify it works:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run --rm hello-world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;docker-data-persistence&#34;&gt;Docker data persistence&lt;/h2&gt;
&lt;p&gt;The immutable root design means Docker&amp;rsquo;s data directory
(&lt;code&gt;/var/lib/docker&lt;/code&gt;) lives on the read-write &lt;code&gt;/var&lt;/code&gt; disk. This is where
images, containers, volumes, and networks are stored. When you upgrade
the base image, all of this survives.&lt;/p&gt;
&lt;p&gt;If you want to run containers with persistent data, use standard
Docker volumes, or you may use bind mounts from another directory
under &lt;code&gt;/var&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;firewall-configuration&#34;&gt;Firewall configuration&lt;/h2&gt;
&lt;p&gt;By default, ports 22, 80, and 443 are open. The firewall rules are
stored per-VM in &lt;code&gt;machines/&amp;lt;name&amp;gt;/tcp_ports&lt;/code&gt; (one port per line). To
open additional ports:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;8080&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; machines/apps01/tcp_ports
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just upgrade apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The upgrade syncs the new port list to the VM&amp;rsquo;s identity files and
rebuilds the boot image, and automatically reboots.&lt;/p&gt;
&lt;p&gt;Important note: The firewall rules are applied inside the VM &lt;em&gt;and&lt;/em&gt; on
the Proxmox host. This is a defense-in-depth approach. However, this
also means that you should never manually touch the firewall config of
the VM on the Proxmox console. All firewall changes must happen in the
&lt;code&gt;machines/&amp;lt;name&amp;gt;/tcp_ports&lt;/code&gt; and &lt;code&gt;machines/&amp;lt;name&amp;gt;/udp_ports&lt;/code&gt; files, and
subsequently run &lt;code&gt;just upgrade&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;network-options&#34;&gt;Network options&lt;/h2&gt;
&lt;p&gt;Proxmox VMs can use any bridge configured on your PVE node. The
network mode is stored per-VM and can be changed after creation:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Move to a different bridge&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just network-config apps01 bridge:vmbr1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just upgrade apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For NAT (if your PVE node has a NAT bridge configured):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just network-config apps01 nat
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just upgrade apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;snapshots-before-risky-changes&#34;&gt;Snapshots before risky changes&lt;/h2&gt;
&lt;p&gt;About to &lt;code&gt;docker system prune -a&lt;/code&gt; and hoping you don&amp;rsquo;t regret it?
Snapshot first:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just snapshot apps01 before-prune
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If things go wrong:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just stop apps01
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just restore-snapshot apps01 before-prune
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just start apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;backups&#34;&gt;Backups&lt;/h2&gt;
&lt;p&gt;For proper backups (not just snapshots), the tool wraps Proxmox&amp;rsquo;s
vzdump:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just backup apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates a compressed backup on your &lt;code&gt;PVE_BACKUP_STORAGE&lt;/code&gt;. To
restore:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just restore-backup apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you have a Proxmox Backup Server, point &lt;code&gt;PVE_BACKUP_STORAGE&lt;/code&gt; at it
and you get incremental, deduplicated backups for free.&lt;/p&gt;
&lt;h2 id=&#34;upgrades&#34;&gt;Upgrades&lt;/h2&gt;
&lt;p&gt;The immutable design makes upgrades straightforward. Say you want to
add
&lt;a href=&#34;https://github.com/jesseduffield/lazydocker&#34;&gt;lazydocker&lt;/a&gt;
(A TUI manager for Docker). Edit the profile:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# profiles/docker.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{ config&lt;span style=&#34;color:#f92672&#34;&gt;,&lt;/span&gt; pkgs&lt;span style=&#34;color:#f92672&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt; }:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  config &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    virtualisation&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;docker&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;enable &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    users&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;users&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;core&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;adminUser&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;extraGroups &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [ &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;docker&amp;#34;&lt;/span&gt; ];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    users&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;users&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;core&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;regularUser&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;extraGroups &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [ &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;docker&amp;#34;&lt;/span&gt; ];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    environment&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;systemPackages &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; pkgs; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      lazydocker
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then upgrade the VM:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just upgrade apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This rebuilds the image, syncs identity files, replaces the boot disk
on Proxmox, and restarts the VM. Your &lt;code&gt;/var&lt;/code&gt; disk - including all
Docker data, volumes, and home directories - is untouched. The VM
comes back up with the app &lt;code&gt;lazydocker&lt;/code&gt; available and all your
containers intact.&lt;/p&gt;
&lt;h2 id=&#34;cloning&#34;&gt;Cloning&lt;/h2&gt;
&lt;p&gt;Need another Docker server with the same setup? Clone it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just clone apps01 apps02
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The clone command prompts for the new VM name and optionally lets you
adjust hardware settings (memory, CPUs, network) while keeping the
same profile and disk contents.&lt;/p&gt;
&lt;p&gt;This makes a full clone of the VM on Proxmox, generates fresh identity
files (new hostname, machine-id, MAC address, SSH host keys), and
syncs them onto the cloned &lt;code&gt;/var&lt;/code&gt; disk. You get an independent VM
that&amp;rsquo;s otherwise identical to the original.&lt;/p&gt;
&lt;h2 id=&#34;serial-console&#34;&gt;Serial console&lt;/h2&gt;
&lt;p&gt;If networking is broken and SSH won&amp;rsquo;t connect, you can attach to the
VM&amp;rsquo;s serial console directly:&lt;/p&gt;
&lt;p&gt;First you&amp;rsquo;ll need to set the root password (root login is disabled
otherwise):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just passwd apps01
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just upgrade apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Reboot, and you&amp;rsquo;ll be able to login via the serial console:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just console apps01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This opens an SSH session to the Proxmox node and attaches to the VM&amp;rsquo;s
serial port. You may need to press &lt;code&gt;Enter&lt;/code&gt; to see the login prompt.
Exit with &lt;code&gt;Ctrl-O&lt;/code&gt; (that&amp;rsquo;s the letter O, not zero).&lt;/p&gt;
&lt;h2 id=&#34;proxmox-uses-full-clones-of-the-disk-image&#34;&gt;Proxmox uses full clones of the disk image&lt;/h2&gt;
&lt;p&gt;Unlike the libvirt backend, which has thin provisioning of the boot
device, the Proxmox backend sends a full clone of the boot device for
each VM you create on Proxmox. This increases the disk space required
per VM by about 3 to 4 GB. You can run as many Docker servers as you
want from a single &lt;code&gt;just build docker&lt;/code&gt;, but, on Proxmox, each &lt;code&gt;just create&lt;/code&gt; produces an independent VM with its own boot disk, its own
identity, and its own &lt;code&gt;/var&lt;/code&gt; disk.&lt;/p&gt;
&lt;p&gt;If you really do want thin provisioning of Proxmox VMs, you could use
nixos-vm-template to create the first VM, then turn that VM into a
Proxmox template, and then use the Proxmox clone feature in the
console, but then those clones would not be managed by
nixos-vm-template.&lt;/p&gt;
&lt;h2 id=&#34;syncing-identity-files-into-var&#34;&gt;Syncing identity files into /var&lt;/h2&gt;
&lt;p&gt;The Proxmox backend&amp;rsquo;s identity sync is worth noting: during upgrades,
it mounts the &lt;code&gt;/var&lt;/code&gt; disk on the PVE node using &lt;code&gt;qemu-nbd&lt;/code&gt; and writes
identity files directly. No need to download a multi-gigabyte disk
just to update a hostname.&lt;/p&gt;
&lt;h2 id=&#34;putting-it-together&#34;&gt;Putting it together&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the full workflow for going from zero to a running Docker
server on Proxmox:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# One-time setup on your workstation:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clone https://github.com/EnigmaCurry/nixos-vm-template
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd nixos-vm-template
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat &amp;gt; .env &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;BACKEND=proxmox
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_HOST=pve
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_NODE=pve
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_STORAGE=local-zfs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_DISK_FORMAT=raw
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PVE_BRIDGE=vmbr0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Build and deploy (interactive wizard)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Enter: apps01, docker, 4096, 2, 50G, bridge, vmbr0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# The VM starts automatically, then SSH in:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just ssh admin@apps01
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# On the VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run -d --name nginx -p 80:80 nginx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now you have an immutable NixOS machine for your Docker server,
running on your own Proxmox infrastructure, built from a declarative
Nix configuration, with snapshots and backups available, upgradeable
without losing data, plus even if you don&amp;rsquo;t make backups, the whole
machine is reproducible from the Nix config in your git repository
(minus your data).&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>NixOS VMs part 1: Running code agents in an immutable NixOS VM</title>
      <link>https://blog.rymcg.tech/blog/linux/code-agent-vm/</link>
      <pubDate>Thu, 22 Jan 2026 13:54:00 -0600</pubDate>
      
      <guid>https://blog.rymcg.tech/blog/linux/code-agent-vm/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part 1 of a series on &lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Running code agents in an immutable NixOS VM (this post)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/nixos-proxmox-vm/&#34;&gt;Bootstrapping a Docker server with immutable NixOS on Proxmox&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/mutable-vms/&#34;&gt;Mutable VMs are cool too&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href=&#34;https://blog.rymcg.tech/blog/linux/nixos-vm-home-manager/&#34;&gt;Managing VMs with home-manager and sway-home&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;AI coding agents like &lt;a href=&#34;https://docs.anthropic.com/en/docs/claude-code&#34;&gt;Claude
Code&lt;/a&gt; and &lt;a href=&#34;https://github.com/sst/opencode&#34;&gt;Open
Code&lt;/a&gt; run in your terminal, read and
write files, execute commands, and generally do whatever you tell them
to. Claude Code is Anthropic&amp;rsquo;s official CLI agent; Open Code is an
open-source alternative that supports multiple model providers
(including Anthropic). Both are powerful, and both have full shell
access to whatever machine they&amp;rsquo;re running on — which is a bit
concerning if that machine is your daily driver laptop.&lt;/p&gt;
&lt;p&gt;This post walks through setting up a code agent inside an immutable
NixOS VM on a Fedora host, editing files remotely with Emacs TRAMP,
and using git branches so that the agent&amp;rsquo;s work is immediately
testable on other machines.&lt;/p&gt;
&lt;h2 id=&#34;why-bother-with-a-vm&#34;&gt;Why bother with a VM?&lt;/h2&gt;
&lt;p&gt;A code agent has shell access to whatever machine it&amp;rsquo;s running on. It
can modify files and run any program. If you point it at your laptop&amp;rsquo;s
home directory and say &amp;ldquo;refactor this project,&amp;rdquo; it will happily do so,
and if something goes wrong, it went wrong on &lt;em&gt;your&lt;/em&gt; machine.&lt;/p&gt;
&lt;p&gt;A VM gives the agent its own filesystem to work with. The
&lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template&#34;&gt;nixos-vm-template&lt;/a&gt;
project builds immutable NixOS images with a read-only root filesystem
and a separate data disk. If the environment gets weird, you can blow
it away and recreate it in two commands. Your laptop stays clean.&lt;/p&gt;
&lt;p&gt;The tradeoff is that you&amp;rsquo;re SSH&amp;rsquo;ing into a VM instead of running
locally. With TRAMP this is basically invisible to Emacs, so it hasn&amp;rsquo;t
bothered me.&lt;/p&gt;
&lt;h2 id=&#34;install-the-prerequisites&#34;&gt;Install the prerequisites&lt;/h2&gt;
&lt;p&gt;This example uses a Fedora laptop, but Debian, Arch, or pretty much
any other Linux distro with KVM support will work. The only software
dependencies you need are libvirt and the Nix package manager:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Fedora&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo dnf install nix just git libvirt qemu-kvm virt-manager guestfs-tools edk2-ovmf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl enable --now nix-daemon libvirtd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enable Nix flakes for this user:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mkdir -p ~/.config/nix
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;experimental-features = nix-command flakes&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; ~/.config/nix/nix.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;clone-and-build&#34;&gt;Clone and build&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clone https://github.com/EnigmaCurry/nixos-vm-template &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;   ~/nixos-vm-template
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd ~/nixos-vm-template
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The template ships with composable profiles that you combine as needed.
For a full development environment with a code agent, combine the
&lt;code&gt;claude&lt;/code&gt; (or &lt;code&gt;open-code&lt;/code&gt;) profile with &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;, and &lt;code&gt;podman&lt;/code&gt;.
The agent itself gets installed via npm on first login. Pick the one
you want and create a VM:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# For Claude Code with full dev environment:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# When prompted, enter the VM name (e.g., &amp;#34;claude-dev&amp;#34;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Select profiles: claude, dev, docker, podman&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Configure memory (8192), CPUs (4), and other settings&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;just create&lt;/code&gt; command runs an interactive configuration wizard
that guides you through all the options, then builds the image,
creates the VM, and starts it automatically. The root filesystem is
read-only (immutable), and all mutable state lives on a separate
&lt;code&gt;/var&lt;/code&gt; disk. Home directories are bind-mounted from &lt;code&gt;/var/home&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;first-boot&#34;&gt;First boot&lt;/h2&gt;
&lt;p&gt;The VM starts automatically after &lt;code&gt;just create&lt;/code&gt; completes. Check its
status and connect:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just status claude-dev      &lt;span style=&#34;color:#75715e&#34;&gt;# prints the IP address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just ssh claude-dev         &lt;span style=&#34;color:#75715e&#34;&gt;# SSH into the VM with the &amp;#39;user&amp;#39; account&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On first login, the shell profile detects that the agent isn&amp;rsquo;t
installed yet and runs the appropriate npm install automatically
(&lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt; or &lt;code&gt;opencode-ai&lt;/code&gt;). Both agents need an
API key for your service of choice. Here&amp;rsquo;s an example for Anthropic:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;export ANTHROPIC_API_KEY=&amp;#34;sk-ant-...&amp;#34;&amp;#39;&lt;/span&gt; &amp;gt;&amp;gt; ~/.bashrc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source ~/.bashrc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Open Code profile also creates a default config at
&lt;code&gt;~/.config/opencode/config.json&lt;/code&gt; pointing at Claude Opus 4.5. You can
edit that file to switch models or providers.&lt;/p&gt;
&lt;p&gt;Set your git user profile and preferences:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git config --global user.email &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;you@example.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git config --global user.name &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Your Name&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git config --global init.defaultBranch master
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;create-a-project-repository&#34;&gt;Create a project repository&lt;/h2&gt;
&lt;p&gt;Create a fresh repository on GitHub for the agent to work in. This
will be the project it has full control over, committing and pushing
on your behalf. Go to GitHub and create a new repository (public or
private, your choice), then come back here.&lt;/p&gt;
&lt;p&gt;Next, generate an SSH key on the VM so it can push to that repo:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh-keygen -t ed25519 -C &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;claude-dev-vm&amp;#34;&lt;/span&gt; -f ~/.ssh/id_ed25519 -N &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat ~/.ssh/id_ed25519.pub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Copy the public key and add it as a deploy key on the repository:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to your repository on GitHub.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Settings &amp;gt; Deploy keys &amp;gt; Add deploy key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Paste the public key, give it a name like &amp;ldquo;claude-dev VM&amp;rdquo;, and check
&lt;strong&gt;Allow write access&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add key&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Deploy keys are scoped to a single repository, which is a good fit
here. The VM only needs push access to the project it&amp;rsquo;s working on,
not your entire GitHub account. If the agent is working on multiple
repos, generate a separate key per repo.&lt;/p&gt;
&lt;p&gt;Verify the key works:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh -T git@github.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see a message confirming authentication. Now clone the repo
on the VM:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd ~
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clone git@github.com:you/your-project.git project
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd project
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run &lt;code&gt;claude&lt;/code&gt; or &lt;code&gt;opencode&lt;/code&gt; in the project directory.&lt;/p&gt;
&lt;h2 id=&#34;editing-files-with-tramp&#34;&gt;Editing files with TRAMP&lt;/h2&gt;
&lt;p&gt;TRAMP (Transparent Remote Access, Multiple Protocols) is Emacs&amp;rsquo;s
built-in facility for editing remote files over SSH. You open a file
with a special path syntax and Emacs handles the rest, no FUSE mounts
or sync tools involved.&lt;/p&gt;
&lt;p&gt;You can read more about how to configure emacs &lt;a href=&#34;https://emacs.rymcg.tech&#34;&gt;with my emacs
config&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;(If you don&amp;rsquo;t want to use Emacs, you can do something pretty similar
in VS Code with the &lt;a href=&#34;https://code.visualstudio.com/docs/remote/remote-overview&#34;&gt;Remote Development
Extension&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;First, add an SSH config entry so you don&amp;rsquo;t have to remember the IP:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# ~/.ssh/config
Host claude-dev
    HostName &amp;lt;ip&amp;gt;
    User user
    ControlMaster auto
    ControlPersist yes
    ControlPath /tmp/ssh-%u-%r@%h:%p
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(The ControlMaster settings are optional, they enable SSH connection
sharing so you won&amp;rsquo;t have to authenticate as often.)&lt;/p&gt;
&lt;p&gt;Now you can open files on the VM directly:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;C-x C-f /ssh:claude-dev:/home/user/project/src/main.rs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To get a shell running inside the VM, open a remote shell buffer:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;M-x shell RET
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When prompted for a directory, enter &lt;code&gt;/ssh:claude-dev:/home/user/&lt;/code&gt;.
(Or use &lt;code&gt;vterm&lt;/code&gt; if you prefer a proper terminal emulator in a buffer.)
Run your agent in that shell. Now you have it running in the VM, and
your Emacs buffers pointing at the same files it&amp;rsquo;s modifying. When the
agent writes to a file you have open. In Emacs enable &lt;code&gt;M-x auto-revert-mode&lt;/code&gt; and the fill will automatically detect changes and
reload the file.&lt;/p&gt;
&lt;h2 id=&#34;ssh-remote-forwarding&#34;&gt;SSH remote forwarding&lt;/h2&gt;
&lt;p&gt;Sometimes the agent needs access to a service running on your laptop.
For example, if you have a Traefik dashboard listening on
&lt;code&gt;127.0.0.1:8080&lt;/code&gt;, you can expose it to the VM with SSH remote
forwarding:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh -R 8080:127.0.0.1:8080 user@claude-dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This binds port 8080 on the VM to port 8080 on your laptop. Inside the
VM, the agent can now reach the Traefik dashboard at &lt;code&gt;127.0.0.1:8080&lt;/code&gt;
as if it were local. You can add this to your SSH config to make it
persistent:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# ~/.ssh/config
Host claude-dev
    ...
    RemoteForward 8080 127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is useful for giving the agent access to local dev servers, API
endpoints, or dashboards without exposing them to your network.
Furthermore, the &lt;code&gt;claude&lt;/code&gt; and &lt;code&gt;open-code&lt;/code&gt; profiles explicitly block
access to &lt;a href=&#34;https://www.rfc-editor.org/rfc/rfc1918&#34;&gt;RFC 1918&lt;/a&gt; networks,
so if you need the agent to access some service on your LAN, this is
the only way.&lt;/p&gt;
&lt;h2 id=&#34;the-git-workflow&#34;&gt;The git workflow&lt;/h2&gt;
&lt;p&gt;The real utility of this setup is the git branching workflow. You work
on a dev branch, the agent commits and pushes to it, and you can pull
those changes on any other machine to test.&lt;/p&gt;
&lt;h3 id=&#34;setup&#34;&gt;Setup&lt;/h3&gt;
&lt;p&gt;In the project you cloned earlier, check out a dev branch:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd ~/project
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git checkout -b dev/claude-work
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;agent-directives&#34;&gt;Agent directives&lt;/h3&gt;
&lt;p&gt;Both Claude Code and Open Code support project-level instruction
files. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;; Open Code reads
&lt;code&gt;AGENTS.md&lt;/code&gt;. Add one (or both) to the project root:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Always run &lt;span style=&#34;color:#e6db74&#34;&gt;`git pull`&lt;/span&gt; before making any changes.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; When working on a branch other than &lt;span style=&#34;color:#e6db74&#34;&gt;`master`&lt;/span&gt; or &lt;span style=&#34;color:#e6db74&#34;&gt;`main`&lt;/span&gt;, automatically commit
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  and push changes when done with a task.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The agent reads this file and follows the directives. When you give it
a task (&amp;ldquo;add input validation to the login handler&amp;rdquo;), it will pull,
make the changes, commit with a message describing what it did, and
push.&lt;/p&gt;
&lt;h3 id=&#34;testing-on-other-machines&#34;&gt;Testing on other machines&lt;/h3&gt;
&lt;p&gt;On your desktop, or wherever you want to test:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git fetch origin
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git checkout dev/claude-work
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git pull
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# run tests, build, review the diff, whatever&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the tests fail, go back to the Emacs shell buffer and tell the
agent what went wrong. It pulls, fixes, commits, pushes. You pull
again. This loop works the same way it would with any remote
collaborator, except the collaborator happens to be an AI running in a
VM on your laptop.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re happy with the branch, merge it however you normally
merge branches.&lt;/p&gt;
&lt;h2 id=&#34;snapshots&#34;&gt;Snapshots&lt;/h2&gt;
&lt;p&gt;One of the nice things about running in a VM is that you can snapshot
the state disk before asking the agent to do anything particularly
adventurous:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just snapshot claude-dev before-refactor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If things go sideways:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just restore-snapshot claude-dev before-refactor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This only snapshots the &lt;code&gt;/var&lt;/code&gt; disk (which contains home directories,
git repos, and all mutable state). The root filesystem is immutable so
there&amp;rsquo;s nothing to snapshot there.&lt;/p&gt;
&lt;h2 id=&#34;upgrades-and-profiles&#34;&gt;Upgrades and profiles&lt;/h2&gt;
&lt;p&gt;The template uses composable mixin profiles. Instead of a deep
inheritance hierarchy, you combine profiles as needed:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;core         → SSH daemon, user accounts, firewall (always included)
docker       → Docker daemon + user access
podman       → Podman + distrobox/buildah/skopeo
nvidia       → NVIDIA GPU support (requires docker)
python       → Python/uv development
rust         → Rust/rustup development
dev          → Development tools (neovim, tmux, etc.)
home-manager → Home-manager with sway-home modules (emacs, shell config, etc.)
claude       → Claude Code CLI
open-code    → OpenCode CLI
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Profiles are specified as comma-separated lists. For example,
&lt;code&gt;claude,dev,docker,podman&lt;/code&gt; gives you Claude Code with the full
development environment, Docker, and Podman. Each VM&amp;rsquo;s selected
profiles are stored in &lt;code&gt;machines/&amp;lt;name&amp;gt;/profile&lt;/code&gt;, which is set during
&lt;code&gt;just create&lt;/code&gt; and read by &lt;code&gt;just upgrade&lt;/code&gt; to know which image to
rebuild. To change a VM&amp;rsquo;s profiles, edit that file and put the
comma-separated list you want, then run &lt;code&gt;just upgrade &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To add packages, edit the relevant profile file in &lt;code&gt;profiles/&lt;/code&gt;, or
create your own. For example, to add &lt;code&gt;go&lt;/code&gt; to the dev profile:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# profiles/dev.nix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{ pkgs&lt;span style=&#34;color:#f92672&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt; }:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  environment&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;systemPackages &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; pkgs; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    neovim
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tmux
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    go  &lt;span style=&#34;color:#75715e&#34;&gt;# add new packages here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After editing, rebuild and upgrade:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;just upgrade claude-dev    &lt;span style=&#34;color:#75715e&#34;&gt;# build and install new image, preserving /var&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your home directory, git repos, npm globals (including the agent
itself), and everything else on &lt;code&gt;/var&lt;/code&gt; survives the upgrade. Only the
read-only root filesystem gets replaced.&lt;/p&gt;
&lt;h2 id=&#34;multiple-vms&#34;&gt;Multiple VMs&lt;/h2&gt;
&lt;p&gt;VMs created from the same base image are thin-provisioned (QCOW2
backing files), so they only store deltas from the shared image. You
can spin up multiple VMs — one per project or task — each with their
own repos and contexts, without duplicating the full OS image for each
one.&lt;/p&gt;
&lt;h2 id=&#34;bridged-networking&#34;&gt;Bridged networking&lt;/h2&gt;
&lt;p&gt;By default VMs use NAT, which means they&amp;rsquo;re accessible from the host
but not from other machines on your LAN. If you want to pull the
agent&amp;rsquo;s commits from a desktop on the same network without going
through GitHub, you can use bridged networking. When running &lt;code&gt;just create&lt;/code&gt;, select &amp;ldquo;bridge&amp;rdquo; for the network mode when prompted.&lt;/p&gt;
&lt;p&gt;The VM will get an IP from your LAN&amp;rsquo;s DHCP server and be directly
reachable from other machines.&lt;/p&gt;
&lt;p&gt;See more information in the nixos-vm-template
&lt;a href=&#34;https://github.com/EnigmaCurry/nixos-vm-template?tab=readme-ov-file#bridged-networking&#34;&gt;README.md&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Putting a code agent in an immutable NixOS VM keeps your laptop safe:
the OS is read-only, mutable state is isolated on &lt;code&gt;/var&lt;/code&gt;, and snapshots
make rollback trivial. With TRAMP and a branch-based git loop, it
still feels local—and the agent’s work is easy to test anywhere.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
