Blogg

Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn

Callista medarbetare Peter Merikan

How to use Lima instead of Docker Desktop

// Peter Merikan

You have multiple options to run Docker containers on your desktop. A standard approach is to install Docker Desktop, but there are other solutions that might fit better. In this blog post I will show you my favorite, how to install and use it. Oh, and by the way, I’m using macOS.

When developing software, it is very common to want access to Docker for various reasons. Installing Docker Desktop is an obvious choice and it was my go-to option for a long time. Later I decided to switch to Lima instead, for the reasons I will explain in this blog post.

I am using macOS since many years back and have been a happy Docker user for many years. On August 31, 2021, Docker announced that Docker Desktop would require a paid subscription for commercial use in larger companies, with a grace period until January 31, 2022. I’m an advocate of free and open source software, but of course I have no problem that companies are trying to have a sustainable business model and make money for the business. For me, it was a no-brainer. I naturally started looking for other alternatives and have tried quite a few of them over the years.

So what is Docker anyway?

With Docker you can package an application to an image, along with everything it needs to run (like code, libraries, and settings) into a single standard container so it can run reliably on any computer. With Docker you can also run these images as containers in an isolated environment.

Docker consists of two parts, the Docker Engine that is the background service that does the heavy lifting of building and running containers, and the Docker CLI that is the command-line tool you use to send instructions to that engine. Together, they form the core system that lets you control your software containers through text commands.

Both the client and the server tools of Docker are open source and free to use and the restrictions apply only to Docker Desktop. Docker Desktop is a GUI application designed for local development that bundles the Docker Engine, CLI, and a lightweight virtual machine to make building and testing containers easy on your laptop.

As I mentioned earlier, I’m using macOS and Docker does only run natively on Linux. Therefore, I have to virtualize a Linux OS to run Docker, and that is what Docker Desktop does. If you use Linux, you don’t need virtualization because you can run Docker natively. If you use Windows, WSL2 runs a Linux VM for you, and Docker runs natively inside it. In both cases you can still use Lima if you want Docker in separate, isolated VMs that you can create and throw away.

What alternatives do I have?

So in the autumn of 2021, I started experimenting with Podman, which worked pretty well, and I really liked the work they did on making it rootless. Since I’m not really that interested in the GUI part, I looked into other alternatives and found lima-vm as well as Colima, which is built on top of lima-vm. Back then, I sometimes had issues mounting to the disk, so it wasn’t always possible to run with my projects. This problem is however fixed in later versions. After a while, Rancher Desktop was released, which was also a fully viable alternative. Still, it was Colima that I continued using, together with Docker Desktop without breaking the license terms.

Today we have a handful of great open source alternatives and those are Lima, Podman Desktop, Rancher Desktop, Colima and Finch. Rancher Desktop, Colima and Finch use Lima under the hood to run Linux VMs on macOS. Colima is a great product that I used for a long time but I later switched to using Lima instead for running my docker environment.

Colima and Lima are both great software. I switched to Lima because I also wanted to run general-purpose Linux virtual machines, not only containers, and Lima gives me both with less abstraction. If you only want to run containers with Docker, Colima is probably a good fit.

What is Lima

Lima is an open-source tool that allows developers to easily spin up Linux virtual machines with automatic file sharing and port forwarding. It provides a lightweight foundation for running containers and local Kubernetes environments and makes it possible to seamlessly run tools like Docker, nerdctl, and Kubernetes directly from the terminal on your computer. Lima has been a CNCF incubating project since October 2025, so it has vendor-neutral governance and isn’t owned by a single company.

While Lima was originally built for macOS, to provide a lightweight, macOS-native equivalent to Docker Desktop using Linux VMs, it works on native Linux hosts as well. Lima supports multiple virtualization frameworks that you can choose between depending on your OS. It has support for QEMU, historically Lima’s default backend, and still widely used for cross-architecture emulation (like running x86_64 containers on Apple Silicon) or older macOS versions. It supports Apple’s Virtualization Framework (vz) and is the default backend on modern macOS versions for native performance on both Apple Silicon and Intel Macs.

Using Lima (instead of Docker Desktop)

A Lima VM with Docker engine can run side by side Docker Desktop. All you have to do is set DOCKER_HOST, or switch Docker context, to use either of them. However, it’s good to know that Docker Desktop also installs the docker client CLIs for you, unlike Lima which only installs the server components.

