npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lima-vm

v0.1.0

Published

JavaScript SDK for Lima virtual machines

Readme

lima-vm

TypeScript SDK for Lima virtual machines. Functional, lean, zero dependencies.

Lima launches Linux VMs on macOS (and Linux) with automatic file sharing and port forwarding. This SDK wraps limactl with a clean programmatic API — no classes, just functions.

Install

npm install lima-vm

Quick Start

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

const vm = await lima.create({
	id: "my-vm",
	cpus: 2,
	memory: 4,
	mounts: [{ src: "./project", dst: "/workspace", writable: true }],
})

await vm.start()

const result = await vm.exec("echo hello from linux")
console.log(result.stdout) // "hello from linux"

await vm.stop()
await vm.delete()

Index

Guides

API — Manage

API — VM Handle

API — Snapshots

API — Storage

API — Networking

API — System


API

Global

lima.create(options)

Create a new VM. Returns a VM handle.

const vm = await lima.create({
	// Identity
	id: "my-vm",

	// Resources
	cpus: 4,
	memory: 4, // GiB
	disk: 10, // GiB
	arch: "aarch64", // "x86_64" | "aarch64" | "riscv64"
	vmType: "vz", // "vz" | "qemu"

	// Template (pick one)
	template: "ubuntu", // built-in template name
	yaml: "/path/to/config.yaml", // custom YAML file
	url: "https://...", // remote YAML URL

	// Mounts
	mounts: [
		{ src: "~/code", dst: "/code" },
		{ src: "./project", dst: "/workspace", writable: true },
	],
	mountOnly: [
		// replaces all default mounts
		{ src: "./project", dst: "/workspace", writable: true },
	],
	mountType: "virtiofs", // "virtiofs" | "9p" | "reverse-sshfs"
	mountInotify: true,

	// Networking
	networks: ["vzNAT"],
	dns: ["8.8.8.8"],
	portForwards: ["8080:80"],

	// Features
	rosetta: true, // x86 on ARM via Rosetta
	nestedVirt: false,
	containerd: "none", // "none" | "user" | "system"
	plain: false, // no mounts, no port forwarding

	// SSH
	sshPort: 0, // 0 = random

	// Provisioning
	provision: [
		{ mode: "system", script: "apt-get update && apt-get install -y git" },
		{ mode: "user", script: "npm install -g @anthropic-ai/claude-code" },
	],

	// Advanced (raw yq expressions)
	set: [".cpus = 8"],
})

lima.get(id)

Get a handle to an existing VM.

const vm = await lima.get("my-vm")
await vm.exec("whoami")

lima.list(options?)

List all VMs.

const vms = await lima.list()
// [{ id: "my-vm", status: "Running", cpus: 4, memory: 4294967296, ... }]

const running = await lima.list({ filter: '.status == "Running"' })

lima.info()

Show diagnostic information about the Lima installation.

const info = await lima.info()

lima.templates()

List available built-in templates.

const templates = await lima.templates()
// ["default", "docker", "ubuntu", "alpine", ...]

lima.prune()

Remove unused cache data.

await lima.prune()

lima.validate(path)

Validate a YAML template file.

await lima.validate("./my-template.yaml")

Lima(options?)

Create a lima instance. Downloads the binary if not present.

import Lima from "lima-vm"

const lima = await Lima() // latest version
const lima = await Lima({ version: "2.1.0" }) // specific version
const lima = await Lima({ home: "/custom/path" }) // custom home dir

VM Handle

Returned by create, get, and clone. All operations target this specific VM.

Lifecycle

await vm.start()
await vm.stop()
await vm.stop({ force: true })
await vm.restart()
await vm.delete()
await vm.delete({ force: true })

vm.exec(command, options?)

Run a command and wait for completion.

const result = await vm.exec("ls -la /workspace")
// { stdout: "...", stderr: "...", exitCode: 0 }

const result = await vm.exec("npm test", {
	cwd: "/workspace",
	env: { NODE_ENV: "test", CI: "true" },
	timeout: 60000,
	shell: "/bin/bash",
})

vm.spawn(command, options?)

Spawn a long-running process. Returns a handle with streams.

const proc = vm.spawn("npm run dev", {
	cwd: "/workspace",
	env: { PORT: "3000" },
})

proc.stdout.on("data", (chunk) => console.log(chunk.toString()))
proc.stderr.on("data", (chunk) => console.error(chunk.toString()))
proc.stdin.write("input\n")

const exitCode = await proc.exit
proc.kill()

vm.copy(src, dst, options?)

Copy files between host and guest. Prefix guest paths with :.

await vm.copy("./local.txt", ":/tmp/file.txt") // host → guest
await vm.copy(":/etc/os-release", "./os-release.txt") // guest → host
await vm.copy("./dir", ":/workspace/", { recursive: true })
await vm.copy("./large", ":/data/", { recursive: true, backend: "rsync" })

vm.clone(newId, options?)

Clone this VM into a new one. Returns a new VM handle.

const task = await vm.clone("task-123")

const task = await vm.clone("task-456", {
	cpus: 2,
	mountOnly: [{ src: "./other", dst: "/workspace", writable: true }],
})

vm.edit(options?)

Edit configuration. VM must be stopped.

await vm.edit({ cpus: 8, memory: 8 })

vm.rename(newId)

Rename this VM. Returns a new handle.

const renamed = await vm.rename("better-name")

vm.protect() / vm.unprotect()

