What This Post Covers
This is a practical walkthrough of Lima — open-source Linux VMs with automatic filesystem sharing and port forwarding, in the same spirit as WSL2, but aimed especially at macOS (and also Linux, with experimental Windows support).
By the end you’ll know how to:
- Install Lima and start a default Ubuntu VM with containerd + nerdctl
- Run, build, and compose containers the Docker-CLI way without Docker Desktop
- Choose rootless vs rootful containerd, and when to switch to a Docker/Podman template
- Use first-party templates for Docker, Podman, and Kubernetes
- Tune mounts, networking, CPU/memory, and Apple Silicon (Rosetta)
- Edit instances, SSH in from VS Code, and debug common failures
Lima is a CNCF Incubating project (Apache 2.0). It started specifically to promote containerd and nerdctl on Mac, then expanded into a general-purpose Linux guest environment.
The Basic Idea
On a Mac, Linux containers don’t run natively. Something has to provide a Linux kernel — usually a VM. Docker Desktop, Colima, Rancher Desktop, and Podman Desktop all sit on that layer. Lima is the layer: a focused CLI that boots a Linux guest, mounts your home directory, forwards ports, and exposes sockets for container engines.
Three things make the day-to-day feel like local Linux rather than a remote box:
- Automatic filesystem sharing — your host home is available inside the guest (read-only by default).
- Automatic port forwarding — guest
localhostports show up on the host. - Templates — one-liners for Docker, Podman, k3s, kubeadm, Fedora, Alpine, and more.
Under the hood on macOS (Lima ≥ 1.0), the default hypervisor is Apple’s Virtualization.framework (vmType: vz). On Linux hosts, the default is QEMU. You can still force QEMU on Mac when you need foreign architectures or options VZ doesn’t support.
Prerequisites
Supported hosts (from the official docs):
- macOS (latest recommended; VZ needs macOS 13+)
- Linux
- NetBSD / DragonFlyBSD / Windows (less tested)
QEMU is only required if you use the QEMU driver. With VZ on modern macOS, you often don’t need it for the default path.
Step 1: Install Lima
Homebrew is the usual path on macOS and Linux:
brew install limaUse brew install lima --HEAD if you want the latest unreleased bits.
Other options:
# MacPortssudo port install lima
# Nixnix-env -i limaOr grab release tarballs from GitHub Releases and extract under /usr/local. For multi-arch guest agents (needed when running foreign-architecture VMs), also install the lima-additional-guestagents archive from the same release.
Check it:
limactl --versionStep 2: Start Your First Instance
Create and start the default instance — Ubuntu with rootless containerd and nerdctl (not Docker Engine; see Step 4):
limactl startYou’ll get an interactive menu:
? Creating an instance "default"> Proceed with the current configuration Open an editor to review or modify the current configuration Choose another template (docker, podman, archlinux, fedora, ...) ExitPick Proceed with the current configuration, wait for READY, then open a shell:
limalima is shorthand for limactl shell default. Any command works:
lima uname -alima cat /etc/os-releaseFor scripts and CI, skip the TUI:
limactl start --tty=falseDefault resource envelope (current docs):
| Resource | Default |
|---|---|
| OS | Ubuntu |
| CPUs | 4 (capped by host) |
| Memory | 4 GiB (capped by host) |
| Disk | 100 GiB |
| Home mount | host ~ → guest (read-only) |
Instance data lives under ~/.lima/<name>/ (not under ~/Library/... on macOS — short paths matter for Unix sockets).
Step 3: Files, Shells, and SSH
Home directory mounts
By default the host home is mounted read-only:
- macOS host:
/Users/<you> - other hosts:
/home/<you>
The guest has its own home for writable state (since Lima v2.1, typically /home/<you>.guest; older guests used /home/<you>.linux).
Make the host mount writable:
limactl edit --mount-writableOr when creating:
limactl start --mount-writableDisable mounts entirely (useful if you want a sandbox for agents/tools):
limactl start --mount-none# orlimactl start --plain--plain goes further: no mounts, no dynamic port forwards, no containerd/guest agent bootstrap — just a VM plus your provisioning scripts.
Mount types (performance)
| Driver | Typical use | Notes |
|---|---|---|
virtiofs |
VZ on macOS (default for VZ) | Fast shared dirs |
9p |
QEMU default since Lima v1.0 | Some RHEL-like guests need reverse-sshfs instead |
reverse-sshfs |
Compatibility | SFTP over SSH; slower, more compatible |
If mounts feel slow, prefer vmType: vz + virtiofs on Apple Silicon. If a distro breaks on 9p after an upgrade, force reverse-sshfs:
limactl edit --mount-type=reverse-sshfs <NAME>SSH from the host
Lima writes an SSH config per instance:
limactl ls --format='{{.SSHConfigFile}}' defaultssh -F "$(limactl ls --format='{{.SSHConfigFile}}' default)" lima-defaultOr include all Lima instances in your global SSH config:
Include ~/.lima/*/ssh.configThen:
ssh lima-defaultThat path is what VS Code Remote-SSH expects.
Step 4: containerd — Lima’s Default Runtime
This is the part most people underuse. The default Lima template does not install the Docker Engine. It installs containerd (the industry-standard container runtime that Kubernetes, many cloud node images, and modern Docker builds already depend on) plus nerdctl (a Docker-compatible CLI on top of containerd).
If that sentence feels abstract, the practical mapping is:
| Piece | Role | Rough Docker equivalent |
|---|---|---|
| containerd | Daemon that pulls images, manages snapshots, runs containers via runc/crun | dockerd (lower half) |
| nerdctl | User-facing CLI (run, build, compose, …) |
docker CLI |
| BuildKit | Image builds (nerdctl build) |
docker build / buildx |
| CNI plugins | Container networking bridges | Docker’s built-in networking |
| Stargz Snapshotter | Lazy-pulling eStargz images | (optional Docker feature) |
Lima’s original project goal was exactly this stack on macOS: a real Linux kernel in a VM, containerd inside it, nerdctl as the UX. Docker Desktop is optional on top if you need the Docker Engine API or tooling that hard-requires it.
Why containerd (and not only Docker)
- Same runtime class as production. Most Kubernetes nodes run containerd. Learning nerdctl on Lima is closer to that world than only learning Docker Desktop’s packaging.
- Rootless by default. The default template enables user-scoped containerd — containers run without a privileged daemon as root. That’s a better default security posture for a laptop VM.
- Composable. You can keep containerd, add BuildKit, switch to a
template:dockerinstance later, or run plain VMs with--containerd=none. Nothing locks you into one product UI. - Open stack. containerd and nerdctl are Apache/CNCF-adjacent open source; you’re not buying a desktop license to get
runandcompose.
Rootless vs rootful
Lima exposes containerd through the instance YAML (and matching CLI flags). From default.yaml:
containerd: # system-wide (rootful) containerd + BuildKit + Stargz Snapshotter # Builtin default: false system: null # user-scoped (rootless) containerd + dependencies # Builtin default: true for Linux x86_64 / aarch64 guests user: nullRootless (default) — start normally, then:
lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpineRootful (system) — create with system containerd:
limactl start --containerd=systemlima sudo nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpineWith rootful containerd, nerdctl.lima alone is not enough for the system instance — you use lima sudo nerdctl ... as the docs show.
Neither:
limactl start --containerd=noneUse that when the VM is only for distro testing, compiling, or you plan to install a container engine yourself.
| Mode | Flag / config | When to use |
|---|---|---|
| Rootless user | default (containerd.user) |
Daily dev, untrusted images, laptop hygiene |
| Rootful system | --containerd=system |
Workloads that need real root in the guest daemon, some network edge cases |
| Off | --containerd=none |
Plain Linux lab, or you install Docker/Podman yourself |
Talking to nerdctl from the host
Inside the guest:
lima nerdctl versionlima nerdctl infoFrom the host, Lima ships a helper that targets the default instance:
nerdctl.lima run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine- If you installed Lima from source (
make install),nerdctl.limamay also be installed as plainnerdctl. - With Homebrew, set an alias:
alias nerdctl=nerdctl.limaOpen http://127.0.0.1:8080 on the host — Lima’s port forwarder picks up the published guest port automatically.
Everyday nerdctl (Docker muscle memory)
The UX is intentionally close to Docker. Common flows:
# Runlima nerdctl run --rm -it alpine shlima nerdctl run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
# Lifecyclelima nerdctl ps -alima nerdctl logs -f weblima nerdctl exec -it web shlima nerdctl stop weblima nerdctl rm web
# Imageslima nerdctl pull alpinelima nerdctl imageslima nerdctl rmi alpine
# Volumes & networkslima nerdctl volume create datalima nerdctl network create appnetlima nerdctl run -d --name db --network appnet -v data:/var/lib/data redis:alpine
# Cleanuplima nerdctl system prune -fCompose works when you have a compose.yaml:
lima nerdctl compose up -dlima nerdctl compose pslima nerdctl compose logs -flima nerdctl compose downBuild uses BuildKit (bundled with Lima’s containerd integration when enabled):
lima nerdctl build -t myapp:dev .lima nerdctl run --rm myapp:devMulti-platform pull/build flags exist on nerdctl (--platform, multi --platform for pulls). On Apple Silicon, combine that with Rosetta or qemu-user binfmt (see Step 8) when you need amd64 images.
containerd namespaces
containerd organizes objects into namespaces (nerdctl has first-class nerdctl namespace commands). That’s different from Kubernetes namespaces and different from Linux user namespaces. Day to day you can ignore it — default is fine — but it’s why some low-level ctr commands need -n <namespace> and why Docker-compat tooling sometimes looks “empty” until you point at the right namespace.
Advanced containerd features Lima documents
These ship with or alongside the default stack; enable them when you care about the specific win.
1. Faster rootless networking — bypass4netns
Rootless networking usually pays a userspace tax. bypass4netns is an experimental accelerator. On macOS, pair it with vzNAT so host↔guest networking isn’t stuck in the slow user-mode path:
limactl start --network=vzNATlima containerd-rootless-setuptool.sh install-bypass4netnsdlima nerdctl run --annotation nerdctl/bypass4netns=true alpine \ sh -euc 'apk add iperf3 && iperf3 -c <host-bridge-ip>'Lima’s own benchmarks (M4 Max, Lima 2.0 alpha stack) show rough order-of-magnitude gains: ~2.3 Gbit/s rootless without bypass4netns vs ~86 Gbit/s with it — approaching rootful (~90 Gbit/s) in that environment. Treat numbers as “this can matter,” not as a guarantee on your machine.
2. Faster cold starts — eStargz lazy pull
eStargz is an OCI-compatible image format that pulls layers lazily. Stargz Snapshotter support is available by default in Lima:
# Regular pull + run (full download first)lima nerdctl run ghcr.io/stargz-containers/python:3.13-org \ python3 -c 'print("hi")'
# Lazy pull via stargz snapshotterlima nerdctl --snapshotter=stargz run \ ghcr.io/stargz-containers/python:3.13-esgz \ python3 -c 'print("hi")'Docs show multi-second cold-start wins on large images when the eStargz variant is used. Pre-converted sample images are listed in the stargz-snapshotter docs.
3. Supply-chain sandbox — gomodjail (experimental)
Lima can expose nerdctl.gomodjail, which runs nerdctl under gomodjail syscall restrictions on selected Go modules:
lima nerdctl.gomodjail ...Useful if you’re evaluating tighter CLI supply-chain controls; not required for normal work.
When containerd/nerdctl is enough — and when it isn’t
Stay on the default containerd template if you:
- Want Docker-like CLI (
run,compose,build) without Docker Desktop - Prefer rootless by default
- Care about Kubernetes-adjacent tooling and runtime literacy
- Are fine with occasional CLI flag differences vs Docker
Switch to template:docker (next step) if you:
- Need the Docker Engine API / socket for host tools that only speak Docker
- Depend on Docker-only extensions, Desktop integrations, or rare CLI flags nerdctl hasn’t mirrored
- Your team standard is “install Docker, set
DOCKER_HOST”
nerdctl aims for Docker UI/UX parity, but it is not a 1:1 reimplementation of every docker flag. When something is missing, check the nerdctl command reference before assuming the runtime can’t do it — often the gap is CLI surface, not containerd itself.
Step 5: Docker Engine Without Docker Desktop
If your workflow expects a real Docker daemon and the host docker CLI:
limactl start template:dockerexport DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')docker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpineRootful Docker:
limactl start template:docker-rootfulexport DOCKER_HOST=$(limactl list docker-rootful --format 'unix://{{.Dir}}/sock/docker.sock')Podman is the same pattern:
limactl start template:podmanexport DOCKER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')docker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpinePersist DOCKER_HOST in your shell profile if this is your daily driver. The socket lives under the instance directory so it survives restarts as long as the VM is running.
Step 6: Kubernetes Templates
Single-node clusters ship as templates. Example with kubeadm (template:k8s):
limactl start template:k8sexport KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')kubectl create deployment nginx --image nginx:alpinekubectl create service nodeport nginx --node-port=31080 --tcp=80:80Other orchestrators:
| Template | Stack |
|---|---|
template:k3s |
k3s |
template:k0s |
k0s |
template:k8s |
kubeadm (Tier 1) |
template:experimental/rke2 |
RKE2 |
template:experimental/u7s |
Usernetes (rootless) |
Multi-node needs a shared guest network (lima:user-v2) so nodes can reach each other. Since Lima v2.0/v2.1, k8s and k3s templates support join workflows over that network — see the Kubernetes examples for the exact join flags.
Step 7: Templates and Distros
Create from a named template:
limactl create --name=default template:dockerlimactl start defaultOr one shot:
limactl start template:fedoralimactl shell fedoraUseful families:
- Distros:
ubuntu,debian,fedora,alpine,archlinux,opensuse,rocky,almalinux, … - Engines:
docker,docker-rootful,podman,apptainer,buildkit - Clusters:
k8s,k3s,k0s - Experimental guests: macOS, FreeBSD, Windows Server (not for daily container work)
Template URLs use the template: scheme (Lima 1.x used template:// with slashes — drop the leading slashes on current releases).
Step 8: A Solid Daily Config (Apple Silicon)
This is a common “I want something like Docker Desktop but mine” setup: VZ, writable home, Rosetta for amd64 containers, and a host-reachable guest IP via vzNAT:
limactl start \ --name=dev \ --cpus=4 \ --memory=8 \ --vm-type=vz \ --rosetta \ --mount-writable \ --network=vzNAT \ template:dockerWhat those flags do:
--vm-type=vz— Virtualization.framework (virtiofs mounts, Rosetta, vzNAT)--rosetta— run Intel Linux binaries/containers faster than full-system QEMU--mount-writable— edit host projects from inside the guest--network=vzNAT— guest gets an IP the host can reach (much faster than user-mode networking for IP access)template:docker— Docker daemon + host socket
Point the host Docker CLI at it:
export DOCKER_HOST=$(limactl list dev --format 'unix://{{.Dir}}/sock/docker.sock')docker infoMulti-arch containers
Three modes exist:
- Slow: entire foreign-arch VM via QEMU (
--arch=x86_64on ARM). Correct but painful — use only when you need a full foreign userspace/kernel. - Fast (QEMU user + binfmt): register qemu-user in the guest and run
--platform=amd64containers. - Fast 2 (Rosetta): best on Apple Silicon with
vmType: vzand--rosetta.
Rosetta example (after a VZ + Rosetta instance is up):
docker run --platform=linux/amd64 --rm alpine uname -mIf Lima hangs on Installing rosetta...:
softwareupdate --install-rosettaStep 9: Networking and Ports
Default behavior
Guest services bound to localhost are forwarded to the host automatically. You usually don’t write portForwards for day-to-day containers.
Port forwarder backends: SSH or GRPC (defaults have shifted across 1.x; recent releases use GRPC by default again). UDP needs GRPC. On modern VZ + recent systemd guests, SSH-over-AF_VSOCK can outperform classic virtio-net SSH forwarding.
Guest IP from the host
The classic guest IP 192.168.5.15 is not reachable from the host in the default user-mode network. That’s expected SLIRP isolation.
To reach the VM by IP:
# VZ only — simplest and fastestlimactl start --network=vzNAT
# QEMU path — socket_vmnet shared networklimactl start --network=lima:sharedNamed networks (shared across instances):
limactl network create mynet --gateway 192.168.42.1/24limactl network listAttach in YAML before start:
networks: - lima: mynetping through user-mode networking is often broken or shows absurd latencies — don’t use that as a health check. Prefer curl against forwarded ports.
Step 10: Edit, Clone, and Day-2 Ops
List instances:
limactl listStop / start / delete:
limactl stop defaultlimactl start defaultlimactl delete defaultEdit live config (opens editor; some options require restart):
limactl edit defaultlimactl edit --cpus=6 --memory=12 defaultCopy files:
limactl cp ./app default:~/applimactl cp -r ./project default:~/projectShell completion:
# bashsource <(limactl completion bash)
# zsh — seelimactl completion zsh --helpVS Code Remote
- Start an instance (
--mount-noneif you want agents/tools isolated from the host FS). - Add
Include ~/.lima/*/ssh.configto~/.ssh/config. - In VS Code Remote Explorer → SSH, pick
lima-<instance>. - Clone inside the guest, or
limactl cp -ra project in.
This is a clean way to keep untrusted toolchains and AI agent shells off the host.
Debugging
limactl --debug startLogs worth reading:
- Host:
~/.lima/<instance>/serial.log - Guest:
/var/log/cloud-init-output.log,/var/log/cloud-init.log
YAML is whitespace-sensitive — mixed tabs/spaces in mounts and networks is a common footgun.
How Lima Compares
| Lima | Docker Desktop | Colima | |
|---|---|---|---|
| License | Apache 2.0 | Proprietary | MIT (built on Lima) |
| Focus | General Linux VMs + containers | Docker product surface | Docker/K8s convenience |
| Templates | Distros, engines, k8s flavors | Docker-centric | Opinionated defaults |
| Under the hood | You own the VM | Hidden VM | Lima instance |
Colima, Rancher Desktop, and similar tools are often Lima underneath. If you only want docker run with zero knobs, Colima is fine. If you want explicit control of distro, mounts, Rosetta, multi-instance labs, and kubeadm/k3s templates, use Lima directly.
Limitations
Be honest about the edges:
- Not magic networking. Default guest IPs aren’t host-routable; pick
vzNATorlima:sharedwhen you need them. - Mount semantics differ by backend. 9p/virtiofs/sshfs don’t behave identically for inotify, symlinks, and chown.
- Foreign-arch full VMs are slow. Prefer Rosetta or qemu-user for containers, not full x86_64 guests on ARM, unless you must.
- Windows host / non-Linux guests are experimental. Don’t plan production workflows there yet.
- Supply chain on guest images. Template cloud images aren’t patched the second a CVE drops — run distro updates in the guest when that matters, or set
upgradePackagesthoughtfully (rapid auto-upgrades trade CVE exposure for supply-chain risk).
Wrapping Up
A useful mental model: Lima is a Linux machine manager, not a container product. Containers are the common workload; the product is the guest.
Minimum viable habit — this is the containerd path Lima was built for:
brew install limalimactl start --tty=falselima nerdctl run --rm hello-worldlima nerdctl compose version # if you live in compose filesWhen a tool hard-requires the Docker Engine API, switch the template:
limactl start template:dockerexport DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')From there, add Rosetta, writable mounts, and vzNAT as your projects demand them. Keep one lean default (containerd/nerdctl) for experiments and named instances (docker, k3s, fedora) for long-lived stacks so you don’t thrash a single VM’s disk and packages.
References
- Lima documentation
- Installation
- Usage
- Templates
- Configuration guide
- VM types (QEMU / VZ / WSL2)
- Filesystem mounts
- Port forwarding
- Network
- Multi-arch (Rosetta / QEMU)
- Container examples
- containerd (default) in Lima
- bypass4netns
- eStargz / stargz snapshotter
- containerd
- nerdctl
- nerdctl command reference
- FAQs
- GitHub: lima-vm/lima
- default.yaml template