If you uninstall Docker Desktop instead, it leaves a few things behind that will trip up the Docker CLI you install later:

  • ~/.docker/config.json still contains "credsStore": "desktop". The docker-credential-desktop helper is gone with Docker Desktop, so pulls and logins fail with exec: "docker-credential-desktop": executable file not found in $PATH. Change the value to "osxkeychain" or remove the line.
  • The same file contains "currentContext": "desktop-linux", which points at a socket that no longer exists. Run docker context use default to reset it.
  • Docker Desktop links its CLI tools into /usr/local/bin or ~/.docker/bin, and its plugins into ~/.docker/cli-plugins. Leftover links can shadow the Homebrew binaries or make brew install docker fail to link. Find the ones that point into /Applications/Docker.app and remove them:
$ ls -l /usr/local/bin ~/.docker/bin ~/.docker/cli-plugins 2>/dev/null | grep Docker.app

Getting started with Lima

Install lima with homebrew, or use your preferred method at Lima installation.

$ brew install lima

When you install Lima, you get two commands: lima and limactl. The lima command is simply a wrapper for Lima’s limactl command to provide quick access to the default instance; lima corresponds to limactl shell default. Therefore, to manage our virtual machines, we use limactl.

To create our first virtualized machine, we can use the command limactl create, which creates a virtual machine named default running the Ubuntu operating system.

$ limactl create
? Creating an instance `default` Proceed with the current configuration
INFO[0001] Attempting to download the image              arch=aarch64 location="https://cloud-images.ubuntu.com/releases/resolute/release-20260927/ubuntu-26.04-server-cloudimg-arm64.img"
INFO[0001] Resize instance default's disk from 3.5GiB to 100GiB
INFO[0001] Run `limactl start default` to start the instance.

And then we start up the default instance.

$ limactl start default
INFO[0000] Using the existing instance `default`
INFO[0000] Starting the instance `default` with internal VM driver `vz`
<snip>
INFO[0035] READY. Run `lima` to open the shell.

We can check which virtual machines are running with limactl ls command.

$ limactl ls
NAME                STATUS     SSH                VMTYPE    ARCH       CPUS    MEMORY    DISK      DIR
default             Running    127.0.0.1:54209    vz        aarch64    4       4GiB      100GiB    ~/.lima/default

We can now open a shell in the newly started Ubuntu machine, with the lima command.

$ lima
peter@lima-default:/Users/peter$ uname -a
Linux lima-default 7.0.0-34-generic #34-Ubuntu SMP PREEMPT_DYNAMIC Wed Sep  2 14:34:54 UTC 2026 aarch64 GNU/Linux

We are now inside the virtual Ubuntu machine and our home folder on the host is mounted read-only into the VM by default (more on that in the tips at the end). In the VM we can install software or whatever we want to do.

You can also use the lima command directly from the host to execute code inside the virtual machine.

$ lima uname -a
Linux lima-default 7.0.0-34-generic #34-Ubuntu SMP PREEMPT_DYNAMIC Wed Sep  2 14:34:54 UTC 2026 aarch64 GNU/Linux

Lima has built-in templates for many Linux distributions such as fedora, debian, alpine and archlinux.

Starting a fedora VM is as simple as:

$ limactl start template:fedora
? Creating an instance `fedora` Proceed with the current configuration
INFO[0001] Starting the instance `fedora` with internal VM driver `vz`
...

and to see what is running we are using the limactl ls command again.

$ limactl ls
NAME                STATUS     SSH                VMTYPE    ARCH       CPUS    MEMORY    DISK      DIR
default             Running    127.0.0.1:54209    vz        aarch64    4       4GiB      100GiB    ~/.lima/default
fedora              Running    127.0.0.1:49241    vz        aarch64    4       4GiB      100GiB    ~/.lima/fedora

and open a shell in the VM

$ limactl shell fedora

[peter@lima-fedora peter]$ uname -a
Linux lima-fedora 6.19.10-300.fc44.aarch64 #1 SMP PREEMPT_DYNAMIC Wed Mar 25 17:45:07 UTC 2026 aarch64 GNU/Linux

Lima has many built-in templates other than Linux distributions. It has templates for non-Linux operating systems such as macOS and Windows, though they are experimental. It also has templates for container engines and orchestrators such as Podman, Docker, apptainer, faasd, Kubernetes (k8s, k3s, k0s, RKE2) and more.