Protect from accidental deletion.

await vm.protect()
await vm.unprotect()

vm.factoryReset()

await vm.factoryReset()

vm.ssh(options?)

Get SSH connection details.

const ssh = await vm.ssh()
// { host: "127.0.0.1", port: 60022, user: "hugo.linux", key: "~/.lima/..." }

const cmd = await vm.ssh({ format: "cmd" })
// "ssh -p 60022 -i ~/.lima/... [email protected]"

vm.status

Current status.

console.log(vm.status) // "Running" | "Stopped" | ...

Snapshots

await vm.snapshot.list()
await vm.snapshot.create("clean-state")
await vm.snapshot.apply("clean-state")
await vm.snapshot.delete("clean-state")

VM must be stopped for create and apply.


Disks

await lima.disk.list()
await lima.disk.create("data", { size: "50GiB", format: "qcow2" })
await lima.disk.resize("data", "100GiB")
await lima.disk.import("external", "/path/to/disk.qcow2")
await lima.disk.unlock("data")
await lima.disk.delete("data")

Networks

Create and manage virtual networks for VM-to-VM communication.

// List networks
const networks = await lima.network.list()

// Create a network with a gateway
await lima.network.create("my-net", { gateway: "192.168.42.1/24" })

// Use it when creating VMs
const vm1 = await lima.create({ id: "vm1", networks: ["lima:my-net"] })
const vm2 = await lima.create({ id: "vm2", networks: ["lima:my-net"] })

// Delete
await lima.network.delete("my-net", { force: true })

Tunnel

Create a SOCKS tunnel so the host can join the guest network.

const tunnel = await vm.tunnel({ port: 1080 })
// SOCKS proxy at localhost:1080 → guest network
tunnel.close()

// Random port
const tunnel = await vm.tunnel()
console.log(tunnel.port) // assigned port

Sudoers

Generate sudoers configuration for Lima (needed for vmnet).

const sudoers = await lima.sudoers()
// Write to /etc/sudoers.d/lima

Events

Watch events from instances. Returns an async iterable.

for await (const event of lima.watch()) {
	console.log(event)
}

for await (const event of lima.watch("my-vm")) {
	if (event.type === "stopped") break
}

Templates

Built-in templates

Pass template when creating a VM to use a pre-configured setup:

const vm = await lima.create({ id: "dev", template: "docker" })
const vm = await lima.create({ id: "k8s", template: "k8s" })

Available built-in templates:

| Category | Templates | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Default | default (Ubuntu) | | Ubuntu | ubuntu, ubuntu-lts, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, ubuntu-24.10, ubuntu-25.04, ubuntu-25.10 | | Debian | debian, debian-11, debian-12, debian-13 | | Fedora | fedora, fedora-41, fedora-42, fedora-43 | | Alpine | alpine, alpine-iso | | Arch | archlinux | | RHEL-family | almalinux, almalinux-8/9/10, rocky, rocky-8/9/10, centos-stream, centos-stream-9/10, oraclelinux, oraclelinux-8/9/10 | | SUSE | opensuse, opensuse-leap, opensuse-leap-15/16 | | Containers | docker, docker-rootful, podman, podman-rootful, containerd | | Kubernetes | k3s, k8s, k0s | | Build | buildkit | | Other Linux | homebrew-linux, linuxbrew, experimental | | Non-Linux | macos, macos-15, macos-26, freebsd, freebsd-15 | | Special | apptainer, apptainer-rootful, faasd |

Run lima.templates() for the current list from your installation.

Custom templates

Create your own YAML template for repeatable environments:

# agent-sandbox.yaml
vmType: vz
arch: aarch64

cpus: 4
memory: 4GiB
disk: 10GiB

mounts: []

mountType: virtiofs

rosetta:
    enabled: true

ssh:
    forwardAgent: true

provision:
    - mode: system
      script: |
          #!/bin/bash
          apt-get update -qq
          apt-get install -y -qq git curl build-essential
    - mode: user
      script: |
          #!/bin/bash
          curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
          source ~/.nvm/nvm.sh
          nvm install 22
          npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli

Use it:

const vm = await lima.create({
	id: "agent",
	yaml: "./agent-sandbox.yaml",
})

// Or from a URL
const vm = await lima.create({
	id: "agent",
	url: "https://raw.githubusercontent.com/you/templates/main/agent-sandbox.yaml",
})

Template YAML reference

Key configuration options for custom templates:

# VM type: "vz" (Apple Virtualization, fast) or "qemu" (cross-platform)
vmType: vz

# Architecture: "x86_64", "aarch64", or "default" (host arch)
arch: default

# Resources
cpus: 4 # default: min(4, host cores)
memory: 4GiB # default: min(4GiB, half host memory)
disk: 100GiB # default: 100GiB

# OS images (auto-selected from built-in templates)
images:
    - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
      arch: "aarch64"
      digest: "sha256:..."

# Mounts
mounts:
    - location: "~"
      writable: false
    - location: "/tmp/lima"
      writable: true

mountType: virtiofs # "virtiofs" | "9p" | "reverse-sshfs"
mountInotify: false # experimental inotify support

# Rosetta (x86 binary translation on ARM)
rosetta:
    enabled: true
    binfmt: true

# Networking
portForwards:
    - guestPort: 8080
      hostPort: 8080

networks:
    - vzNAT: true # or "lima:shared" for vmnet

