NixOS에서 microvm.nix로 코딩 에이전트 VM 만들기
원문은 Michael Stapelberg님이 에 게재했습니다. 이 블로그 구독하기
코딩 에이전트가 프로그램 아키텍처를 파악하거나 버그를 진단하거나 개념 증명을 개발하는 등 코드를 다루는 거의 모든 작업에서 유용한 도구라는 점을 점점 실감하게 됐다. 사용 사례에 따라 에이전트가 실행하려는 명령을 하나하나 검토하는 일은 금방 지루하고 시간이 많이 드는 작업이 된다. 검토 없이도 코딩 에이전트를 안전하게 실행하기 위해, 에이전트가 내 개인 파일에 접근할 수 없고 악성코드에 감염되더라도 부담 없이 VM을 버리고 새로 시작하면 되는 가상 머신(VM) 솔루션을 원했다.
상태가 유지되는 VM을 만들어 두고 필요할 때마다 재설치하는 방식(생각만 해도 귀찮다!) 대신, 호스트와 명시적으로 공유한 것을 제외하고는 아무것도 디스크에 남지 않는 휘발성(ephemeral) VM 모델을 선호한다.
microvm.nix 프로젝트를 이용하면 NixOS에서 이런 VM을 쉽게 만들 수 있으며, 이 글에서는 내가 VM을 설정하는 방법을 소개한다.
참고
NixOS를 처음 들어본다면 NixOS 위키백과 문서와 nixos.org를 참고하면 좋다. 나는 2025년에 왜 Nix로 갈아탔는지에 대해 발표한 바 있으며, Nix에 관한 블로그 글을 몇 편 썼다.
AI 에이전트의 위협 모델을 이해하려면 사이먼 윌리슨(Simon Willison)의 “The lethal trifecta for AI agents: private data, untrusted content, and external communication”(2025년 6월)을 읽어보길 권한다. 이 글에서 취하는 대응 방식은 위협 모델에서 ‘개인 데이터’ 부분을 아예 제거해 버리는 것이다.
샌드박싱 분야 전반을 알고 싶다면 루이스 카르도소(Luis Cardoso)의 “A field guide to sandboxes for AI”(2026년 1월)를 참고하면 된다. 이 글에서는 여러 솔루션을 비교하지 않고, 가능한 방법 중 하나만 보여줄 것이다.
마지막으로, 샌드박싱 인프라를 직접 구축하고 운영하고 싶지 않을 수도 있다. 다행히 샌드박싱은 요즘 뜨거운 주제라 이 수요를 겨냥한 상용 서비스가 많이 생겨나고 있다. 예를 들어 데이비드 크로쇼(David Crawshaw)와 조시 블리처 스나이더(Josh Bleecher Snyder)(둘 다 Go 커뮤니티에서 알게 된 사람들)가 최근 에이전트 친화적인 VM 호스팅 서비스인 exe.dev를 출시했다. 또 다른 예로는 Sprites를 출시한 Fly.io가 있다.
microvm.nix 설정하기
바로 본론으로 들어가 보자. 다음 절들에서 내 설정을 어떻게 구성했는지 단계별로 설명한다.
1단계: 네트워크 준비
먼저 192.168.33.1/24 대역을 사용하는 microbr 브리지를 새로 만들고, eno1 네트워크 인터페이스를 통해 NAT되도록 했다. microvm* 인터페이스는 모두 이 브리지에 연결된다:
systemd.network.netdevs."20-microbr".netdevConfig = {
Kind = "bridge";
Name = "microbr";
};
systemd.network.networks."20-microbr" = {
matchConfig.Name = "microbr";
addresses = [ { Address = "192.168.83.1/24"; } ];
networkConfig = {
ConfigureWithoutCarrier = true;
};
};
systemd.network.networks."21-microvm-tap" = {
matchConfig.Name = "microvm*";
networkConfig.Bridge = "microbr";
};
networking.nat = {
enable = true;
internalInterfaces = [ "microbr" ];
externalInterface = "eno1";
};2단계: flake.nix
그 다음 microvm 모듈을 flake.nix의 새로운 input으로 추가하고(microvm.nix 문서에서 자세한 내용을 확인하라) 내 PC(midna)의 NixOS 설정에서 microvm.nixosModules.host 모듈을 활성화했다. 그리고 모든 VM을 선언하는 microvm.nix 파일을 새로 만들었다. 내 flake.nix는 이렇게 생겼다:
{
inputs = {
nixpkgs = {
url = "github:nixos/nixpkgs/nixos-25.11";
};
# For more recent claude-code
nixpkgs-unstable = {
url = "github:nixos/nixpkgs/nixos-unstable";
};
stapelbergnix = {
url = "github:stapelberg/nix";
inputs.nixpkgs.follows = "nixpkgs";
};
zkjnastools = {
url = "github:stapelberg/zkj-nas-tools";
inputs.nixpkgs.follows = "nixpkgs";
};
microvm = {
url = "github:microvm-nix/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
configfiles = {
url = "github:stapelberg/configfiles";
flake = false; # repo is not a flake
};
};
outputs =
{
self,
stapelbergnix,
zkjnastools,
nixpkgs,
nixpkgs-unstable,
microvm,
home-manager,
configfiles,
}@inputs:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = false;
};
pkgs-unstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = true;
};
in
{
nixosConfigurations = {
midna = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
(import ./configuration.nix)
stapelbergnix.lib.userSettings
# Use systemd for network configuration
stapelbergnix.lib.systemdNetwork
# Use systemd-boot as bootloader
stapelbergnix.lib.systemdBoot
# Run prometheus node exporter in tailnet
stapelbergnix.lib.prometheusNode
zkjnastools.nixosModules.zkjbackup
microvm.nixosModules.host
./microvm.nix
];
};
};
};
}3단계: microvm.nix
다음 microvm.nix는 두 개의 microVM을 선언한다. 하나는 더 알아보고 싶었던 Emacs용이고, 다른 하나는 내가 잘 알고 있어 Claude의 능력을 가늠해 볼 수 있는 코드베이스인 Go Protobuf용이다:
{
config,
lib,
pkgs,
inputs,
...
}:
let
inherit (inputs)
nixpkgs-unstable
stapelbergnix
microvm
configfiles
home-manager
;
microvmBase = import ./microvm-base.nix;
in
{
microvm.vms.emacsvm = {
autostart = false;
config = {
imports = [
stapelbergnix.lib.userSettings
microvm.nixosModules.microvm
(microvmBase {
hostName = "emacsvm";
ipAddress = "192.168.83.6";
tapId = "microvm4";
mac = "02:00:00:00:00:05";
workspace = "/home/michael/microvm/emacs";
inherit
nixpkgs-unstable
configfiles
home-manager
stapelbergnix
;
})
./microvms/emacs.nix
];
};
};
microvm.vms.goprotobufvm = {
autostart = false;
config = {
imports = [
stapelbergnix.lib.userSettings
microvm.nixosModules.microvm
(microvmBase {
hostName = "goprotobufvm";
ipAddress = "192.168.83.7";
tapId = "microvm5";
mac = "02:00:00:00:00:06";
workspace = "/home/michael/microvm/goprotobuf";
inherit
nixpkgs-unstable
configfiles
home-manager
stapelbergnix
;
extraZshInit = ''
export GOPATH=$HOME/go
export PATH=$GOPATH/bin:$PATH
'';
})
./microvms/goprotobuf.nix
];
};
};
}4단계: microvm-base.nix
microvm-base.nix 모듈은 이런 파라미터들을 받아 다음과 같은 것들을 선언한다:
- 네트워크 설정: systemd-networkd(8)과 systemd-resolved(8)을 사용한다.
- 공유 디렉터리:
- 워크스페이스 디렉터리(예:
~/microvm/emacs) - 호스트의 Nix 스토어. VM이 (대부분) 캐시에서 소프트웨어를 가져올 수 있게 한다
- 이 VM의 SSH 호스트 키
~/claude-microvm. microVM에서만 사용하는 별도의 상태 디렉터리다.
- 워크스페이스 디렉터리(예:
- 8GB 디스크 오버레이(var.img).
/var/lib/microvms/<name>에 저장된다 - 하이퍼바이저로
cloud-hypervisor(QEMU도 잘 동작한다!)를 사용하며, vCPU 8개와 RAM 4GB를 할당한다. - systemd가
/nix/store를 언마운트하려다 데드록이 발생하는 문제를 우회하는 설정.
microvm-base.nix 전체 코드 펼치기
{
hostName,
ipAddress,
tapId,
mac,
workspace,
nixpkgs-unstable,
configfiles,
home-manager,
stapelbergnix,
extraZshInit ? "",
}:
{
config,
lib,
pkgs,
...
}:
let
system = pkgs.stdenv.hostPlatform.system;
pkgsUnstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = true;
};
in
{
imports = [ home-manager.nixosModules.home-manager ];
# home-manager configuration
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = { inherit configfiles stapelbergnix; };
home-manager.users.michael = {
imports = [ ./microvm-home.nix ];
microvm.extraZshInit = extraZshInit;
};
# Claude Code CLI (from nixpkgs-unstable, unfree)
environment.systemPackages = [
pkgsUnstable.claude-code
];
networking.hostName = hostName;
system.stateVersion = "25.11";
services.openssh.enable = true;
# To match midna (host)
users.groups.michael = {
gid = 1000;
};
users.users.michael = {
group = "michael";
};
services.resolved.enable = true;
networking.useDHCP = false;
networking.useNetworkd = true;
networking.tempAddresses = "disabled";
systemd.network.enable = true;
systemd.network.networks."10-e" = {
matchConfig.Name = "e*";
addresses = [ { Address = "${ipAddress}/24"; } ];
routes = [ { Gateway = "192.168.83.1"; } ];
};
networking.nameservers = [
"8.8.8.8"
"1.1.1.1"
];
# Disable firewall for faster boot and less hassle;
# we are behind a layer of NAT anyway.
networking.firewall.enable = false;
systemd.settings.Manager = {
# fast shutdowns/reboots! https://mas.to/@zekjur/113109742103219075
DefaultTimeoutStopSec = "5s";
};
# Fix for microvm shutdown hang (issue #170):
# Without this, systemd tries to unmount /nix/store during shutdown,
# but umount lives in /nix/store, causing a deadlock.
systemd.mounts = [
{
what = "store";
where = "/nix/store";
overrideStrategy = "asDropin";
unitConfig.DefaultDependencies = false;
}
];
# Use SSH host keys mounted from outside the VM (remain identical).
services.openssh.hostKeys = [
{
path = "/etc/ssh/host-keys/ssh_host_ed25519_key";
type = "ed25519";
}
];
microvm = {
# Enable writable nix store overlay so nix-daemon works.
# This is required for home-manager activation.
# Uses tmpfs by default (ephemeral), which is fine since we
# don't build anything in the VM.
writableStoreOverlay = "/nix/.rw-store";
volumes = [
{
mountPoint = "/var";
image = "var.img";
size = 8192; # MB
}
];
shares = [
{
# use proto = "virtiofs" for MicroVMs that are started by systemd
proto = "virtiofs";
tag = "ro-store";
# a host's /nix/store will be picked up so that no
# squashfs/erofs will be built for it.
source = "/nix/store";
mountPoint = "/nix/.ro-store";
}
{
proto = "virtiofs";
tag = "ssh-keys";
source = "${workspace}/ssh-host-keys";
mountPoint = "/etc/ssh/host-keys";
}
{
proto = "virtiofs";
tag = "claude-credentials";
source = "/home/michael/claude-microvm";
mountPoint = "/home/michael/claude-microvm";
}
{
proto = "virtiofs";
tag = "workspace";
source = workspace;
mountPoint = workspace;
}
];
interfaces = [
{
type = "tap";
id = tapId;
mac = mac;
}
];
hypervisor = "cloud-hypervisor";
vcpu = 8;
mem = 4096;
socket = "control.socket";
};
}5단계: microvm-home.nix
microvm-base.nix는 다시 microvm-home.nix를 불러오는데, 이 파일은 home-manager를 이용해 다음을 설정한다:
- 내 설정대로 Zsh 설정
- 내 설정대로 Emacs 설정
- 공유 디렉터리
~/claude-microvm에 Claude Code 설정.
microvm-home.nix 전체 코드 펼치기
{
config,
pkgs,
lib,
configfiles,
stapelbergnix,
...
}:
{
options.microvm = {
extraZshInit = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra lines to add to zsh initContent";
};
};
config = {
home.username = "michael";
home.homeDirectory = "/home/michael";
programs.zsh = {
enable = true;
history = {
size = 4000;
save = 10000000;
ignoreDups = true;
share = false;
append = true;
};
initContent = ''
${builtins.readFile "${configfiles}/zshrc"}
export CLAUDE_CONFIG_DIR=/home/michael/claude-microvm
${config.microvm.extraZshInit}
'';
};
programs.emacs = {
enable = true;
package = stapelbergnix.lib.emacsWithPackages { inherit pkgs; };
};
home.file.".config/emacs" = {
source = "${configfiles}/config/emacs";
};
home.stateVersion = "25.11";
programs.home-manager.enable = true;
};
}6단계: goprotobuf.nix
goprotobuf.nix는 필요하고 유용한 패키지들을 사용할 수 있게 한다:
# Project-specific configuration for goprotobufvm
{ pkgs, ... }:
{
# Development environment for Go Protobuf
environment.systemPackages = with pkgs; [
# Go toolchain
go
gopls
delve
protobuf
gnumake
gcc
git
ripgrep
];
}VM 실행하기
워크스페이스 디렉터리를 만들고 SSH 호스트 키를 생성해 보자:
mkdir -p ~/microvm/emacs/ssh-host-keys
ssh-keygen -t ed25519 -N "" \
-f ~/microvm/emacs/ssh-host-keys/ssh_host_ed25519_key
이제 VM을 시작할 수 있다:
sudo systemctl start microvm@emacsvm
VM은 몇 초 만에 부팅되어 ping에 응답한다.
그런 다음 (아마도 tmux(1) 세션에서) VM에 SSH로 접속해 공유 워크스페이스 디렉터리에서 권한 확인 없이 Claude(또는 원하는 코딩 에이전트)를 실행한다:
% ssh 192.168.83.2
emacsvm% cd microvm/emacs
emacsvm% claude --dangerously-skip-permissions
이런 환경에서 Claude를 실행하면 이렇게 보인다:

Claude로 VM 만들기
MicroVM을 한 번 직접 설정해 보고 나면, 그 과정이 꽤 번거롭다는 걸 알게 된다.
이런 작업에 Claude Skills가 도움이 될지 궁금했다. Skills는 특정 상황에서 Claude가 특정 단계들을 수행하도록 지시하는 마크다운 파일이다.
.claude/skills/create-microvm/SKILL.md를 다음과 같이 만들었다:
---
name: create-microvm
description: Creates a new microvm Virtual Machine on midna for running Claude in, with source code repositories and build dependencies available inside the microvm. Use when the user asks to create a new microvm.
---
Inspect the existing structure at ~/machines/midna (NixOS configuration using Flakes), which includes several MicroVMs in the ~/machines/midna/microvms/ directory.
Then, create a similar structure for the microvm the user asked to create. Be sure to consider:
1. Create a new subdirectory for this microvm, named NAME (the microvm name).
2. Create an entry in microvm.nix similar to an existing microvm's, but:
3. Change hostname to NAME
4. Change IP address (e.g., 192.168.83.3): find used ones and chose next free
5. Change workspace share to /home/michael/microvm/NAME
6. Include build dependencies for the new microvm based on user request
7. Create ssh-host-keys directory
8. Generate SSH host keys
9. Clone/download source code repositories into this microvm based on user request
Once the microvm is defined, update ~/machines/midna to pull it in. To verify, build the config (but do not deploy).
You need to git-commit changes to the microvm before ~/machines/midna can see them.이 스킬을 Claude Code(테스트 버전: v2.0.76 및 v2.1.15)와 Opus 4.5 모델로 사용하면, 다음과 같은 프롬프트를 보낼 수 있다:
Debian Code Search(dcs)용 microVM을 설정해 줘. 소스 코드는 ~/dcs를 참고하되 (https://github.com/Debian/dcs에서 클론해 줘)
그러면 Claude가 몇 분 동안 작업한 뒤, 중간에 확인 질문을 할 수도 있고, 완료되면 다음과 같이 보고한다:
dcsvm microVM이 성공적으로 설정되었습니다. 생성된 내용은 다음과 같습니다:
설정:
- /home/michael/machines/midna/microvms/dcs.nix - 프로젝트별 패키지(Go 툴체인, protobuf 도구, uglify-js, yuicompressor, zopfli)
- /home/michael/machines/midna/microvm.nix를 dcsvm을 포함하도록 업데이트:
- IP: 192.168.83.8
- tapId: microvm6
- mac: 02:00:00:00:00:07
워크스페이스:
- /home/michael/microvm/dcs/ - 워크스페이스 디렉터리
- /home/michael/microvm/dcs/ssh-host-keys/ - SSH 호스트 키(ed25519)
- /home/michael/microvm/dcs/dcs/ - https://github.com/Debian/dcs에서 클론한 저장소
빌드 검증 완료 - 설정이 성공적으로 빌드됩니다.
배포 후 microVM을 시작하려면:
systemctl start microvm@dcsvmSSH로 접속하려면:
ssh [email protected]
훌륭하다! 내 경험상 Claude는 VM 생성을 항상 정확하게 해냈다.
Claude Code로 이 설정 따라 하기
사실 한 걸음 더 나아갈 수도 있다. Claude에게 새로운 MicroVM 생성을 부탁하는 것을 넘어, 이 전체 설정을 통째로 당신의 NixOS 설정에 복제해 달라고 요청할 수도 있다!
다음과 같은 프롬프트를 시도해 보라:
https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/를 읽어줘 — 내 midna NixOS 설정에 똑같은 환경을 구축해 줘!
결론
NixOS는 진입 장벽이 높다는 평이 있지만, 일단 사용하기 시작하면 몇 분 만에 새로운 프로젝트를 위한 휘발성 MicroVM을 띄우는 등 강력한 일들을 할 수 있다.
유지보수도 거의 손이 안 간다. 내 개인 PC를 업데이트하면 MicroVM 설정도 새로운 소프트웨어 버전을 자동으로 사용하게 된다. 필요하면 커스터마이징도 쉽다.
이는 사실 코딩 에이전트에 대한 내 경험과도 닮아 있다. 에이전트가 기존 작업을 자동으로 더 효율적으로 만든다고 느끼기보다는, 이전에는 엄두도 못 냈던 일들을 가능하게 만든다고 느낀다(제본스 패러독스(Jevons paradox)와 비슷하다).
2025년 동안 코딩 에이전트의 품질이 향상되는 과정을 지켜보는 것은 흥미로우면서도 (좀 무섭기도 했다!) 2025년 초에는 LLM이 과대평가된 장난감이라고 생각했고, 사람들이 이 모델들이 생성한 텍스트나 코드를 보여주면 거의 모욕적으로 느껴질 정도였다. 하지만 거의 모든 차세대 프론티어 모델이 출시될 때마다 눈에 띄게 좋아졌고, 이제는 Claude Code의 능력과 품질에 여러 번 긍정적으로 놀라곤 한다. 내가 미처 고려하지 못했던 정당한 엣지 케이스까지 처리하는 코드를 만들어 내기도 했다.
이 글에서는 코딩 에이전트를 안전하게 실행하는(사실 개인 데이터에 접근해서는 안 되는 어떤 워크로드든) 한 가지 방법을 보여주었으며, 필요에 따라 여러 방식으로 조정할 수 있다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기