Lima and Docker

Docker Server

With Lima we can spawn up a virtualized Linux machine with the Docker Server, also known as Docker engine, by using the Lima template for Docker. Lima has two Docker templates, template:docker runs rootless Docker and template:docker-rootful for running rootful Docker.

First, we will create a VM with the default rootless Docker engine.

$ limactl start template:docker
? Creating an instance `docker` Proceed with the current configuration
INFO[0002] Starting the instance `docker` with internal VM driver `vz`
...
------
To run `docker` on the host (assumes docker-cli is installed), run the following commands:
docker context create lima-docker --docker "host=unix:///Users/peter/.lima/docker/sock/docker.sock"
docker context use lima-docker
docker run hello-world

Let’s see which Lima VMs are running.

$ limactl ls
NAME                STATUS     SSH                VMTYPE    ARCH       CPUS    MEMORY    DISK      DIR
default             Running    127.0.0.1:54209    vz        aarch64    4       4GiB      100GiB    ~/.lima/default
docker              Running    127.0.0.1:58112    vz        aarch64    4       4GiB      100GiB    ~/.lima/docker
fedora              Running    127.0.0.1:49241    vz        aarch64    4       4GiB      100GiB    ~/.lima/fedora

Above we can see that we also have a Lima VM named docker running. We can create multiple virtual machines with the Docker engine, but then we need to give them different names using the --name flag.

Each VM can use up to 4 CPUs and 4 GiB of memory, so three of them add up. From now on we only need the docker instance, so stop the other two:

$ limactl stop default
$ limactl stop fedora

We can verify with curl that the Docker server is answering. We will point curl to our Docker unix socket.

$ curl --unix-socket $(limactl list docker --format '{{.Dir}}/sock/docker.sock') http://localhost/_ping ; echo

OK

We now have a Docker server running, and to use that we need a Docker client.

Docker Client and Plugins

Docker Desktop comes pre-packaged with the Docker Engine and most of the Docker client software you need. When using Lima to run the Docker engine, without Docker Desktop installed, we also need to install the Docker client software that we want to use, the docker cli, docker compose and probably docker buildx.

Later on we will use a Java project that uses Testcontainers to verify our installation. Testcontainers is used for integration tests and Testcontainers then uses Docker as the underlying engine to start backing services. I am using the following software: compose, buildx and credential-helper, but you might have other preferences.

I am using macOS and Homebrew as installer and using the following command:

$ brew install docker docker-compose docker-buildx docker-credential-helper

docker-compose and docker-buildx are CLI plugins. docker-credential-helper is not a plugin. It stores registry credentials in the macOS keychain, and Docker finds it on your PATH when "credsStore": "osxkeychain" is set in ~/.docker/config.json. Each plugin is a binary, with the prefix docker-, but to use them as sub-commands in the Docker CLI (i.e. docker compose) we must help the Docker client to find the plugins. The easiest way is to update the ~/.docker/config.json file with the information.

When installing with Homebrew, you get the following important post-installation instructions:

Compose is a Docker plugin. For Docker to find the plugin, add “cliPluginsExtraDirs” to ~/.docker/config.json:

So add the setting to your Docker config.json, next to any settings already in the file. The path below is for Homebrew on Apple Silicon. On an Intel Mac it is /usr/local/lib/docker/cli-plugins, and brew --prefix prints the prefix for your machine.

{
  "cliPluginsExtraDirs": [
    "/opt/homebrew/lib/docker/cli-plugins"
  ]
}

We now have installed the plugins we need and pointed them out in our configuration file so that the Docker CLI can find them. Now we should be able to use Docker subcommands such as compose.

$ docker compose --help

However, one issue I have run into is that I use the Docker Maven plugin that uses buildx to build multi-architecture images. When I debugged the build, it turned out that the Docker Maven plugin runs buildx with its own scratch Docker config.json, which doesn’t contain my cliPluginsExtraDirs setting. Without it, Docker can’t find the Homebrew plugins, so buildx fails. To work around this, you need to symlink your plugins into the $HOME/.docker/cli-plugins directory. For me as a homebrew user I’ll run the following command, and only for buildx since it is with that command I have problems.