# SSH
ssh:
    localPort: 0 # 0 = auto
    forwardAgent: false # forward SSH agent to guest

# Containerd
containerd:
    system: false
    user: false

# Cloud-init provisioning scripts
provision:
    - mode: system # run as root
      script: |
          #!/bin/bash
          apt-get update
          apt-get install -y git
    - mode: user # run as user
      script: |
          #!/bin/bash
          echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc

# Additional disks
additionalDisks:
    - name: "data"
      format: true
      fsType: "ext4"

# Environment variables
env:
    KEY: value

# CA certificates
caCerts:
    removeDefaults: false
    files:
        - /path/to/cert.pem

Full reference: lima-vm/lima/templates/default.yaml


AI Agent Sandbox

The primary use case — run coding agents with full permissions in complete isolation:

import Lima from "lima-vm"

const lima = await Lima()

// 1. Create a base image with all tools pre-installed
const base = await lima.create({
	id: "agent-base",
	cpus: 4,
	memory: 4,
	vmType: "vz",
	mountType: "virtiofs",
	rosetta: true,
})

await base.start()
await base.exec("sudo apt-get update -qq && sudo apt-get install -y -qq git curl build-essential")
await base.exec("curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash")
await base.exec(
	"source ~/.nvm/nvm.sh && nvm install 22 && npm install -g @anthropic-ai/claude-code @openai/codex",
)
await base.stop()
await base.snapshot.create("ready")

// 2. Per task: clone base, mount project, run agent, cleanup
async function runAgent(taskId, projectPath, prompt) {
	const vm = await base.clone(`task-${taskId}`, {
		mountOnly: [{ src: projectPath, dst: "/workspace", writable: true }],
	})

	await vm.start()

	try {
		const proc = vm.spawn(`claude -p '${prompt}'`, {
			cwd: "/workspace",
			env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
		})

		let output = ""
		proc.stdout.on("data", (chunk) => {
			output += chunk.toString()
		})
		const exitCode = await proc.exit
		return { output, exitCode }
	} finally {
		await vm.stop()
		await vm.delete({ force: true })
	}
}

// 3. Run multiple agents in parallel
await Promise.all([
	runAgent("1", "./myapp", "Fix the failing tests"),
	runAgent("2", "./myapp", "Add authentication"),
	runAgent("3", "./myapp", "Write API docs"),
])

Why Lima for AI agents?

  • Full isolation — agent can rm -rf / and your host is safe
  • Real Linux — no Docker layer, no container quirks, real kernel
  • Fastvz + virtiofs gives near-native performance (~2s boot)
  • File sharing — mount your project directory, agent sees real files
  • Snapshots — snapshot a clean state, restore after each task
  • Clone — clone a base VM with all tools pre-installed, instant spin-up
  • Network control — restrict outbound if needed

Using a custom template for agents

# agent.yaml — save once, reuse forever
vmType: vz
cpus: 4
memory: 4GiB
disk: 10GiB
mountType: virtiofs
rosetta:
    enabled: true
    binfmt: true
mounts: []
provision:
    - mode: system
      script: |
          #!/bin/bash
          set -eux
          apt-get update -qq
          apt-get install -y -qq git curl build-essential
    - mode: user
      script: |
          #!/bin/bash
          set -eux
          curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
          source ~/.nvm/nvm.sh
          nvm install 22
          npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli
const vm = await lima.create({
	id: "agent-1",
	yaml: "./agent.yaml",
	mountOnly: [{ src: "./project", dst: "/workspace", writable: true }],
})

await vm.start()
await vm.exec("claude -p 'Fix the bug'", {
	cwd: "/workspace",
	env: { ANTHROPIC_API_KEY: "..." },
})

Docker

Run Docker without Docker Desktop. No daemon on the host, no license fees, no bloat — just Docker running inside a lightweight Lima VM with automatic port forwarding and file sharing.

Quick start

import Lima from "lima-vm"

const lima = await Lima()

// Create a VM with Docker pre-installed
const vm = await lima.create({
	id: "docker",
	template: "docker",
	cpus: 4,
	memory: 4,
})

await vm.start()

// Docker is ready — run containers
const result = await vm.exec("docker run --rm hello-world")
console.log(result.stdout) // "Hello from Docker!"

Run containers

// Pull and run
await vm.exec("docker pull nginx:alpine")
await vm.exec("docker run --rm -d --name web -p 8080:80 nginx:alpine")

// Port forwarding works automatically — access from your Mac:
// curl http://localhost:8080

// Check running containers
const ps = await vm.exec("docker ps")
console.log(ps.stdout)

// Stop
await vm.exec("docker stop web")

Build images

Mount your project directory and build images inside the VM:

const vm = await lima.create({
	id: "docker-dev",
	template: "docker",
	cpus: 4,
	memory: 4,
	mounts: [{ src: "./my-app", dst: "/workspace", writable: true }],
})

await vm.start()

// Build from your project's Dockerfile
const build = await vm.exec("docker build -t my-app .", { cwd: "/workspace" })
console.log(build.stdout)

// Run the built image
await vm.exec("docker run --rm -d -p 3000:3000 my-app")

Docker Compose

// Mount project with docker-compose.yml
const vm = await lima.create({
	id: "compose-dev",
	template: "docker",
	cpus: 4,
	memory: 4,
	mounts: [{ src: "./my-project", dst: "/workspace", writable: true }],
})

await vm.start()

// Start services
await vm.exec("docker compose up -d", { cwd: "/workspace" })

