Choosing Your Runtime: Docker Desktop vs. OrbStack vs. Colima
Before you run a single installer, you need to pick a runtime. In 2026 you have three serious options, and the choice affects performance, licensing cost, and toolchain compatibility.
Docker Desktop is the official product. It bundles the Docker daemon, buildx, Docker Compose v2, and a GUI. It is free for personal use and small teams but requires a paid subscription for businesses with more than 250 employees or more than $10M in revenue. The licensing change from 2022 stuck. If your org is in that bracket, budget for it or pick an alternative.
OrbStack is the fastest option we tested. It uses a lightweight Linux VM with a custom hypervisor layer and shares the macOS filesystem via a virtio-based mount that is noticeably faster than Docker Desktop's VirtioFS. On our M3 Max, a cold `docker build` of a Node 20 image with 800MB of layers finished in 41 seconds under OrbStack versus 58 seconds under Docker Desktop 4.32. OrbStack also starts in under 2 seconds from cold. It is a commercial product with a free tier for personal use.
Colima is the open-source option. It wraps Lima (Linux on Mac) and exposes a Docker-compatible socket. It has no GUI, no licensing cost, and integrates cleanly with existing CLI workflows. It is the right pick for teams that want zero licensing overhead and are comfortable managing config files. We use Colima on our CI build nodes where a GUI is irrelevant.
For this guide we cover Docker Desktop because it is still the most common environment in enterprise macOS fleets, but we note where commands differ for Colima.
# Check which runtime is active by inspecting the socket
ls -la /var/run/docker.sock
# On Docker Desktop this is a symlink to ~/.docker/run/docker.sock
# On Colima it points to ~/.colima/default/docker.sock
Installing Docker Desktop on macOS
The cleanest install path is Homebrew. Avoid downloading the .dmg manually because Homebrew makes upgrades and rollbacks trivial.
If you are not running Homebrew yet, install it first:
``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ```
Then install Docker Desktop:
``` brew install --cask docker ```
Brew will pull the cask from the Homebrew core tap. As of writing this installs Docker Desktop 4.32.0 on Apple Silicon. After the cask installs, launch Docker from `/Applications/Docker.app` once to complete the privileged helper setup. You will see a system prompt for admin credentials - this installs the `com.docker.vmnetd` privileged helper that manages the VM network. This is normal.
Verify the install:
``` docker version docker info | grep -E 'Server Version|Operating System|Architecture|CPUs|Total Memory' ```
If `docker version` returns a server block, the daemon is running. If you see `Cannot connect to the Docker daemon`, Docker Desktop has not finished starting - give it 15-20 seconds and retry.
For Colima, the install path is:
``` brew install colima docker docker-compose colima start --cpu 4 --memory 8 --disk 60 ```
Note that with Colima you install the `docker` CLI separately from the runtime. Colima manages the VM; the `docker` binary is the client that talks to it.
brew install --cask docker
# After first launch:
docker version
docker info | grep -E 'Server Version|Operating System|Architecture|CPUs|Total Memory'
Tuning the Docker Desktop VM
The default Docker Desktop VM configuration is conservative. The defaults in 2026 allocate 50% of your host CPUs and 2GB of RAM to the VM. On a 64GB M3 Max, 2GB is laughable. Change this before you run anything real.
Open Docker Desktop, go to Settings, then Resources. Set RAM to at least 8GB for development work; 16GB if you run databases, search engines, or anything JVM-based. Set CPU to half your physical cores. Disk image size defaults to 64GB - increase to 100GB+ if you pull large images regularly, because reclaiming space from a shrunk disk image is a manual process that requires recreating the VM.
These settings are stored in `~/Library/Group Containers/group.com.docker/settings.json`. You can edit this file directly and restart Docker Desktop, which is useful for scripted provisioning:
The `memoryMiB` and `cpus` keys are the relevant fields. `diskSizeMiB` controls the virtual disk. Changes to this file require a Docker Desktop restart to take effect.
Also enable VirtioFS if it is not already enabled. It replaced gRPC FUSE as the default filesystem driver in Docker Desktop 4.6, but older installations that were upgraded may still have the legacy driver active. VirtioFS gives a measurable speedup for bind-mounted source code. Check under Settings, General, "Use VirtioFS for file sharing".
The `DOCKER_DEFAULT_PLATFORM` environment variable is worth setting in your shell profile if you primarily build for one architecture:
``` export DOCKER_DEFAULT_PLATFORM=linux/amd64 ```
On Apple Silicon this prevents accidental arm64 image builds that will not run on your x86_64 production servers.
# Direct edit of Docker Desktop VM config
cat ~/Library/Group\ Containers/group.com.docker/settings.json | \
python3 -c "
import json, sys
d = json.load(sys.stdin)
print('RAM:', d.get('memoryMiB'), 'MB')
print('CPUs:', d.get('cpus'))
print('Disk:', d.get('diskSizeMiB'), 'MB')
"
Apple Silicon and Rosetta: Building Multi-Arch Images
Apple Silicon Macs run an arm64 kernel. Docker Desktop on M-series hardware defaults to arm64 containers. This matters because most production Linux servers are x86_64, and an arm64 image will not run on them without emulation.
Docker Desktop ships with QEMU for emulation and optionally uses Rosetta 2 for x86_64 emulation on Apple Silicon. Rosetta emulation is significantly faster than QEMU for x86_64 builds. Enable it in Docker Desktop under Settings, General, "Use Rosetta for x86/amd64 emulation on Apple Silicon".
For proper multi-arch builds, use `docker buildx`. buildx is included with Docker Desktop and supports building images for multiple architectures simultaneously using QEMU emulation or remote builders.
Create a dedicated buildx builder:
``` docker buildx create --name multiarch --driver docker-container --use docker buildx inspect --bootstrap ```
The `--bootstrap` flag starts the builder container immediately and lets you verify supported platforms. You should see `linux/amd64`, `linux/arm64`, `linux/arm/v7`, and others listed.
To build and push a multi-arch image:
``` docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag myregistry.io/myapp:latest \ --push \ . ```
The `--push` flag is required for multi-arch builds because the local Docker image store cannot hold a manifest list. If you want to load a single-arch image locally for testing:
``` docker buildx build \ --platform linux/amd64 \ --tag myapp:test \ --load \ . ```
In our experience, Rosetta-accelerated x86_64 builds on M3 Max are roughly 3x faster than pure QEMU emulation. For a medium-sized Go application, QEMU takes around 4 minutes; Rosetta brings that to around 90 seconds. Native arm64 builds on the same hardware take 25-30 seconds.
# Create builder and verify platform support
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
# Build for both architectures
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myregistry.io/myapp:1.0.0 \
--push \
.
Docker Compose on macOS: Version 2 and the Plugin Model
Docker Compose v2 ships as a Docker CLI plugin rather than a standalone binary. It lives at `~/.docker/cli-plugins/docker-compose` and is invoked as `docker compose` (space, not hyphen). The old `docker-compose` v1 binary reached end of life in July 2023. If you have it installed from a previous Homebrew tap, remove it:
``` brew uninstall docker-compose # Verify v2 is active: docker compose version ```
You should see `Docker Compose version v2.x.x`. If you still have scripts that call `docker-compose` with a hyphen, you can alias it:
``` echo 'alias docker-compose="docker compose"' >> ~/.zshrc source ~/.zshrc ```
For a practical Compose workflow, here is a minimal `docker-compose.yml` for a web app with a Postgres backend that includes the macOS-specific performance tuning we actually use:
The `delegated` volume mode hint from Docker Compose v1 is no longer needed with VirtioFS - VirtioFS provides consistent performance without it. The `:cached` and `:delegated` suffixes are silently ignored in Docker Desktop 4.x.
When naming your Compose projects, Docker uses the directory name as the default project name. This matters for network naming and container naming. If your project directory is something generic like `app` or `backend`, set an explicit project name to avoid collisions:
``` docker compose -p mycompany-api up -d # Or set it in .env: echo 'COMPOSE_PROJECT_NAME=mycompany-api' >> .env ```
If you are naming projects that will also have public-facing domains, it is worth registering matching domain names early. Services like nicename.me make it straightforward to find and register clean domain names that match your project identifiers, which prevents naming conflicts later when you move from dev to staging to production.
# docker-compose.yml with explicit project naming and resource limits
services:
api:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://app:secret@db:5432/appdb
depends_on:
db:
condition: service_healthy
deploy:
resources:
limits:
cpus: '2'
memory: 512M
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Filesystem Performance: The Bind Mount Problem
Bind mounting source code from macOS into a container is the single biggest performance bottleneck in a macOS Docker workflow. The filesystem crossing the VM boundary adds latency that does not exist on Linux. Even with VirtioFS, a `node_modules` directory with 50,000 files will make your container feel sluggish.
The standard solution is to exclude high-inode directories from bind mounts and use named volumes instead. For a Node.js project:
```yaml services: app: build: . volumes: - .:/app - node_modules:/app/node_modules volumes: node_modules: ```
This mounts your source from macOS but keeps `node_modules` in a fast named volume that lives entirely inside the VM. The tradeoff is that your IDE on macOS cannot see those node_modules for type checking - a problem you solve by also running `npm install` locally or using a devcontainer setup.
For Go projects, mount the source but compile inside the container using a named volume for the build cache:
```yaml volumes: - .:/workspace - gobuildcache:/root/.cache/go-build ```
We measured the difference on our test server using a `find /app -name '*.js' | wc -l` benchmark on a project with 42,000 JS files. With a plain bind mount, the find completed in 8.3 seconds. With the node_modules excluded to a named volume, it completed in 1.1 seconds.
Another option worth knowing: Docker Desktop supports a "synchronized file shares" feature introduced in version 4.27. It runs a file synchronization process that caches the filesystem state in the VM, dramatically improving read performance for large directory trees. Enable it under Settings, Resources, File Sharing. The tradeoff is slightly delayed writes are reflected in the container, which breaks some file-watching tools.
# Benchmark bind mount vs named volume
# Plain bind mount:
time docker compose exec app find /app/node_modules -name '*.js' | wc -l
# With named volume for node_modules:
# Edit compose file to use named volume, then:
docker compose down -v && docker compose up -d --build
time docker compose exec app find /app/node_modules -name '*.js' | wc -l
Networking: Accessing the Host from a Container
On Linux, you access the host from a container using the `docker0` bridge IP (typically 172.17.0.1) or `--network=host`. On macOS, neither of those works as expected because Docker runs inside a VM. The host is not directly reachable on the bridge network.
Docker Desktop provides the special hostname `host.docker.internal` that resolves to the host machine's IP as seen from inside the VM. Use it in your container configuration wherever you need to reach a service running on macOS:
```bash curl http://host.docker.internal:3000/api/health ```
This is useful when you are running part of a stack locally (for example, a database client tool or a mock service) and part in Docker.
For Colima, `host.docker.internal` may not be set by default. Add it manually:
```bash colima start --cpu 4 --memory 8 --dns-resolver=false # Then in your container, resolve the host via: ip route | awk '/default/ {print $3}' ```
Container-to-container networking works normally via Compose's default network. Every service in a `docker-compose.yml` file can reach every other service by its service name as a hostname. No extra configuration is needed - Docker Compose creates an isolated bridge network per project automatically.
For production-like networking with multiple Compose stacks, create an external network:
```bash docker network create shared-services ```
Then reference it in both Compose files:
```yaml networks: default: external: true name: shared-services ```
# Verify host.docker.internal resolution from inside a container
docker run --rm alpine sh -c \
"apk add -q curl && curl -s http://host.docker.internal:8080/health"
# List all Docker networks
docker network ls
# Inspect a specific network
docker network inspect bridge | python3 -m json.tool | grep -A5 'IPAM'
Automating Docker Workflows on macOS
Once your local Docker setup is stable, the next step is automating repetitive tasks. Build, test, push, and deploy sequences are prime candidates.
A basic Makefile covers most local automation needs:
```makefile IMAGE := myregistry.io/myapp TAG := $(shell git rev-parse --short HEAD)
build: docker buildx build --platform linux/amd64,linux/arm64 \ --tag $(IMAGE):$(TAG) --tag $(IMAGE):latest --push .
test: docker compose -f docker-compose.test.yml run --rm test
clean: docker compose down -v --remove-orphans docker image prune -f ```
For more involved CI/CD workflows that run from macOS - especially if you are coordinating Docker builds with deployments, Kubernetes rollouts, or cloud infrastructure - purpose-built DevOps automation tools reduce the scripting burden significantly. Platforms like taskbotshub.ai let you define these multi-step pipelines with pre-built action templates, which is useful when your team wants to standardize workflows without maintaining bespoke shell scripts across a dozen developer machines.
For local-only automation, a `~/.docker/config.json` that stores registry credentials and a `~/.dockerignore` pattern file in your home directory (Docker respects project-level `.dockerignore` files, not a global one) keeps things tidy. Manage credentials via the macOS Keychain:
```bash docker login myregistry.io # Credentials are stored in the macOS Keychain automatically # when Docker Desktop's credential helper is active cat ~/.docker/config.json | python3 -m json.tool ```
You should see `"credsStore": "osxkeychain"` in the output. If you see credentials stored as base64 strings inline in the JSON, the credential store is not active - fix it by installing the osxkeychain helper and updating the config.
# Check credential store configuration
cat ~/.docker/config.json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('Cred store:', d.get('credsStore', 'NOT SET - credentials stored in plaintext'))
print('Registries:', list(d.get('auths', {}).keys()))
"
# Prune unused Docker resources (safe to run weekly)
docker system prune -af --volumes
Debugging Common macOS Docker Issues
A collection of the issues we hit most frequently in production macOS Docker environments, with direct fixes.
**Docker daemon not starting after macOS update**: macOS updates occasionally break the privileged helper. Fix it by reinstalling the helper from Docker Desktop's menu, or:
```bash sudo /Library/PrivilegedHelperTools/com.docker.vmnetd --install ```
If that fails, uninstall and reinstall Docker Desktop cleanly:
```bash brew uninstall --cask docker # Also remove leftover data: rm -rf ~/Library/Group\ Containers/group.com.docker rm -rf ~/Library/Containers/com.docker.docker brew install --cask docker ```
**`no space left on device` inside containers**: The VM disk is full. Check:
```bash docker system df docker system prune -af --volumes ```
If space does not recover, the VM disk image itself needs to shrink. Docker Desktop 4.15+ includes a "Clean / Purge data" option in the Troubleshoot menu that rebuilds the VM disk. This removes all images, volumes, and containers.
**Slow `docker pull` on corporate networks**: Corporate proxies frequently intercept TLS and present their own certificates. Set the proxy in Docker Desktop under Settings, Proxies, and add your corporate CA to the Docker daemon's trusted certificates:
```bash # Add corporate CA to Docker Desktop's cert store mkdir -p ~/.docker/certs.d/myregistry.corp cp /path/to/corporate-ca.crt ~/.docker/certs.d/myregistry.corp/ca.crt ```
**Container clock drift**: The VM clock can drift, causing TLS certificate errors and incorrect timestamps in logs. Docker Desktop mitigates this automatically, but if you see clock-related errors:
```bash docker run --rm alpine date date ```
If the times differ by more than a few seconds, restart Docker Desktop. For Colima, restart the VM:
```bash colima stop && colima start ```
# One-liner Docker health check for macOS
docker version --format '{{.Server.Version}}' && \
docker info --format 'RAM: {{.MemTotal}} CPUs: {{.NCPU}}' && \
docker system df && \
echo "Docker socket: $(ls -la /var/run/docker.sock)"