$ mkdir -p ~/.docker/cli-plugins
$ ln -sfn $(brew --prefix)/opt/docker-buildx/bin/docker-buildx ~/.docker/cli-plugins/docker-buildx

We have now installed all the Docker client software that we need.

To use our Lima Docker engine with a docker client, we point the client at the Docker socket of our running Lima instance named docker. There are two ways to do that: a Docker context, or the DOCKER_HOST environment variable. I recommend DOCKER_HOST. Testcontainers for Java, which we use later, looks for DOCKER_HOST but doesn’t support Docker contexts, so with DOCKER_HOST the Docker CLI and your tests use the same engine. If both are set, DOCKER_HOST wins over the context anyway.

For an instance created from Lima’s Docker template, set it for the current shell session with:

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

This is the same line as in the comments at the top of Lima’s Docker template. limactl list resolves the instance directory, so you don’t have to hard-code ~/.lima/docker.

To make it permanent, add the same line to ~/.zshrc (or ~/.bashrc) and open a new terminal. Note that limactl then runs every time a shell starts. If you want to avoid that, hard-code the path instead.

If you prefer contexts, for example to switch between Lima and Docker Desktop with docker context use, the commands that Lima prints when the instance starts create one for you. Just remember that Testcontainers for Java still needs DOCKER_HOST.

Run the hello-world docker image

$ docker run --rm hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

Great, the Docker engine inside the Lima VM is working as expected.

Lima also takes care of port forwarding. When a container publishes a port, for example with docker run -p 8080:80 nginx, Lima’s guest agent sees the port open in the VM and forwards it to 127.0.0.1:8080 on your Mac. This is also why Testcontainers works with Lima, since it connects to the containers’ mapped ports on localhost.

Using Testcontainers

Testcontainers runs your backing services, such as databases and message queues, in throwaway containers. Integration tests can then run against the real service instead of an embedded or mocked one. Testcontainers is very common in the Java world and it is now considered a best practice and the de facto standard, at least in the Java ecosystem. It has also gained significant traction in other languages and ecosystems like Go, Rust, .Net, Python and Node.js.

The Testcontainers libraries for Java, Go, Node.js, Python and .NET all look for Docker by working through a fixed list. First come explicit settings (tc.host, DOCKER_HOST or docker.host), then /var/run/docker.sock, then the socket paths that Docker Desktop (~/.docker/desktop/docker.sock and ~/.docker/run/docker.sock) and rootless Docker use. Lima’s socket, ~/.lima/<vm-name>/sock/docker.sock, isn’t on that list, so you have to point Testcontainers at it yourself. We already did that when we set DOCKER_HOST earlier.

Testcontainers with Ryuk

Testcontainers uses Ryuk to automatically clean up and remove leftover Docker containers, networks, and volumes if your test process crashes or is abruptly interrupted. Ryuk talks to Docker through the Docker socket, which Testcontainers bind-mounts into the Ryuk container.

The problem is that the bind mount is resolved inside the Lima VM, where the Docker daemon runs. By default Testcontainers mounts the socket path from DOCKER_HOST, ~/.lima/docker/sock/docker.sock, which only exists on your Mac. Inside the VM the socket is somewhere else: /var/run/docker.sock for rootful Docker, and a path under the user’s runtime directory for rootless Docker. TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE tells Testcontainers which path to mount instead.

To find the socket path in your rootless Docker VM you can run the following command:

$ limactl shell docker sh -c 'echo $XDG_RUNTIME_DIR/docker.sock'
/run/user/501/docker.sock

501 is your user ID. Lima creates the VM user with the same UID as your macOS user, so run the command yourself instead of copying my path.

To set the TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable in your terminal and the current shell session:

export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/run/user/501/docker.sock

To make your environment variable permanent, add the same line to ~/.zshrc (or ~/.bashrc) and open a new terminal.

IDE

When using Intellij, or Eclipse, started from the regular launcher on macOS doesn’t read ~/.zshrc (or .bashrc), so tests run from the IDE won’t see these variables. You have two options:

Start your IDE from the terminal. This is probably the best and easiest way to pick up the environment variables. You can run the command:

$ open -na "IntelliJ IDEA.app"

to start Intellij in the terminal. Or even better, put it in a shell function.

idea() {
  open -na "IntelliJ IDEA.app" --args "$@"
}

and then use idea . to open current directory in Intellij.