// Check logs
const logs = await vm.exec("docker compose logs --tail 50", { cwd: "/workspace" })
console.log(logs.stdout)

// Teardown
await vm.exec("docker compose down", { cwd: "/workspace" })

Port forwarding

Lima forwards ports automatically. Any port a container listens on inside the VM is accessible from your host at localhost:<port>:

// Container ports are accessible from host
await vm.exec("docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16")
await vm.exec("docker run -d -p 6379:6379 redis:7")

// From your Mac:
// psql -h localhost -p 5432 -U postgres
// redis-cli -h localhost -p 6379

For explicit port forwarding rules:

const vm = await lima.create({
	id: "docker",
	template: "docker",
	portForwards: ["3000:3000", "8080:80"],
})

Development workflow

Mount your project, run services, develop on your Mac with hot reload:

const vm = await lima.create({
	id: "dev-env",
	template: "docker",
	cpus: 4,
	memory: 8,
	mounts: [{ src: "~/code/my-project", dst: "/workspace", writable: true }],
})

await vm.start()

// Start dev dependencies
await vm.exec("docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16")
await vm.exec("docker run -d -p 6379:6379 redis:7")

// Your app runs on your Mac, talks to localhost:5432 and localhost:6379
// Or run everything in the VM:
await vm.exec("docker compose up -d", { cwd: "/workspace" })

Why not Docker Desktop?

| | Docker Desktop | Lima + lima-vm | | -------------------- | --------------------------------- | ------------------------- | | License | Paid for companies >250 employees | Free (Apache-2.0 + MIT) | | Host daemon | Runs dockerd on your Mac | Nothing on host — VM only | | Resource control | Global settings | Per-VM cpus/memory/disk | | Isolation | Shared daemon | Separate VMs per project | | Programmatic | Docker SDK (REST API) | TypeScript-native SDK | | Snapshots | ✗ | ✓ snapshot & clone VMs | | Cleanup | Prune commands | Delete the VM — gone |


Node.js Development Environment

Run Node.js projects in an isolated Linux VM — no nvm/fnm conflicts on the host, reproducible across machines, easy to snapshot and share.

Quick setup

import Lima from "lima-vm"

const lima = await Lima()

// Create a VM with Ubuntu
const vm = await lima.create({
	id: "node-dev",
	cpus: 4,
	memory: 4,
	mounts: [{ src: "./my-project", dst: "/workspace", writable: true }],
})

await vm.start()

// Install nvm + Node.js 22
await vm.exec("curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash")
await vm.exec("source ~/.nvm/nvm.sh && nvm install 22")

// Install package managers
await vm.exec("source ~/.nvm/nvm.sh && npm install -g pnpm yarn")

// Install dev tools
await vm.exec("source ~/.nvm/nvm.sh && npm install -g tsx vitest eslint prettier")

// Verify
const node = await vm.exec("source ~/.nvm/nvm.sh && node --version")
console.log(node.stdout.trim()) // v22.x.x

Mount your project and develop

// Your host project directory is mounted at /workspace
const result = await vm.exec("pnpm install", { cwd: "/workspace" })

// Run dev server — port forwarding works automatically
const dev = vm.spawn("pnpm dev", {
	cwd: "/workspace",
	env: { PORT: "3000" },
})

dev.stdout.on("data", (chunk) => console.log(chunk.toString()))

// Run tests inside the VM
const tests = await vm.exec("pnpm test", {
	cwd: "/workspace",
	env: { NODE_ENV: "test", CI: "true" },
})
console.log(tests.exitCode === 0 ? "✅ Tests passed" : "❌ Tests failed")

Snapshot for reuse

Don't repeat the setup every time. Snapshot the configured environment and restore it instantly:

// After installing everything:
await vm.stop()
await vm.snapshot.create("node22-ready")

// Later — restore the clean environment
await vm.snapshot.apply("node22-ready")
await vm.start()
// Everything is exactly as you left it — Node.js, pnpm, tools, all there

Reusable template

Save a YAML template so any team member (or CI) gets the same environment:

# node-dev.yaml
vmType: vz
arch: aarch64
cpus: 4
memory: 4GiB
disk: 20GiB

mountType: virtiofs
rosetta:
    enabled: true
    binfmt: true

mounts:
    - location: "~"
      writable: false
    - location: "/tmp/lima"
      writable: true

portForwards:
    - guestPort: 3000
      hostPort: 3000
    - guestPort: 5173
      hostPort: 5173
    - guestPort: 8080
      hostPort: 8080

provision:
    - mode: system
      script: |
          #!/bin/bash
          set -eux
          apt-get update -qq
          apt-get install -y -qq ca-certificates curl git build-essential
    - mode: user
      script: |
          #!/bin/bash
          set -eux
          curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
          source ~/.nvm/nvm.sh
          nvm install 22
          npm install -g pnpm yarn tsx vitest eslint prettier
const vm = await lima.create({
	id: "node-dev",
	yaml: "./node-dev.yaml",
	mounts: [{ src: "./my-project", dst: "/workspace", writable: true }],
})
await vm.start()
// Ready to go — Node.js 22, pnpm, yarn, tsx, vitest, eslint, prettier

Why a VM instead of nvm/fnm?

  • No host pollution — Node versions, global packages, and native deps stay in the VM
  • Reproducible — same YAML template = same environment everywhere
  • Snapshottable — save a configured state, restore in seconds
  • Real Linux — test against the same OS as production
  • Parallel versions — run Node 20 and Node 22 VMs side by side, no switching
  • Shareable — commit the YAML template, team gets identical setup

