Lima: Run Linux VMs on macOS for Development
Lima (short for “Linux on Mac”) lets you spin up lightweight Linux virtual machines directly on macOS without the overhead of full‑blown hypervisors. It’s built on QEMU, leverages Apple’s Hypervisor.framework, and integrates seamlessly with tools like Docker, VS Code, and Homebrew. In this guide we’ll walk through installing Lima, configuring a dev‑ready VM, and using it for real‑world workflows.
Why Choose Lima over Traditional VMs?
Traditional VM solutions such as VirtualBox or Parallels require a GUI, heavy resource allocation, and often a separate licensing model. Lima, on the other hand, runs headless, starts in seconds, and consumes only the resources you explicitly request. This makes it perfect for developers who need a Linux environment for building, testing, or running containers, but don’t want to juggle another desktop app.
Because Lima uses the native Hypervisor.framework, performance is comparable to native Linux on Intel Macs and even better on Apple Silicon. It also respects macOS security policies, so you won’t need to lower System Integrity Protection (SIP) or grant unnecessary kernel extensions.
Getting Started: Installation
The easiest way to install Lima is via Homebrew. Open Terminal and run:
brew install lima
brew install colima # optional: Docker‑compatible wrapper
After installation, verify the version:
limactl version
If you see a version string (e.g., v0.12.0), you’re ready to create your first VM.
Creating a Basic Ubuntu Instance
Lima ships with a set of pre‑configured images. The most common is Ubuntu 22.04. Create a VM with a single command:
limactl start template://ubuntu
This pulls the image, sets up a default configuration (2 CPU, 4 GB RAM, 20 GB disk), and boots the VM. You’ll be dropped into an SSH session automatically.
Understanding the Default Configuration
The default config lives in ~/.lima/ubuntu/lima.yaml. A trimmed excerpt looks like this:
arch: "x86_64"
images:
- location: "https://cloud-images.ubuntu.com/releases/jammy/release-20230420/ubuntu-22.04-server-cloudimg-amd64.img"
arch: "x86_64"
digest: "sha256:..."
cpus: 2
memory: "4GiB"
disk: "20GiB"
ssh:
localPort: 60022
loadDotSSHPubKeys: true
You can edit this file to tweak resources, add mounts, or enable port forwarding. Any changes require a VM restart (limactl stop ubuntu && limactl start ubuntu).
Customizing Your VM for Development
Most developers need shared directories, specific toolchains, and fast networking. Below we’ll cover three common customizations: mounting macOS folders, installing a package manager, and exposing services.
Mounting macOS Directories
Lima can mount host paths directly into the VM using the mounts section. Add the following snippet to lima.yaml:
mounts:
- location: "~"
writable: true
mountPoint: "/home/ubuntu/host"
- location: "/Users/Shared"
writable: true
mountPoint: "/shared"
After restarting the VM, you’ll see your home directory mirrored at /home/ubuntu/host. This eliminates the need for scp or rsync when moving source files.
Installing Homebrew Inside the VM
While macOS already has Homebrew, many Linux‑only formulas are only available inside a Linux environment. Run the official installer:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.profile
source ~/.profile
brew doctor
Now you have a full Linux brew, letting you install node, go, ffmpeg, and more with a single command.
Port Forwarding for Web Services
If you’re running a local web server inside the VM, you’ll need to expose it to macOS. Add a portForwards entry:
portForwards:
- guestSocket: "127.0.0.1:3000"
hostSocket: "127.0.0.1:3000"
Now a Node.js app listening on localhost:3000 inside the VM is reachable from your macOS browser at http://localhost:3000.
Pro tip: Use a range of ports (e.g., 8000‑8100) for different services and map them in a single portForwards block to avoid conflicts.
Lima + Docker: A Match Made in Container Heaven
One of Lima’s most popular use cases is to run Docker inside the Linux VM, giving you a native‑Linux Docker daemon without Docker Desktop. The colima wrapper automates this, but you can also set it up manually.
Manual Docker Installation
SSH into your VM (if you aren’t already) and install Docker Engine:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world
The hello-world image should print a success message, confirming Docker works inside Lima.
Running Docker Compose Projects
Let’s spin up a simple Python Flask API with Docker Compose. First, create a docker-compose.yml inside your mounted folder (/home/ubuntu/host/myapp):
version: "3.9"
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/app
environment:
- FLASK_ENV=development
Next, add a Dockerfile next to it:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
And a minimal app.py and requirements.txt:
# app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/ping")
def ping():
return jsonify(message="pong")
if __name__ == "__main__":
app.run()
Flask==3.0.0
Run the stack from inside the VM:
cd /home/ubuntu/host/myapp
docker compose up --build
Because we forwarded port 5000 earlier, you can now hit http://localhost:5000/ping from macOS and see {"message":"pong"}.
Pro tip: When using volume mounts with Docker, prefer thecachedordelegatedoptions on macOS to improve I/O performance. Example:- .:/app:cached.
Remote Development with VS Code
VS Code’s Remote‑SSH extension works flawlessly with Lima because the VM already runs an SSH server. This lets you edit files, run debuggers, and launch terminals as if you were on a native Linux box.
Connecting VS Code to Lima
Open the Command Palette (⇧⌘P) and select Remote‑SSH: Connect to Host…. Add a new entry to ~/.ssh/config:
Host lima-ubuntu
HostName 127.0.0.1
Port 60022 # matches the ssh.localPort from lima.yaml
User ubuntu
IdentityFile ~/.lima/_config/ubuntu/id_ecdsa
Now select “lima-ubuntu” from the host list. VS Code will open a new window whose status bar shows “SSH: lima‑ubuntu”. All extensions you install in this window run inside the VM.
Running Linters and Formatters Inside the VM
Because the environment is Linux, you can install native tools without macOS compatibility concerns. For a Python project, you might run:
pip install black flake8 mypy
Configure VS Code’s settings (in the remote window) to use these binaries:
{
"python.formatting.provider": "black",
"python.linting.flake8Enabled": true,
"python.linting.mypyEnabled": true
}
All linting now happens inside the VM, guaranteeing the same results you’d get on a CI Linux runner.
Real‑World Use Cases
1. Cross‑Platform CI Emulation
Developers building CI pipelines for Linux can test their scripts locally with Lima. By replicating the exact Ubuntu version used on GitHub Actions, you catch distro‑specific bugs early.
2. Embedded Development
Toolchains like arm-none-eabi-gcc often ship only for Linux. Install them inside Lima, compile firmware, and then copy the binaries back to macOS for flashing.
3. Data Science on Apple Silicon
Many Python packages (e.g., tensorflow) still lack native Apple Silicon wheels. Running them inside a Linux VM with x86_64 emulation gives you access to the full ecosystem while still using your Mac’s UI.
Example: Building a Rust Binary for Linux
Suppose you’re writing a CLI tool in Rust and want to ship a Linux binary from your Mac. Inside Lima:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
rustup target add x86_64-unknown-linux-gnu
cargo build --release --target x86_64-unknown-linux-gnu
The resulting binary lives in target/x86_64-unknown-linux-gnu/release/your_tool. You can copy it to macOS with scp or directly via the mounted folder.
Pro tip: Keep a.cargo/config.tomlthat pointstarget-dirto a shared host directory. This avoids duplicate builds when switching between macOS and Linux toolchains.
Performance Tuning & Best Practices
- Allocate Resources Wisely: Start with 2 CPU and 4 GB RAM. If you notice slow builds, incrementally increase
cpusandmemoryinlima.yaml. - Use SSD‑Backed Disks: The default disk image lives on your macOS SSD, giving near‑native I/O speeds. Avoid network‑mounted disks for heavy workloads.
- Leverage Snapshots: Before major changes, snapshot the VM:
limactl snapshot save ubuntu my-snap. Restore withlimactl snapshot restore ubuntu my-snap. - Keep the VM Updated: Periodically run
sudo apt update && sudo apt upgrade -yinside the VM to receive security patches.
Automating VM Lifecycle with Scripts
For developers who spin up a VM for each feature branch, a simple Bash wrapper can streamline the process:
#!/usr/bin/env bash
set -e
VM_NAME="dev-${PWD##*/}" # name based on current directory
if ! limactl list | grep -q "$VM_NAME"; then
echo "Creating VM $VM_NAME..."
cp ~/.lima/ubuntu/lima.yaml "$HOME/.lima/$VM_NAME.yaml"
# customize name in the copy
sed -i '' "s/name: ubuntu/name: $VM_NAME/" "$HOME/.lima/$VM_NAME.yaml"
limactl start "$HOME/.lima/$VM_NAME.yaml"
else
echo "Starting existing VM $VM_NAME..."
limactl start "$VM_NAME"
fi
echo "SSH into $VM_NAME..."
limactl shell "$VM_NAME"
Running this script in any project folder creates a dedicated VM, keeping dependencies isolated per project.
Troubleshooting Common Issues
VM Fails to Start (QEMU Error)
Check that macOS’s Hypervisor.framework is enabled (it is by default). If you’re on an older macOS version, upgrade to at least macOS 12.3.
SSH Connection Refused
Verify the ssh.localPort in lima.yaml matches the port you use in ~/.ssh/config. Restart the VM after any changes.
Docker Commands Hang
Ensure your user is in the docker group inside the VM (sudo usermod -aG docker $USER) and re‑login to the VM session.
Pro tip: Run limactl logs <vm-name> to view detailed startup logs. The output often pinpoints missing kernel extensions or mis‑configured mounts.
Conclusion
Lima bridges the gap between macOS and Linux, offering a fast, lightweight, and secure way to run Linux VMs for development. By mastering its configuration, integrating Docker, and leveraging VS Code’s remote capabilities, you can create a fully featured Linux workstation without leaving your Mac. Whether you’re building containers, compiling cross‑platform binaries, or testing CI pipelines, Lima gives you the flexibility of Linux with the convenience of macOS. Happy coding!