Quit IntelliJ before you start it from the terminal. If it is already running, the project opens in the running instance, which keeps its old environment. If you use JetBrains Toolbox, it can also generate an idea launcher script, so give the function another name if you have that script on your PATH.

The second option is to add the variables in Intellij. Open Intellij IDEA > Settings... > Build, Execution, Deployment > Build Tools > Maven > Runner and click the button next to Environment variables. Here you can add User environment variables for your docker environment. Below the user environment variables you can see that Include system environment variables is selected, and that list contains most of your system variables. These settings only apply when IntelliJ runs Maven. If you run tests with IntelliJ’s own JUnit runner, add the variables to the JUnit run configuration template instead.

Run tests with Testcontainers

Like a good TV chef, I have prepared a project containing three applications that use Testcontainers in different ways. The project uses Maven to build the applications.

Three ways to configure containers with Testcontainers, named by where the configuration lives.

The git repo also contains a project for building multi-architecture images.

  • multi-arch: the application is built into a multi-architecture image (linux/amd64, linux/arm64) with docker-maven-plugin and Buildx, and tested as a container.

How to run the code

You need Java 25 and a running Docker environment, such as Lima.

  1. Check out the code

    $ git clone https://github.com/merikan/lima-and-testcontainers.git
    $ cd lima-and-testcontainers
    
  2. Run the unit and integration tests for all projects except multi-arch

    $ ./mvnw verify
    

    Building the multi-arch image is slow, so multi-arch is only included with the multi-arch profile

    $ ./mvnw verify -Pmulti-arch
    

    or for one project: self-contained, project-mounted, compose or multi-arch

    $ cd self-contained
    $ ./mvnw verify
    

Tips for everyday use

A few things I have picked up after using Lima as my Docker engine every day.

Give the VM more resources

By default an instance gets at most 4 CPUs and 4 GiB of memory, as we saw in limactl ls earlier. That might be too little if you run several containers or a bigger Testcontainers suite. You can set the resources when you create a new instance:

$ limactl create --name=docker --cpus=6 --memory=8 template:docker

or change the instance we created earlier. Stop it first, and --start starts it again when the edit is done:

$ limactl stop docker
$ limactl edit docker --cpus=6 --memory=8 --start

Make your home directory writable

Lima mounts your home directory read-only into the VM. Docker bind mounts can read your project files, but a container that tries to write to them, for example a build that writes to ./target, will fail. Make the mounts writable with:

$ limactl stop docker
$ limactl edit docker --mount-writable --start

With writable mounts, everything running in the VM can write to your whole home directory. If you want to limit that, use --mount-only to mount just the directories you need, and add the :w suffix to make them writable, for example --mount-only ~/Repos:w.

Start the VM when you log in

Instead of running limactl start docker every morning, let macOS start it for you:

$ limactl autostart enable docker

Older Lima versions call this command limactl start-at-login.

Run x86_64 images on Apple Silicon

Some images are still only published for amd64. Out of the box, the VM can’t run them, unless you set up QEMU user mode emulation yourself. Rosetta is faster than QEMU, and you can enable it on the existing instance:

$ limactl stop docker
$ limactl edit docker --rosetta --start
$ docker run --rm --platform=linux/amd64 alpine uname -m
x86_64

Clean up

Stop an instance when you don’t need it, and delete it when you want to start over:

$ limactl stop docker
$ limactl delete docker

limactl prune removes cached images and other leftovers that Lima no longer needs.

Closing

I use Lima as my Docker engine every day, and I don’t miss Docker Desktop. The setup takes a few more steps, since you install the Docker CLI and plugins yourself and have to tell Testcontainers where the socket is. Once that is done it just works, and I get a free and open source setup with less abstraction and full control over my VMs.

Lima isn’t the only good option, though. Colima, Podman Desktop, Rancher Desktop and Finch are all very good alternatives to Docker Desktop, so pick the one that fits the way you work.

If you want to try Lima, clone the example project, start a Lima Docker instance and run ./mvnw verify.

If you enjoyed this post, I’d love to hear from you. Got questions, disagree with something, or have your own container war story? Drop a comment below, or find me on LinkedIn, Bluesky, Mastodon, or GitHub - pretty much everywhere except the one I eXited.

Tack för att du läser Callistas blogg.
Hjälp oss att nå ut med information genom att dela nyheter och artiklar i ditt nätverk.

Kommentarer