Kubernetes (k3s)

Run a local Kubernetes cluster with k3s inside a Lima VM. No Docker Desktop, no minikube — just a lightweight single-node (or multi-node) K8s cluster that matches production.

Single-node cluster

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

const vm = await lima.create({
	id: "k3s",
	template: "k3s",
	cpus: 4,
	memory: 8,
})

await vm.start()

// k3s installs automatically — wait for it to be ready
await vm.exec("timeout 120 bash -c 'until kubectl get nodes | grep Ready; do sleep 2; done'")

// Deploy an app
await vm.exec("kubectl create deployment nginx --image=nginx:alpine")
await vm.exec("kubectl expose deployment nginx --port=80 --type=NodePort")

// Check it
const pods = await vm.exec("kubectl get pods")
console.log(pods.stdout)

// Get the kubeconfig for host-side kubectl
const kubeconfig = await vm.exec("cat /etc/rancher/k3s/k3s.yaml")
console.log(kubeconfig.stdout)

Multi-node cluster

Spin up a control plane and worker nodes connected via Lima's user-v2 network:

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

// Control plane
const cp = await lima.create({
	id: "k3s-cp",
	template: "k3s",
	cpus: 2,
	memory: 4,
	networks: ["lima:user-v2"],
})
await cp.start()
await cp.exec("timeout 120 bash -c 'until kubectl get nodes | grep Ready; do sleep 2; done'")

// Get join token and URL
const token = (await cp.exec("sudo cat /var/lib/rancher/k3s/server/node-token")).stdout.trim()
const url = `https://lima-k3s-cp.internal:6443`

// Worker node
const worker = await lima.create({
	id: "k3s-w1",
	template: "k3s",
	cpus: 2,
	memory: 4,
	networks: ["lima:user-v2"],
	set: [`.param.url = "${url}"`, `.param.token = "${token}"`],
})
await worker.start()

// Verify multi-node
await cp.exec(
	"timeout 60 bash -c 'until kubectl get nodes | grep -c Ready | grep -q 2; do sleep 2; done'",
)
const nodes = await cp.exec("kubectl get nodes -o wide")
console.log(nodes.stdout)

Helm + Ingress

// Install Helm inside the VM
await vm.exec(
	"curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash",
)

// Deploy with Helm
await vm.exec("helm repo add bitnami https://charts.bitnami.com/bitnami")
await vm.exec("helm install my-redis bitnami/redis --set auth.enabled=false")

// Install ingress controller
await vm.exec(
	"kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml",
)

Why a VM for Kubernetes?

  • Real cluster — k3s is production-grade Kubernetes, not a simulation
  • Network isolation — cluster network doesn't pollute your host
  • Multi-node — test leader election, pod scheduling, node failures
  • Snapshots — snapshot a working cluster, restore after experiments
  • Reproducible — same template = same cluster on any machine
  • No Docker dependency — k3s bundles containerd, no Docker Desktop needed

CI/CD Matrix Testing

Test your code across multiple Linux distributions and versions in parallel. Each VM is a clean, isolated environment — no container limitations, real systemd, real kernel.

Test across distros

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

const distros = [
	{ id: "ci-ubuntu", template: "ubuntu-24.04" },
	{ id: "ci-debian", template: "debian" },
	{ id: "ci-fedora", template: "fedora" },
	{ id: "ci-alpine", template: "alpine" },
]

const projectPath = "./my-project"

async function testOn(distro) {
	const vm = await lima.create({
		...distro,
		cpus: 2,
		memory: 2,
		mounts: [{ src: projectPath, dst: "/workspace" }],
	})

	try {
		await vm.start()

		// Install deps (distro-specific)
		if (distro.template.startsWith("alpine")) {
			await vm.exec("sudo apk add --no-cache nodejs npm git build-base")
		} else if (distro.template.startsWith("fedora")) {
			await vm.exec("sudo dnf install -y nodejs npm git gcc-c++ make")
		} else {
			await vm.exec(
				"sudo apt-get update -qq && sudo apt-get install -y -qq nodejs npm git build-essential",
			)
		}

		// Run tests
		await vm.exec("npm ci", { cwd: "/workspace" })
		const result = await vm.exec("npm test", {
			cwd: "/workspace",
			env: { CI: "true", NODE_ENV: "test" },
		})

		return { distro: distro.id, passed: result.exitCode === 0, output: result.stdout }
	} finally {
		await vm.stop({ force: true }).catch(() => {})
		await vm.delete({ force: true }).catch(() => {})
	}
}

// Run all in parallel
const results = await Promise.all(distros.map(testOn))

for (const r of results) {
	console.log(`${r.passed ? "✅" : "❌"} ${r.distro}`)
}

Snapshot-based fast CI

Create a base VM with all dependencies, snapshot it, then restore for each test run — much faster than reprovisioning:

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

// One-time setup: create and snapshot a ready environment
const base = await lima.create({
	id: "ci-base",
	cpus: 2,
	memory: 4,
	provision: [
		{
			mode: "system",
			script: "apt-get update -qq && apt-get install -y -qq git curl build-essential",
		},
		{
			mode: "user",
			script: "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && source ~/.nvm/nvm.sh && nvm install 22",
		},
	],
})
await base.start()
await base.stop()
await base.snapshot.create("ready")

