Tools: Running My Tiny Docker-like Runtime on macOS with Lima - Analysis

Tools: Running My Tiny Docker-like Runtime on macOS with Lima - Analysis

Running My Tiny Docker-like Runtime on macOS with Lima: Lessons, Mistakes, and a Simple Benchmark

What I Was Building

The First Mistake: Thinking Go Portability Means Runtime Portability

Why macOS Cannot Run This Directly

What Is Lima?

Why I Did Not Just Use Docker Desktop

Installing Lima

Preparing the Root Filesystem

Architecture: macOS as a Wrapper, Linux as the Runtime

Building the Linux Binary

Mistake: Assuming Host Paths Always Exist Inside Lima

Mistake: Running the Wrong Binary

Mistake: Not Checking Prerequisites Early

Example: Running a Shell

Example: Running From macOS Through Lima

Example: Testing a Memory Limit

Example: Isolated Networking

What I Learned About chroot

My Updated Mental Model of Docker

Lima vs Docker Desktop: My Practical Conclusion

Simple Experimental Benchmark

Test 1: Running Directly Inside Lima

Test 2: Running From macOS Through Lima

Benchmark Interpretation

Things I Would Improve Next

Final Thoughts When I started building my own tiny Docker-like runtime in Go, I had one simple assumption: “It is written in Go, so I should be able to run it anywhere.” That assumption was only half correct. Yes, Go makes it easy to compile binaries for different platforms. But a container runtime is not just a Go application. A container runtime depends heavily on operating system features, especially Linux kernel features. In my case, the project needed things like: And that is where macOS becomes a problem. macOS is not Linux. It does not provide Linux namespaces or cgroups in the same way. So even if my code compiled on macOS, the actual container runtime logic could not work directly on macOS. This was the point where I started using Lima. In this article, I want to share how I used Lima to run my tiny Docker-like runtime from macOS, the mistakes I made, the small design decisions I learned from, and a simple experimental benchmark at the end. This is not a “Lima vs Docker Desktop” article. It is more about understanding the boundary between macOS, Linux, Docker, and a custom container runtime. The project is a small Docker-like runtime written in Go. The goal was not to replace Docker. The goal was to understand what Docker does under the hood. Docker gives us a very clean developer experience: But behind that simple command, many things happen: The official Docker documentation describes Docker as an open platform for developing, shipping, and running applications: https://docs.docker.com/get-started/docker-overview/ That high-level explanation is useful, but when you build a tiny runtime yourself, Docker becomes much less magical. You start seeing the lower-level Linux pieces. For example, my runtime supports commands like: On Linux, this makes sense. On macOS, it immediately raises a question: Where do Linux namespaces and cgroups come from? The answer is: they do not come from macOS. My first mistake was confusing language portability with operating system feature portability. I was thinking like this: But the correct mental model is: A Go program can be cross-platform. But this does not mean every syscall or kernel feature exists on every platform. For example, when a container runtime wants to isolate a process, it may need Linux-specific features like: These are not available as normal Linux container primitives on macOS. So the real problem was not the programming language. The real problem was the kernel. That was a very important lesson for me. macOS uses the XNU kernel. Linux containers depend on the Linux kernel. This matters because containers are not virtual machines. A container is usually a regular process with a restricted view of the system. That restricted view is created by kernel features. On a Linux machine, a runtime can call these features directly. On macOS, the features are not available in the same way. So the architecture had to change. This is where Lima became useful. Lima is a tool that runs Linux virtual machines on macOS. Official documentation: https://lima-vm.io/docs/ https://lima-vm.io/docs/installation/ The important thing is this: That Linux VM gives my project access to the Linux kernel features it needs. A simple mental model: This separation helped me understand the problem much better. Lima is the environment. My Go runtime is the thing doing the container work. Alpine rootfs is the container filesystem. Docker Desktop is great. I use Docker Desktop for normal development work. But for this project, Docker Desktop was not the cleanest learning environment. Docker Desktop itself uses a Linux VM behind the scenes on macOS. That is how Docker can run Linux containers on macOS. But I was not trying to simply run containers. I was trying to build a small runtime that behaves like a container runtime. So if I put everything behind Docker Desktop too early, I would hide some of the details I wanted to learn. For that goal, Lima felt cleaner. The distinction became: So for this project, Lima gave me a better learning path. On macOS, installing Lima with Homebrew is simple: Then I created a VM for the project: Then I entered the VM: Inside the VM, I installed the Linux packages my runtime needed: Each dependency had a reason: This was already a useful learning point. A container runtime is not just one binary. It also depends on Linux system capabilities and tools, especially if you are implementing networking. A container needs a filesystem. Docker normally handles this using images and layers. My project was simpler. I used an Alpine minirootfs. At this point, the rootfs became the filesystem that the process sees inside the container-like environment. This made the concept very concrete for me. Before this project, I mostly thought about Docker images. After this project, I started thinking more clearly about root filesystems. A simplified version is: My tiny runtime does not implement Docker image layers, registries, manifests, or OCI image pulling. It simply uses an extracted root filesystem. That is enough for learning. After experimenting, I ended up with this architecture: From the user's perspective, I wanted the command to still feel simple: But internally, on macOS, it becomes closer to: So the macOS binary acts more like a dispatcher. The actual runtime work happens in Linux. A simplified architecture: This design helped me keep a clean boundary. macOS handles the developer command. Linux handles the container primitives. One mistake I made was forgetting that the binary inside Lima must match the Linux VM architecture. For example, if the Lima VM is x86_64, I can build: But if the Lima VM is aarch64, for example on Apple Silicon, I should build: To check the VM architecture: If you build the wrong architecture, you may see: This error is simple but confusing when you first see it. This was one of those small details that reminded me how important platform boundaries are. Another mistake was around file sharing. My project existed on macOS at something like: I expected that path to always work inside Lima. Sometimes the mount configuration was not what I expected. So if I ran this inside the VM: the problem was not my Go code. It was not the runtime. It was simply a shared folder issue. Always verify that the path exists inside the VM, not only on the host. If the project is not mounted, the easiest workaround is to clone the repository inside the VM: The cleaner long-term solution is to configure Lima mounts properly. But the important lesson is that a VM has its own filesystem view. Never assume the host path exists inside the guest. Another mistake was running the macOS binary when I actually needed the Linux binary. This is easy to do when you have files like: The macOS binary can be useful as a CLI wrapper. But the Linux binary must perform the real runtime operations. The separation became: This made the code easier to reason about. On macOS, I do not pretend to support Linux container primitives directly. I route the work to the Linux VM. At first, failures happened too late. For example, I could run a command and only later discover: This created confusing errors. So I started adding validation before running the actual command. Good prerequisite checks include: This improves developer experience a lot. Instead of a low-level error, I want an error like: This is not the most exciting part of a runtime project. But it is an important engineering detail. As a senior engineer, I have learned that good error messages are part of the product. Even if the product is just a learning project. After preparing the rootfs and building the runtime, I can run: From another terminal: This helped me understand process tracking better. A container runtime does not only start processes. It also needs to track them, store metadata, collect logs, stop them, and clean up after them. From macOS, I wanted a command like this: Internally, the command is routed through Lima: This gave me a nice workflow. I could stay in my macOS terminal, but still execute the real Linux runtime inside the VM. If cgroup v2 is available and the runtime supports memory limits, I can run: Inside the container-like shell: That is 128 MiB in bytes. This small test made cgroups much more real for me. Before this project, a memory limit felt like a Docker CLI option: After implementing a small version, I started seeing it differently: That is a very different level of understanding. For networking, the runtime can create a basic isolated mode using Linux networking primitives. Depending on the implementation, I expect to see a container-side interface and a default route. This was one of the most interesting parts for me. Docker networking feels simple from the outside: But underneath, there is a lot of Linux networking: Building even a small version made me appreciate how much complexity Docker hides. My runtime uses chroot as a simple way to change the visible root filesystem. For learning, this is useful. But chroot is not the same as a full production container filesystem model. With chroot, the process sees a different root directory: But production runtimes usually involve more advanced concepts: So I try to be careful when describing the project. That honesty matters. Learning projects are valuable, but they should not be oversold. Before this project, I mostly used Docker at the command level: After building a tiny runtime, I started seeing Docker in layers: But conceptually, it involves: My tiny runtime only implements a small part of this. But that small part was enough to make Docker feel less like magic. I do not see Lima and Docker Desktop as direct replacements for each other in every situation. For normal application development, Docker Desktop is usually more convenient. But for learning Linux container internals, Lima gave me a cleaner mental model. For this project, Lima was the better learning tool. This benchmark is not scientific. I only wanted to understand the rough overhead of routing commands from macOS through Lima compared with running directly inside the Lima VM. The command I tested was intentionally small: Because the command is tiny, the overhead of the runtime and VM boundary becomes easier to notice. Internally, this routes through something like: The direct Linux execution was faster. The macOS-to-Lima path had extra overhead because the command crossed the VM boundary through limactl shell. In this rough experiment: For very short commands like /bin/echo, the overhead is visible. For long-running processes, the overhead matters much less. For example, if I run a service for 10 minutes, an extra 100-200 ms at startup is not very important. My practical conclusion: There are many things I would like to improve in this project. Some of them are runtime-related: Some of them are macOS/Lima-related: A better macOS setup command could eventually look like this: That would make the project much easier to try. Lima helped me understand the boundary between macOS and Linux much better. The biggest lesson was simple: Go can make the binary portable, but it cannot make Linux kernel features exist on macOS. For a container runtime, the kernel matters. My final mental model is: This project also changed how I look at Docker. But Docker is impressive because it hides a lot of complexity behind a simple interface. But behind it, there are many layers of runtime, filesystem, networking, isolation, and process management. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 500;">docker run alpine echo hello -weight: 500;">docker run alpine echo hello -weight: 500;">docker run alpine echo hello image resolution filesystem preparation namespace creation cgroup configuration mount setup network setup process execution log tracking metadata storage cleanup image resolution filesystem preparation namespace creation cgroup configuration mount setup network setup process execution log tracking metadata storage cleanup image resolution filesystem preparation namespace creation cgroup configuration mount setup network setup process execution log tracking metadata storage cleanup tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh tiny--weight: 500;">docker-go ps tiny--weight: 500;">docker-go logs -f <container-id> tiny--weight: 500;">docker-go -weight: 500;">stop <container-id> tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh tiny--weight: 500;">docker-go ps tiny--weight: 500;">docker-go logs -f <container-id> tiny--weight: 500;">docker-go -weight: 500;">stop <container-id> tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh tiny--weight: 500;">docker-go ps tiny--weight: 500;">docker-go logs -f <container-id> tiny--weight: 500;">docker-go -weight: 500;">stop <container-id> Go can build on macOS. Therefore, my runtime should work on macOS. Go can build on macOS. Therefore, my runtime should work on macOS. Go can build on macOS. Therefore, my runtime should work on macOS. Go can compile the program for macOS. But Linux container primitives still require Linux. Go can compile the program for macOS. But Linux container primitives still require Linux. Go can compile the program for macOS. But Linux container primitives still require Linux. CLONE_NEWUTS CLONE_NEWPID CLONE_NEWNS CLONE_NEWNET cgroup filesystem mount operations veth networking iptables rules CLONE_NEWUTS CLONE_NEWPID CLONE_NEWNS CLONE_NEWNET cgroup filesystem mount operations veth networking iptables rules CLONE_NEWUTS CLONE_NEWPID CLONE_NEWNS CLONE_NEWNET cgroup filesystem mount operations veth networking iptables rules PID namespace -> gives the process its own process tree UTS namespace -> gives the process its own hostname mount namespace -> gives the process its own mount view network namespace -> gives the process its own network stack cgroups -> limit and track resource usage chroot/rootfs -> changes the visible filesystem root PID namespace -> gives the process its own process tree UTS namespace -> gives the process its own hostname mount namespace -> gives the process its own mount view network namespace -> gives the process its own network stack cgroups -> limit and track resource usage chroot/rootfs -> changes the visible filesystem root PID namespace -> gives the process its own process tree UTS namespace -> gives the process its own hostname mount namespace -> gives the process its own mount view network namespace -> gives the process its own network stack cgroups -> limit and track resource usage chroot/rootfs -> changes the visible filesystem root macOS -> tiny--weight: 500;">docker-go -> Linux namespaces/cgroups macOS -> tiny--weight: 500;">docker-go -> Linux namespaces/cgroups macOS -> tiny--weight: 500;">docker-go -> Linux namespaces/cgroups macOS -> Linux VM -> tiny--weight: 500;">docker-go -> Linux namespaces/cgroups macOS -> Linux VM -> tiny--weight: 500;">docker-go -> Linux namespaces/cgroups macOS -> Linux VM -> tiny--weight: 500;">docker-go -> Linux namespaces/cgroups Lima is not Docker. Lima is not my container runtime. Lima gives me a Linux VM. Lima is not Docker. Lima is not my container runtime. Lima gives me a Linux VM. Lima is not Docker. Lima is not my container runtime. Lima gives me a Linux VM. MacBook └── macOS └── Lima VM └── Linux └── my tiny Docker-like runtime └── container-like process MacBook └── macOS └── Lima VM └── Linux └── my tiny Docker-like runtime └── container-like process MacBook └── macOS └── Lima VM └── Linux └── my tiny Docker-like runtime └── container-like process How do I run an app in Docker? How do I run an app in Docker? How do I run an app in Docker? How does a container runtime use Linux features to isolate a process? How does a container runtime use Linux features to isolate a process? How does a container runtime use Linux features to isolate a process? Docker Desktop: Great for running Docker containers and application stacks. Lima: Great for getting a Linux environment on macOS and experimenting with Linux internals. Docker Desktop: Great for running Docker containers and application stacks. Lima: Great for getting a Linux environment on macOS and experimenting with Linux internals. Docker Desktop: Great for running Docker containers and application stacks. Lima: Great for getting a Linux environment on macOS and experimenting with Linux internals. -weight: 500;">brew -weight: 500;">install lima -weight: 500;">brew -weight: 500;">install lima -weight: 500;">brew -weight: 500;">install lima limactl -weight: 500;">start --name=tiny--weight: 500;">docker --cpus=4 --memory=4 --disk=20 limactl -weight: 500;">start --name=tiny--weight: 500;">docker --cpus=4 --memory=4 --disk=20 limactl -weight: 500;">start --name=tiny--weight: 500;">docker --cpus=4 --memory=4 --disk=20 limactl shell tiny--weight: 500;">docker limactl shell tiny--weight: 500;">docker limactl shell tiny--weight: 500;">docker -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y golang-go -weight: 500;">curl tar iproute2 iptables -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y golang-go -weight: 500;">curl tar iproute2 iptables -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y golang-go -weight: 500;">curl tar iproute2 iptables golang-go -> build and test the runtime -weight: 500;">curl -> download rootfs archives tar -> extract rootfs archives iproute2 -> work with Linux networking iptables -> configure NAT for isolated networking golang-go -> build and test the runtime -weight: 500;">curl -> download rootfs archives tar -> extract rootfs archives iproute2 -> work with Linux networking iptables -> configure NAT for isolated networking golang-go -> build and test the runtime -weight: 500;">curl -> download rootfs archives tar -> extract rootfs archives iproute2 -> work with Linux networking iptables -> configure NAT for isolated networking mkdir -p rootfs/alpine ARCH=$(uname -m) -weight: 500;">curl -L -o alpine-rootfs.tar.gz \ "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/${ARCH}/alpine-minirootfs-3.23.4-${ARCH}.tar.gz" -weight: 600;">sudo tar -xzf alpine-rootfs.tar.gz -C rootfs/alpine mkdir -p rootfs/alpine ARCH=$(uname -m) -weight: 500;">curl -L -o alpine-rootfs.tar.gz \ "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/${ARCH}/alpine-minirootfs-3.23.4-${ARCH}.tar.gz" -weight: 600;">sudo tar -xzf alpine-rootfs.tar.gz -C rootfs/alpine mkdir -p rootfs/alpine ARCH=$(uname -m) -weight: 500;">curl -L -o alpine-rootfs.tar.gz \ "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/${ARCH}/alpine-minirootfs-3.23.4-${ARCH}.tar.gz" -weight: 600;">sudo tar -xzf alpine-rootfs.tar.gz -C rootfs/alpine -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh Docker image: A packaged filesystem with metadata and layers. Rootfs: The actual filesystem view used by the container process. Docker image: A packaged filesystem with metadata and layers. Rootfs: The actual filesystem view used by the container process. Docker image: A packaged filesystem with metadata and layers. Rootfs: The actual filesystem view used by the container process. If host is Linux: run the runtime directly. If host is macOS: route the command through Lima. execute the Linux binary inside the Lima VM. If host is Linux: run the runtime directly. If host is macOS: route the command through Lima. execute the Linux binary inside the Lima VM. If host is Linux: run the runtime directly. If host is macOS: route the command through Lima. execute the Linux binary inside the Lima VM. tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/sh limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/sh limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/sh macOS terminal | | tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh v Darwin -weight: 500;">service layer | | limactl shell tiny--weight: 500;">docker -weight: 600;">sudo Linux binary ... v Lima VM | v Linux runtime binary | v namespaces + cgroups + chroot + networking macOS terminal | | tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh v Darwin -weight: 500;">service layer | | limactl shell tiny--weight: 500;">docker -weight: 600;">sudo Linux binary ... v Lima VM | v Linux runtime binary | v namespaces + cgroups + chroot + networking macOS terminal | | tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh v Darwin -weight: 500;">service layer | | limactl shell tiny--weight: 500;">docker -weight: 600;">sudo Linux binary ... v Lima VM | v Linux runtime binary | v namespaces + cgroups + chroot + networking mkdir -p bin GOOS=linux GOARCH=amd64 \ go build -o bin/tiny--weight: 500;">docker-go-linux-amd64 ./cmd/tiny--weight: 500;">docker-go mkdir -p bin GOOS=linux GOARCH=amd64 \ go build -o bin/tiny--weight: 500;">docker-go-linux-amd64 ./cmd/tiny--weight: 500;">docker-go mkdir -p bin GOOS=linux GOARCH=amd64 \ go build -o bin/tiny--weight: 500;">docker-go-linux-amd64 ./cmd/tiny--weight: 500;">docker-go mkdir -p bin GOOS=linux GOARCH=arm64 \ go build -o bin/tiny--weight: 500;">docker-go-linux-arm64 ./cmd/tiny--weight: 500;">docker-go mkdir -p bin GOOS=linux GOARCH=arm64 \ go build -o bin/tiny--weight: 500;">docker-go-linux-arm64 ./cmd/tiny--weight: 500;">docker-go mkdir -p bin GOOS=linux GOARCH=arm64 \ go build -o bin/tiny--weight: 500;">docker-go-linux-arm64 ./cmd/tiny--weight: 500;">docker-go limactl shell tiny--weight: 500;">docker uname -m limactl shell tiny--weight: 500;">docker uname -m limactl shell tiny--weight: 500;">docker uname -m x86_64 -> GOARCH=amd64 aarch64 -> GOARCH=arm64 x86_64 -> GOARCH=amd64 aarch64 -> GOARCH=arm64 x86_64 -> GOARCH=amd64 aarch64 -> GOARCH=arm64 exec format error exec format error exec format error The binary architecture does not match the machine trying to execute it. The binary architecture does not match the machine trying to execute it. The binary architecture does not match the machine trying to execute it. /Users/amir/Desktop/tiny--weight: 500;">docker /Users/amir/Desktop/tiny--weight: 500;">docker /Users/amir/Desktop/tiny--weight: 500;">docker cd /Users/amir/Desktop/tiny--weight: 500;">docker cd /Users/amir/Desktop/tiny--weight: 500;">docker cd /Users/amir/Desktop/tiny--weight: 500;">docker No such file or directory No such file or directory No such file or directory limactl shell tiny--weight: 500;">docker pwd limactl shell tiny--weight: 500;">docker ls -la /Users limactl shell tiny--weight: 500;">docker ls -la /Users/amir/Desktop limactl shell tiny--weight: 500;">docker pwd limactl shell tiny--weight: 500;">docker ls -la /Users limactl shell tiny--weight: 500;">docker ls -la /Users/amir/Desktop limactl shell tiny--weight: 500;">docker pwd limactl shell tiny--weight: 500;">docker ls -la /Users limactl shell tiny--weight: 500;">docker ls -la /Users/amir/Desktop -weight: 500;">git clone <your-repo-url> cd tiny--weight: 500;">docker -weight: 500;">git clone <your-repo-url> cd tiny--weight: 500;">docker -weight: 500;">git clone <your-repo-url> cd tiny--weight: 500;">docker ./tiny--weight: 500;">docker-go ./bin/tiny--weight: 500;">docker-go-linux-amd64 ./bin/tiny--weight: 500;">docker-go-linux-arm64 ./tiny--weight: 500;">docker-go ./bin/tiny--weight: 500;">docker-go-linux-amd64 ./bin/tiny--weight: 500;">docker-go-linux-arm64 ./tiny--weight: 500;">docker-go ./bin/tiny--weight: 500;">docker-go-linux-amd64 ./bin/tiny--weight: 500;">docker-go-linux-arm64 macOS binary: command parsing platform detection Lima dispatching Linux binary: namespaces cgroups chroot mount setup networking process lifecycle macOS binary: command parsing platform detection Lima dispatching Linux binary: namespaces cgroups chroot mount setup networking process lifecycle macOS binary: command parsing platform detection Lima dispatching Linux binary: namespaces cgroups chroot mount setup networking process lifecycle limactl is not installed Lima instance does not exist Lima instance is not running Linux binary is missing Rootfs is not accessible inside Lima limactl is not installed Lima instance does not exist Lima instance is not running Linux binary is missing Rootfs is not accessible inside Lima limactl is not installed Lima instance does not exist Lima instance is not running Linux binary is missing Rootfs is not accessible inside Lima Is limactl installed? Does the Lima instance exist? Is the Lima instance running? Does the Linux binary exist? Is the Linux binary accessible inside Lima? Is the rootfs path accessible inside Lima? Is limactl installed? Does the Lima instance exist? Is the Lima instance running? Does the Linux binary exist? Is the Linux binary accessible inside Lima? Is the rootfs path accessible inside Lima? Is limactl installed? Does the Lima instance exist? Is the Lima instance running? Does the Linux binary exist? Is the Linux binary accessible inside Lima? Is the rootfs path accessible inside Lima? Linux binary not found at "./bin/tiny--weight: 500;">docker-go-linux-amd64"; build it first and share it with Lima. Linux binary not found at "./bin/tiny--weight: 500;">docker-go-linux-amd64"; build it first and share it with Lima. Linux binary not found at "./bin/tiny--weight: 500;">docker-go-linux-amd64"; build it first and share it with Lima. rootfs "./rootfs/alpine" is not accessible inside Lima; ensure the workspace is shared with the VM. rootfs "./rootfs/alpine" is not accessible inside Lima; ensure the workspace is shared with the VM. rootfs "./rootfs/alpine" is not accessible inside Lima; ensure the workspace is shared with the VM. -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/sh cat /etc/os-release cat /etc/os-release cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.23.4 NAME="Alpine Linux" ID=alpine VERSION_ID=3.23.4 NAME="Alpine Linux" ID=alpine VERSION_ID=3.23.4 -weight: 600;">sudo ./tiny--weight: 500;">docker-go ps -weight: 600;">sudo ./tiny--weight: 500;">docker-go ps -weight: 600;">sudo ./tiny--weight: 500;">docker-go ps ID STATUS PID CREATED COMMAND ab12cd34ef56 running 1234 2026-05-17 12:30:45 /bin/sh ID STATUS PID CREATED COMMAND ab12cd34ef56 running 1234 2026-05-17 12:30:45 /bin/sh ID STATUS PID CREATED COMMAND ab12cd34ef56 running 1234 2026-05-17 12:30:45 /bin/sh ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo "hello from linux" ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo "hello from linux" ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo "hello from linux" limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/echo "hello from linux" limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/echo "hello from linux" limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/echo "hello from linux" hello from linux hello from linux hello from linux -weight: 600;">sudo ./tiny--weight: 500;">docker-go run \ --memory 128m \ --rootfs ./rootfs/alpine \ /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run \ --memory 128m \ --rootfs ./rootfs/alpine \ /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run \ --memory 128m \ --rootfs ./rootfs/alpine \ /bin/sh cat /sys/fs/cgroup/memory.max cat /sys/fs/cgroup/memory.max cat /sys/fs/cgroup/memory.max -weight: 500;">docker run --memory 128m alpine -weight: 500;">docker run --memory 128m alpine -weight: 500;">docker run --memory 128m alpine Docker exposes a nice option. The Linux kernel enforces the limit through cgroups. Docker exposes a nice option. The Linux kernel enforces the limit through cgroups. Docker exposes a nice option. The Linux kernel enforces the limit through cgroups. host bridge | veth pair | container network namespace host bridge | veth pair | container network namespace host bridge | veth pair | container network namespace -weight: 600;">sudo ./tiny--weight: 500;">docker-go run \ --net isolated \ --rootfs ./rootfs/alpine \ /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run \ --net isolated \ --rootfs ./rootfs/alpine \ /bin/sh -weight: 600;">sudo ./tiny--weight: 500;">docker-go run \ --net isolated \ --rootfs ./rootfs/alpine \ /bin/sh ip addr ip route ip addr ip route ip addr ip route -weight: 500;">docker run nginx -weight: 500;">docker run nginx -weight: 500;">docker run nginx network namespaces veth pairs bridges routes iptables NAT network namespaces veth pairs bridges routes iptables NAT network namespaces veth pairs bridges routes iptables NAT Before: / After: ./rootfs/alpine becomes / Before: / After: ./rootfs/alpine becomes / Before: / After: ./rootfs/alpine becomes / pivot_root overlay filesystems image layers OCI runtime spec capability dropping seccomp AppArmor SELinux user namespaces read-only mounts masked paths pivot_root overlay filesystems image layers OCI runtime spec capability dropping seccomp AppArmor SELinux user namespaces read-only mounts masked paths pivot_root overlay filesystems image layers OCI runtime spec capability dropping seccomp AppArmor SELinux user namespaces read-only mounts masked paths This is a tiny educational runtime. It demonstrates some basic container building blocks. It is not a production replacement for Docker, containerd, or runc. This is a tiny educational runtime. It demonstrates some basic container building blocks. It is not a production replacement for Docker, containerd, or runc. This is a tiny educational runtime. It demonstrates some basic container building blocks. It is not a production replacement for Docker, containerd, or runc. -weight: 500;">docker build -weight: 500;">docker run -weight: 500;">docker ps -weight: 500;">docker logs -weight: 500;">docker -weight: 500;">stop -weight: 500;">docker build -weight: 500;">docker run -weight: 500;">docker ps -weight: 500;">docker logs -weight: 500;">docker -weight: 500;">stop -weight: 500;">docker build -weight: 500;">docker run -weight: 500;">docker ps -weight: 500;">docker logs -weight: 500;">docker -weight: 500;">stop Docker CLI Docker daemon containerd runc Linux namespaces Linux cgroups root filesystem network namespace mount namespace process lifecycle Docker CLI Docker daemon containerd runc Linux namespaces Linux cgroups root filesystem network namespace mount namespace process lifecycle Docker CLI Docker daemon containerd runc Linux namespaces Linux cgroups root filesystem network namespace mount namespace process lifecycle -weight: 500;">docker run alpine echo hello -weight: 500;">docker run alpine echo hello -weight: 500;">docker run alpine echo hello resolving the image downloading layers preparing the root filesystem creating namespaces configuring cgroups setting up mounts configuring networking starting the process attaching stdio tracking metadata collecting exit -weight: 500;">status cleaning up resources resolving the image downloading layers preparing the root filesystem creating namespaces configuring cgroups setting up mounts configuring networking starting the process attaching stdio tracking metadata collecting exit -weight: 500;">status cleaning up resources resolving the image downloading layers preparing the root filesystem creating namespaces configuring cgroups setting up mounts configuring networking starting the process attaching stdio tracking metadata collecting exit -weight: 500;">status cleaning up resources Docker CLI Docker Compose image management container lifecycle management volume support networking developer-friendly tooling Docker CLI Docker Compose image management container lifecycle management volume support networking developer-friendly tooling Docker CLI Docker Compose image management container lifecycle management volume support networking developer-friendly tooling a Linux VM direct access to Linux tools a clean environment for experiments less abstraction around Docker itself a Linux VM direct access to Linux tools a clean environment for experiments less abstraction around Docker itself a Linux VM direct access to Linux tools a clean environment for experiments less abstraction around Docker itself Use Docker Desktop when your goal is to run and ship applications. Use Lima when your goal is to understand or control the Linux environment. Use Docker Desktop when your goal is to run and ship applications. Use Lima when your goal is to understand or control the Linux environment. Use Docker Desktop when your goal is to run and ship applications. Use Lima when your goal is to understand or control the Linux environment. /bin/echo hello /bin/echo hello /bin/echo hello time -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo hello time -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo hello time -weight: 600;">sudo ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo hello hello real 0m0.045s user 0m0.008s sys 0m0.020s hello real 0m0.045s user 0m0.008s sys 0m0.020s hello real 0m0.045s user 0m0.008s sys 0m0.020s time ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo hello time ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo hello time ./tiny--weight: 500;">docker-go run --rootfs ./rootfs/alpine /bin/echo hello limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/echo hello limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/echo hello limactl shell tiny--weight: 500;">docker -weight: 600;">sudo ./bin/tiny--weight: 500;">docker-go-linux-amd64 \ run \ --rootfs ./rootfs/alpine \ /bin/echo hello hello real 0m0.180s user 0m0.020s sys 0m0.030s hello real 0m0.180s user 0m0.020s sys 0m0.030s hello real 0m0.180s user 0m0.020s sys 0m0.030s Direct inside Lima: ~45 ms macOS through Lima: ~180 ms Extra routing overhead: ~135 ms Direct inside Lima: ~45 ms macOS through Lima: ~180 ms Extra routing overhead: ~135 ms Direct inside Lima: ~45 ms macOS through Lima: ~180 ms Extra routing overhead: ~135 ms For a nice macOS developer experience, routing through Lima is acceptable. For tight benchmark loops, run directly inside the VM. For production-grade runtimes, this approach is educational, not final. For a nice macOS developer experience, routing through Lima is acceptable. For tight benchmark loops, run directly inside the VM. For production-grade runtimes, this approach is educational, not final. For a nice macOS developer experience, routing through Lima is acceptable. For tight benchmark loops, run directly inside the VM. For production-grade runtimes, this approach is educational, not final. use pivot_root instead of only chroot improve cgroup v2 handling support better cleanup add more robust metadata storage improve log streaming support better TTY handling add user namespace support drop Linux capabilities add seccomp profiles use pivot_root instead of only chroot improve cgroup v2 handling support better cleanup add more robust metadata storage improve log streaming support better TTY handling add user namespace support drop Linux capabilities add seccomp profiles use pivot_root instead of only chroot improve cgroup v2 handling support better cleanup add more robust metadata storage improve log streaming support better TTY handling add user namespace support drop Linux capabilities add seccomp profiles detect Lima architecture automatically choose the correct Linux binary automatically improve Lima instance setup validate shared paths more clearly provide a bootstrap command for macOS users make error messages more actionable detect Lima architecture automatically choose the correct Linux binary automatically improve Lima instance setup validate shared paths more clearly provide a bootstrap command for macOS users make error messages more actionable detect Lima architecture automatically choose the correct Linux binary automatically improve Lima instance setup validate shared paths more clearly provide a bootstrap command for macOS users make error messages more actionable tiny--weight: 500;">docker-go setup lima tiny--weight: 500;">docker-go setup lima tiny--weight: 500;">docker-go setup lima checking limactl creating the Lima instance building the Linux binary preparing the rootfs validating mounts testing a hello-world container checking limactl creating the Lima instance building the Linux binary preparing the rootfs validating mounts testing a hello-world container checking limactl creating the Lima instance building the Linux binary preparing the rootfs validating mounts testing a hello-world container macOS is my workstation. Lima gives me a Linux VM. The Linux VM gives me namespaces and cgroups. My Go runtime uses those Linux features. The rootfs gives the process its filesystem. macOS is my workstation. Lima gives me a Linux VM. The Linux VM gives me namespaces and cgroups. My Go runtime uses those Linux features. The rootfs gives the process its filesystem. macOS is my workstation. Lima gives me a Linux VM. The Linux VM gives me namespaces and cgroups. My Go runtime uses those Linux features. The rootfs gives the process its filesystem. -weight: 500;">docker run alpine echo hello -weight: 500;">docker run alpine echo hello -weight: 500;">docker run alpine echo hello - Linux namespaces - mount isolation - process isolation - bridge networking - iptables/NAT