Genel bakış
Lima: Linux VMs for Containers Without Docker Desktop

Lima: Linux VMs for Containers Without Docker Desktop

archynarchyn
13 Temmuz 2026
17 dk okuma

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:

  1. Automatic filesystem sharing — your host home is available inside the guest (read-only by default).
  2. Automatic port forwarding — guest localhost ports show up on the host.
  3. 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:

Terminal window
brew install lima

Use brew install lima --HEAD if you want the latest unreleased bits.

Other options:

Terminal window
# MacPorts
sudo port install lima
# Nix
nix-env -i lima

Or 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:

Terminal window
limactl --version

Step 2: Start Your First Instance

Create and start the default instance — Ubuntu with rootless containerd and nerdctl (not Docker Engine; see Step 4):

Terminal window
limactl start

You’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, ...)
Exit

Pick Proceed with the current configuration, wait for READY, then open a shell:

Terminal window
lima

lima is shorthand for limactl shell default. Any command works:

Terminal window
lima uname -a
lima cat /etc/os-release

For scripts and CI, skip the TUI:

Terminal window
limactl start --tty=false

Default 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:

Terminal window
limactl edit --mount-writable

Or when creating:

Terminal window
limactl start --mount-writable

Disable mounts entirely (useful if you want a sandbox for agents/tools):

Terminal window
limactl start --mount-none
# or
limactl 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:

Terminal window
limactl edit --mount-type=reverse-sshfs <NAME>

SSH from the host

Lima writes an SSH config per instance:

Terminal window
limactl ls --format='{{.SSHConfigFile}}' default
ssh -F "$(limactl ls --format='{{.SSHConfigFile}}' default)" lima-default

Or include all Lima instances in your global SSH config:

~/.ssh/config
Include ~/.lima/*/ssh.config

Then:

Terminal window
ssh lima-default

That 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:docker instance 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 run and compose.

Rootless vs rootful

Lima exposes containerd through the instance YAML (and matching CLI flags). From default.yaml:

lima.yaml (excerpt)
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: null

Rootless (default) — start normally, then:

Terminal window
lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine

Rootful (system) — create with system containerd:

Terminal window
limactl start --containerd=system
lima sudo nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine

With rootful containerd, nerdctl.lima alone is not enough for the system instance — you use lima sudo nerdctl ... as the docs show.

Neither:

Terminal window
limactl start --containerd=none

Use 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:

Terminal window
lima nerdctl version
lima nerdctl info

From the host, Lima ships a helper that targets the default instance:

Terminal window
nerdctl.lima run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine
  • If you installed Lima from source (make install), nerdctl.lima may also be installed as plain nerdctl.
  • With Homebrew, set an alias:
Terminal window
alias nerdctl=nerdctl.lima

Open 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:

Terminal window
# Run
lima nerdctl run --rm -it alpine sh
lima nerdctl run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
# Lifecycle
lima nerdctl ps -a
lima nerdctl logs -f web
lima nerdctl exec -it web sh
lima nerdctl stop web
lima nerdctl rm web
# Images
lima nerdctl pull alpine
lima nerdctl images
lima nerdctl rmi alpine
# Volumes & networks
lima nerdctl volume create data
lima nerdctl network create appnet
lima nerdctl run -d --name db --network appnet -v data:/var/lib/data redis:alpine
# Cleanup
lima nerdctl system prune -f

Compose works when you have a compose.yaml:

Terminal window
lima nerdctl compose up -d
lima nerdctl compose ps
lima nerdctl compose logs -f
lima nerdctl compose down

Build uses BuildKit (bundled with Lima’s containerd integration when enabled):

Terminal window
lima nerdctl build -t myapp:dev .
lima nerdctl run --rm myapp:dev

Multi-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:

Terminal window
limactl start --network=vzNAT
lima containerd-rootless-setuptool.sh install-bypass4netnsd
lima 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:

Terminal window
# 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 snapshotter
lima 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:

Terminal window
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:

Terminal window
limactl start template:docker
export DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')
docker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine

Rootful Docker:

Terminal window
limactl start template:docker-rootful
export DOCKER_HOST=$(limactl list docker-rootful --format 'unix://{{.Dir}}/sock/docker.sock')

Podman is the same pattern:

Terminal window
limactl start template:podman
export DOCKER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')
docker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine

Persist 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):

Terminal window
limactl start template:k8s
export KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')
kubectl create deployment nginx --image nginx:alpine
kubectl create service nodeport nginx --node-port=31080 --tcp=80:80

Other 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:

Terminal window
limactl create --name=default template:docker
limactl start default

Or one shot:

Terminal window
limactl start template:fedora
limactl shell fedora

Useful 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:

Terminal window
limactl start \
--name=dev \
--cpus=4 \
--memory=8 \
--vm-type=vz \
--rosetta \
--mount-writable \
--network=vzNAT \
template:docker

What 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:

Terminal window
export DOCKER_HOST=$(limactl list dev --format 'unix://{{.Dir}}/sock/docker.sock')
docker info

Multi-arch containers

Three modes exist:

  1. Slow: entire foreign-arch VM via QEMU (--arch=x86_64 on ARM). Correct but painful — use only when you need a full foreign userspace/kernel.
  2. Fast (QEMU user + binfmt): register qemu-user in the guest and run --platform=amd64 containers.
  3. Fast 2 (Rosetta): best on Apple Silicon with vmType: vz and --rosetta.

Rosetta example (after a VZ + Rosetta instance is up):

Terminal window
docker run --platform=linux/amd64 --rm alpine uname -m

If Lima hangs on Installing rosetta...:

Terminal window
softwareupdate --install-rosetta

Step 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:

Terminal window
# VZ only — simplest and fastest
limactl start --network=vzNAT
# QEMU path — socket_vmnet shared network
limactl start --network=lima:shared

Named networks (shared across instances):

Terminal window
limactl network create mynet --gateway 192.168.42.1/24
limactl network list

Attach in YAML before start:

lima.yaml
networks:
- lima: mynet

ping 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:

Terminal window
limactl list

Stop / start / delete:

Terminal window
limactl stop default
limactl start default
limactl delete default

Edit live config (opens editor; some options require restart):

Terminal window
limactl edit default
limactl edit --cpus=6 --memory=12 default

Copy files:

Terminal window
limactl cp ./app default:~/app
limactl cp -r ./project default:~/project

Shell completion:

Terminal window
# bash
source <(limactl completion bash)
# zsh — see
limactl completion zsh --help

VS Code Remote

  1. Start an instance (--mount-none if you want agents/tools isolated from the host FS).
  2. Add Include ~/.lima/*/ssh.config to ~/.ssh/config.
  3. In VS Code Remote Explorer → SSH, pick lima-<instance>.
  4. Clone inside the guest, or limactl cp -r a project in.

This is a clean way to keep untrusted toolchains and AI agent shells off the host.

Debugging

Terminal window
limactl --debug start

Logs 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 vzNAT or lima:shared when 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 upgradePackages thoughtfully (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:

Terminal window
brew install lima
limactl start --tty=false
lima nerdctl run --rm hello-world
lima nerdctl compose version # if you live in compose files

When a tool hard-requires the Docker Engine API, switch the template:

Terminal window
limactl start template:docker
export 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