// Per test run: clone from base (instant, no reprovisioning)
async function ciRun(taskId, projectPath) {
	const vm = await base.clone(`ci-${taskId}`, {
		mountOnly: [{ src: projectPath, dst: "/workspace", writable: true }],
	})
	await vm.start()

	try {
		const install = await vm.exec("source ~/.nvm/nvm.sh && npm ci", { cwd: "/workspace" })
		const test = await vm.exec("source ~/.nvm/nvm.sh && npm test", {
			cwd: "/workspace",
			env: { CI: "true" },
		})
		return { exitCode: test.exitCode, stdout: test.stdout, stderr: test.stderr }
	} finally {
		await vm.stop({ force: true }).catch(() => {})
		await vm.delete({ force: true }).catch(() => {})
	}
}

Why VMs for CI?

  • Real OS — test against actual Ubuntu/Fedora/Alpine, not a container layer
  • Systemd support — test services, daemons, init scripts that need systemd
  • Kernel features — test eBPF, cgroups, kernel modules, /proc and /sys
  • Clean state — each run starts fresh, no leftover state
  • Parallel — run 4+ distros simultaneously on a single machine
  • Reproducible — snapshot a working base, clone for each run

Production Parity

Develop on macOS but deploy to Linux? Lima gives you an identical Linux environment locally — same distro, same packages, same kernel. No more "works on my Mac" surprises.

Match your production stack

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

// Mirror your production Ubuntu 24.04 server
const vm = await lima.create({
	id: "prod-mirror",
	template: "ubuntu-24.04",
	cpus: 4,
	memory: 4,
	vmType: "vz",
	rosetta: true,
	mounts: [{ src: "./my-app", dst: "/app", writable: true }],
	provision: [
		{
			mode: "system",
			script: `#!/bin/bash
set -eux
# Match production packages exactly
apt-get update -qq
apt-get install -y -qq \\
  postgresql-client-16 \\
  redis-tools \\
  nginx \\
  certbot \\
  ffmpeg \\
  imagemagick

# Same locale as production
locale-gen en_US.UTF-8
update-locale LANG=en_US.UTF-8

# Same timezone
timedatectl set-timezone UTC
`,
		},
		{
			mode: "user",
			script: `#!/bin/bash
set -eux
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.nvm/nvm.sh
nvm install 22
npm install -g pm2
`,
		},
	],
})

await vm.start()

// Run your app exactly as it runs in production
await vm.exec("source ~/.nvm/nvm.sh && npm ci --production", { cwd: "/app" })
const proc = vm.spawn("source ~/.nvm/nvm.sh && pm2-runtime start ecosystem.config.js", {
	cwd: "/app",
	env: { NODE_ENV: "production", PORT: "3000" },
})

Reusable production template

# production.yaml — commit to your repo, everyone gets the same environment
vmType: vz
cpus: 4
memory: 4GiB
disk: 20GiB

mountType: virtiofs
rosetta:
    enabled: true
    binfmt: true

portForwards:
    - guestPort: 3000
      hostPort: 3000
    - guestPort: 5432
      hostPort: 5432
    - guestPort: 6379
      hostPort: 6379

provision:
    - mode: system
      script: |
          #!/bin/bash
          set -eux
          apt-get update -qq
          apt-get install -y -qq \
            postgresql-16 postgresql-client-16 \
            redis-server \
            nginx \
            git curl build-essential

          # Start services
          systemctl enable --now postgresql redis-server

          # Create database
          sudo -u postgres createuser -s $(whoami) 2>/dev/null || true
          sudo -u postgres createdb myapp 2>/dev/null || true
    - mode: user
      script: |
          #!/bin/bash
          set -eux
          curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
          source ~/.nvm/nvm.sh
          nvm install 22
          npm install -g pm2 pnpm
const vm = await lima.create({
	id: "prod",
	yaml: "./production.yaml",
	mounts: [{ src: "./my-app", dst: "/app", writable: true }],
})

await vm.start()

// PostgreSQL, Redis, and nginx are running — just like production
await vm.exec("source ~/.nvm/nvm.sh && pnpm install", { cwd: "/app" })
await vm.exec("source ~/.nvm/nvm.sh && pnpm db:migrate", { cwd: "/app" })
await vm.exec("source ~/.nvm/nvm.sh && pnpm dev", { cwd: "/app" })

Why VMs for production parity?

  • Real systemd — test service files, process managers, init scripts
  • Real kernel — no Docker-for-Mac kernel quirks, test kernel-level features
  • Full services — run PostgreSQL, Redis, nginx natively, not in containers
  • Same distro — Ubuntu 24.04 locally = Ubuntu 24.04 in production
  • Network stack — real iptables, real DNS resolution, real /etc/hosts
  • File permissions — Linux file ownership and permissions work correctly

Cross-Architecture Builds

Build and test x86_64 binaries on Apple Silicon (or ARM binaries on Intel). Lima supports Rosetta for near-native x86 performance on ARM Macs, and QEMU for full cross-arch emulation.

Fast x86 builds with Rosetta

On Apple Silicon Macs, Rosetta translates x86_64 instructions at near-native speed:

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

const vm = await lima.create({
	id: "x86-builder",
	cpus: 4,
	memory: 4,
	vmType: "vz",
	rosetta: true, // Enable Rosetta translation
	mounts: [{ src: "./my-project", dst: "/workspace", writable: true }],
})

await vm.start()

// Install x86_64 toolchain
await vm.exec("sudo dpkg --add-architecture amd64")
await vm.exec(
	"sudo apt-get update -qq && sudo apt-get install -y -qq gcc-x86-64-linux-gnu g++-x86-64-linux-gnu",
)

// Build x86_64 binary from ARM host
await vm.exec("x86_64-linux-gnu-gcc -o myapp-amd64 main.c", { cwd: "/workspace" })

// Test it — Rosetta runs it transparently
const result = await vm.exec("./myapp-amd64", { cwd: "/workspace" })
console.log(result.stdout)

Multi-arch container images

Build container images for both architectures using buildx:

const vm = await lima.create({
	id: "multiarch",
	template: "docker",
	cpus: 4,
	memory: 4,
	rosetta: true,
	mounts: [{ src: "./my-app", dst: "/workspace", writable: true }],
})

await vm.start()

// Set up buildx for multi-platform builds
await vm.exec("docker buildx create --name multiarch --use")
await vm.exec("docker buildx inspect --bootstrap")

// Build for both architectures
await vm.exec("docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --load .", {
	cwd: "/workspace",
})

// Test the x86 image on ARM (Rosetta handles it)
await vm.exec("docker run --platform linux/amd64 --rm myapp:latest ./run-tests")

Full cross-arch VM with QEMU

For architectures Rosetta doesn't support (RISC-V, PPC64), use QEMU system emulation:

// Run a full x86_64 VM on an ARM Mac via QEMU
const vm = await lima.create({
	id: "qemu-x86",
	arch: "x86_64",
	vmType: "qemu",
	cpus: 2,
	memory: 2,
	plain: true, // Disable mounts/port-forwarding for stability
})

await vm.start()

// This is a real x86_64 Linux environment
const uname = await vm.exec("uname -m")
console.log(uname.stdout.trim()) // "x86_64"

Why VMs for cross-arch?

  • Rosetta speed — x86 on ARM at near-native performance (~80-90%)
  • Real binaries — test actual x86 executables, not just container layers
  • CI validation — verify your ARM Mac builds work on x86 before deploying
  • Multi-arch images — build Docker images for both architectures locally
  • No cross-compile hassle — run in the target architecture natively

Multi-VM Microservices

Test distributed systems by running each service in its own VM, connected via Lima's virtual networks. Realistic network conditions, separate IP addresses, independent failure domains.

Service mesh with virtual networking

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

// Create a shared network
await lima.network.create("services", { gateway: "192.168.50.1/24" })

// API service
const api = await lima.create({
	id: "svc-api",
	cpus: 2,
	memory: 2,
	networks: ["lima:services"],
	mounts: [{ src: "./api", dst: "/app", writable: true }],
	provision: [
		{ mode: "system", script: "apt-get update -qq && apt-get install -y -qq curl" },
		{
			mode: "user",
			script: `curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.nvm/nvm.sh && nvm install 22`,
		},
	],
})

// Database service
const db = await lima.create({
	id: "svc-db",
	cpus: 2,
	memory: 4,
	networks: ["lima:services"],
	provision: [
		{
			mode: "system",
			script: `apt-get update -qq && apt-get install -y -qq postgresql-16
systemctl enable --now postgresql
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'dev'"
sudo -u postgres psql -c "CREATE DATABASE myapp"
# Listen on all interfaces
sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" /etc/postgresql/16/main/postgresql.conf
echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/16/main/pg_hba.conf
systemctl restart postgresql`,
		},
	],
})

// Cache service
const cache = await lima.create({
	id: "svc-cache",
	cpus: 1,
	memory: 1,
	networks: ["lima:services"],
	provision: [
		{
			mode: "system",
			script: `apt-get update -qq && apt-get install -y -qq redis-server
sed -i 's/bind 127.0.0.1/bind 0.0.0.0/' /etc/redis/redis.conf
systemctl restart redis-server`,
		},
	],
})

// Start all
await Promise.all([api.start(), db.start(), cache.start()])

// Services can reach each other via hostname:
// lima-svc-db.internal, lima-svc-cache.internal
await api.exec(`source ~/.nvm/nvm.sh && npm ci && npm start`, {
	cwd: "/app",
	env: {
		DATABASE_URL: "postgresql://postgres:[email protected]:5432/myapp",
		REDIS_URL: "redis://lima-svc-cache.internal:6379",
	},
})

Failure testing

// Simulate database failure
await db.stop({ force: true })

// Does the API handle it gracefully?
const health = await api.exec("curl -sf http://localhost:3000/health || echo 'unhealthy'")
console.log(health.stdout) // Should show degraded state, not crash

// Bring it back
await db.start()

// Simulate network partition via iptables
await api.exec("sudo iptables -A OUTPUT -d lima-svc-cache.internal -j DROP")
// Test how API behaves without cache...
await api.exec("sudo iptables -D OUTPUT -d lima-svc-cache.internal -j DROP")

Cleanup

// Tear down everything
await Promise.all([
	api.stop({ force: true }).then(() => api.delete({ force: true })),
	db.stop({ force: true }).then(() => db.delete({ force: true })),
	cache.stop({ force: true }).then(() => cache.delete({ force: true })),
])
await lima.network.delete("services", { force: true })

Why VMs for microservices?

  • Real networking — each VM gets its own IP, real DNS, real TCP/UDP
  • Independent failure — stop/crash one service without affecting others
  • Resource isolation — each service gets its own CPU/memory allocation
  • Production-like — closer to real server deployment than Docker Compose
  • Network testing — simulate partitions, latency, DNS failures with iptables
  • Full OS — each service runs in a real Linux with systemd, cron, syslog

Security Sandbox

Run untrusted code, scripts, or binaries in a completely isolated disposable VM. The host filesystem is never exposed, the VM has no write access to anything on the host, and you can destroy it instantly.

Run untrusted code safely

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

async function runUntrusted(code, language = "python3") {
	const vm = await lima.create({
		id: `sandbox-${Date.now()}`,
		cpus: 1,
		memory: 1,
		plain: true, // No mounts, no port forwarding — total isolation
		provision: [
			{ mode: "system", script: `apt-get update -qq && apt-get install -y -qq ${language}` },
		],
	})

	try {
		await vm.start()

		// Copy the code into the VM (no shared filesystem)
		await vm.exec(`cat > /tmp/run.py << 'SCRIPT'\n${code}\nSCRIPT`)

		// Run with resource limits
		const result = await vm.exec("timeout 30 python3 /tmp/run.py", { timeout: 35000 })

		return {
			stdout: result.stdout,
			stderr: result.stderr,
			exitCode: result.exitCode,
		}
	} finally {
		// Destroy immediately — no traces left
		await vm.stop({ force: true }).catch(() => {})
		await vm.delete({ force: true }).catch(() => {})
	}
}

// Safe to run anything — it can't escape the VM
const result = await runUntrusted(`
import os
print("I'm in:", os.getcwd())
print("User:", os.getenv("USER"))
# Even malicious code can't touch the host
`)
console.log(result.stdout)

Pre-built sandbox pool

For lower latency, maintain a pool of ready-to-use sandbox VMs using snapshots:

import Lima from "lima-vm"

const lima = await Lima({ version: "2.1.0" })

// One-time: create and snapshot a sandbox base
const base = await lima.create({
	id: "sandbox-base",
	cpus: 1,
	memory: 1,
	plain: true,
	provision: [
		{
			mode: "system",
			script: `#!/bin/bash
apt-get update -qq
apt-get install -y -qq python3 python3-pip nodejs npm ruby gcc g++ rustc
# Lock down network if desired
# iptables -A OUTPUT -j DROP
`,
		},
	],
})
await base.start()
await base.stop()
await base.snapshot.create("clean")

// Per execution: clone from base (fast, seconds not minutes)
async function quickSandbox(command) {
	const vm = await base.clone(`sandbox-${Date.now()}`)
	await vm.start()

	try {
		return await vm.exec(command, { timeout: 30000 })
	} finally {
		await vm.stop({ force: true }).catch(() => {})
		await vm.delete({ force: true }).catch(() => {})
	}
}

// Runs in seconds, not minutes
await quickSandbox("python3 -c 'print(sum(range(1000)))'")
await quickSandbox("node -e 'console.log(Array.from({length:10},(_,i)=>i*i))'")
await quickSandbox("ruby -e 'puts (1..10).select(&:odd?)'")

Network-restricted sandbox

const vm = await lima.create({
	id: "air-gapped",
	cpus: 1,
	memory: 1,
	plain: true,
	provision: [
		{
			mode: "system",
			script: `#!/bin/bash
apt-get update -qq
apt-get install -y -qq python3
# Cut all outbound network access after setup
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -j DROP
`,
		},
	],
})

await vm.start()

// Code runs with zero network access
const result = await vm.exec(
	"python3 -c 'import urllib.request; urllib.request.urlopen(\"https://evil.com\")'",
)
console.log(result.exitCode) // Non-zero — network blocked

Why VMs for sandboxing?

  • Hardware-level isolation — VM boundary, not just kernel namespaces
  • No host filesystemplain: true means zero mounts
  • Disposable — delete the VM, everything is gone
  • Network control — cut network access entirely with iptables
  • Resource limits — cap CPU, memory, disk per sandbox
  • Root is safe — untrusted code can be root inside the VM, can't touch host
  • Snapshots — restore to clean state instantly

Docker Sandbox Performance

Using the "Docker inside Lima" pattern — one VM with Docker, containers as disposable sandboxes.

Benchmarked on Apple M4 (10 cores, 16 GB RAM), VM: 4 CPUs / 4 GiB, macOS Darwin 25.3.0

| Operation | Median | P90 | Overhead vs raw exec | | ------------------------------------ | ------ | ----- | -------------------- | | Raw VM exec (echo) | 35ms | 44ms | baseline | | Container cold start (alpine echo) | 173ms | 252ms | +138ms | | Node.js container (node -e) | 193ms | 225ms | +158ms | | Python container (python -c) | 185ms | 225ms | +150ms | | Docker build + run | 257ms | 401ms | +223ms |

Key takeaway: Docker containers inside a Lima VM add ~140–220ms of overhead on top of the ~35ms SSH baseline. A full docker run with an Alpine image completes in under 200ms median — fast enough for disposable sandbox use cases.

Full results: bench/docker-sandbox-results.md


Design

  • Functionallima.create() returns a handle, not an instance of a class
  • Handle pattern — create/get once, call methods without repeating the id
  • Lean — thin wrapper around limactl, no hidden abstractions
  • Typed — full TypeScript types for all options and return values
  • Zero dependencies — just child_process from Node stdlib
  • Composableclone() and rename() return new handles

License

MIT