04 — Docker, OrbStack, Colima, and Dev Containers on Apple Silicon (2026)

Why containers matter in your roadmap. Phase 4 (Build/Test/Tooling) and Phase 6 (Production Systems) both require a reproducible Linux C++ build environment that matches CI. macOS-native builds are useful but the industry ships Linux binaries. Containers close the gap without a second machine.


§1. The 2026 landscape on Apple Silicon (verified July 2026)

Tool

Type

License

Native ARM64?

RAM idle

Speed vs Docker Desktop

Verdict

Docker Desktop

GUI + engine

Free personal / paid biz

Yes

~2 GB

1.0× baseline

Default; works but heavy

OrbStack

GUI + engine

Paid (~$8/mo personal)

Yes

~200 MB

~1.7× faster boot, ~1.7× more power-efficient

Recommended if you can pay

Colima

CLI only

Free, OSS

Yes

~500 MB

~1.1×

Best free alternative

Apple container

CLI, WWDC 2025+

Free (Apple)

Yes

~100 MB

Fast but limited

Too new for daily use

Rancher Desktop

GUI

Free

Yes

~1 GB

~1.0×

Fine, k8s-focused

Lima

CLI, VM-only

Free

Yes

Variable

N/A (no Docker default)

Skip unless you’re a Lima user

Practical decision for Raghul: if the ~₹700/month for OrbStack is justifiable, that’s the best time-saver. If not, Colima is a fully capable free path. Docker Desktop’s free personal tier also works and is the most vanilla choice.

Do NOT pick Apple’s native container yet — as of July 2026 it lacks some docker-compose features and Dev Containers integration is incomplete.



§3. Alternative: Colima install (free path, 5 min)

brew install colima docker docker-buildx docker-compose

# Start the Colima VM (Ubuntu 24.04 by default in 2026)
colima start --arch aarch64 --cpu 4 --memory 8 --disk 60

# Confirm the docker client sees Colima's VM
docker context ls
# expected: colima (current)

docker run --rm hello-world

Notes:

  • --memory 8 allocates 8 GB to the Linux VM. On a 16 GB Mac, do NOT exceed this. On a 32 GB M-Pro/Max, --memory 16 is comfortable.

  • Colima persists across reboots via colima start. Add alias k='colima start && docker ps' to ~/.zshrc if you use it daily.

  • Colima’s Docker plugin support: buildx works, compose works, but networking is slightly less polished than OrbStack for cross-container DNS.


§4. Minimal Dockerfile for a Linux C++20/23 build env

Put this at the root of any project where you want CI parity:

# Dockerfile — C++20/23 dev environment matching modern CI (July 2026 baseline)
# Ubuntu 24.04 LTS (Noble Numbat) is the current baseline; 26.04 (LTS Apr 2026)
# is also viable but had late-2025 apt-mirror quirks — stick with 24.04 through
# Phase 6 unless you have a reason.

FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive \
    TZ=Asia/Kolkata \
    LANG=en_US.UTF-8 \
    LC_ALL=en_US.UTF-8

# LLVM apt repository (Kitware LLVM builds are more current than Ubuntu's)
RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates curl gnupg lsb-release software-properties-common \
        wget xz-utils \
    && rm -rf /var/lib/apt/lists/*

# Add LLVM apt repo (llvm.org/apt) — pins to LLVM 20 as of July 2026 baseline.
# Bump to 21 when Ubuntu 24.04 packages catch up.
RUN wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
        | gpg --dearmor -o /usr/share/keyrings/llvm.gpg \
    && echo "deb [signed-by=/usr/share/keyrings/llvm.gpg] http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" \
        > /etc/apt/sources.list.d/llvm.list

# Kitware CMake apt repo (more current than distro-cmake)
RUN wget -qO- https://apt.kitware.com/keys/kitware-archive-latest.asc \
        | gpg --dearmor -o /usr/share/keyrings/kitware.gpg \
    && echo "deb [signed-by=/usr/share/keyrings/kitware.gpg] https://apt.kitware.com/ubuntu/ noble main" \
        > /etc/apt/sources.list.d/kitware.list

RUN apt-get update && apt-get install -y --no-install-recommends \
        clang-20 clang-tidy-20 clang-format-20 clangd-20 lld-20 lldb-20 \
        libc++-20-dev libc++abi-20-dev \
        cmake ninja-build \
        git gh \
        python3 python3-pip python3-venv pipx \
        pkg-config \
        gdb valgrind strace ltrace \
        linux-tools-common \
        ripgrep fd-find fzf jq bat \
        zsh sudo \
    && rm -rf /var/lib/apt/lists/*

# Symlink LLVM-versioned binaries to unversioned names
RUN for tool in clang clang++ clang-tidy clang-format clangd lld lldb; do \
      ln -sf /usr/bin/${tool}-20 /usr/local/bin/${tool}; \
    done

# Install Conan 2 via pipx (isolated, upgradeable)
RUN pipx install conan --global \
    && conan --version

# Install uv (Astral's fast Python manager) — Phase 5
RUN curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --no-modify-path \
    && mv /root/.local/bin/uv /usr/local/bin/uv \
    && uv --version

# Create a non-root user matching macOS UID for bind-mount friendliness
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN groupadd -g ${GROUP_ID} dev \
    && useradd -m -u ${USER_ID} -g ${GROUP_ID} -s /bin/zsh dev \
    && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

USER dev
WORKDIR /workspace

# Force libc++ for parity with brew LLVM on macOS
ENV CC=/usr/local/bin/clang \
    CXX=/usr/local/bin/clang++ \
    CXXFLAGS="-stdlib=libc++" \
    LDFLAGS="-stdlib=libc++ -lc++abi"

CMD ["/bin/zsh"]

Build and run:

docker build -t cxx-dev:2026 .
docker run --rm -it -v "$(pwd):/workspace" cxx-dev:2026
# inside container:
clang++ --version
# expected: Ubuntu clang version 20.x.x
cmake --version
# expected: cmake version 3.31.x or newer (Kitware apt)

§5. VSCode Dev Containers — the reproducible-env workflow

Extension: ms-vscode-remote.remote-containers. Install:

code --install-extension ms-vscode-remote.remote-containers

File: .devcontainer/devcontainer.json at repo root.

{
  "name": "C++20/23 Dev (Ubuntu 24.04, LLVM 20)",
  "build": {
    "dockerfile": "../Dockerfile",
    "context": ".."
  },
  "workspaceFolder": "/workspace",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
  "remoteUser": "dev",
  "customizations": {
    "vscode": {
      "extensions": [
        "llvm-vs-code-extensions.vscode-clangd",
        "vadimcn.vscode-lldb",
        "ms-vscode.cmake-tools",
        "twxs.cmake",
        "usernamehw.errorlens",
        "eamodio.gitlens",
        "jeff-hykin.better-cpp-syntax"
      ],
      "settings": {
        "clangd.path": "/usr/local/bin/clangd",
        "cmake.generator": "Ninja",
        "cmake.buildDirectory": "${workspaceFolder}/build-linux"
      }
    }
  },
  "features": {
    "ghcr.io/devcontainers/features/common-utils:2": {
      "installZsh": true,
      "configureZshAsDefaultShell": true,
      "installOhMyZsh": false,
      "upgradePackages": false
    }
  },
  "postCreateCommand": "cmake --version && clang++ --version && conan --version",
  "mounts": [
    "source=cxx-dev-conan-cache,target=/home/dev/.conan2,type=volume",
    "source=cxx-dev-clangd-cache,target=/home/dev/.cache/clangd,type=volume"
  ]
}

Usage:

  1. Open the repo in VSCode: code ..

  2. Cmd+Shift+PDev Containers: Reopen in Container.

  3. VSCode builds the image (first time: ~5 min), starts the container, attaches its language servers inside.

  4. All your VSCode extensions, .clangd, .clang-format, .clang-tidy now run against the Linux toolchain instead of macOS.

  5. Terminal inside VSCode is now Ubuntu bash/zsh, uname -a shows Linux.

Why this is the killer workflow. You develop C++ against the same toolchain as GitHub Actions / any Linux CI. When the CI is red and your Mac is green, you check the container. If the container is red, the fault is in your code, not your environment.

The build-linux/ output directory is separate from your macOS build/, so both toolchains coexist without stomping each other.

The two named volumes (cxx-dev-conan-cache, cxx-dev-clangd-cache) persist across container rebuilds — Conan package cache and the clangd index survive. Without these you’d re-download Boost or Eigen every rebuild.


§6. docker compose for multi-service dev (Phase 6)

For Phase 6 (production systems), you’ll want a Postgres or Redis alongside your C++ service:

# docker-compose.yml
version: '3.9'

services:
  cxx-dev:
    build: .
    image: cxx-dev:2026
    volumes:
      - .:/workspace
      - cxx-dev-conan-cache:/home/dev/.conan2
    working_dir: /workspace
    tty: true
    stdin_open: true
    depends_on: [postgres, redis]

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: dev
    ports: ["5432:5432"]
    volumes: [pgdata:/var/lib/postgresql/data]

  redis:
    image: redis:8-alpine
    ports: ["6379:6379"]

volumes:
  cxx-dev-conan-cache:
  pgdata:

Usage:

docker compose up -d postgres redis
docker compose run --rm cxx-dev
# inside container: cmake -B build && cmake --build build && ./build/service

§7. Cross-platform builds (ARM64 macOS → x86_64 Linux)

Two paths:

Emulation (slow but simple). Docker + QEMU can build x86_64 images on an ARM host:

docker buildx build --platform linux/amd64 -t cxx-dev:2026-x86 .

This is 3–5× slower than native ARM builds. Acceptable for one-off CI parity checks. Do not use for daily dev.

Native (fast, correct). For real x86_64 testing, use a cheap Linux VM in the cloud. Hetzner (hetzner.com/cloud) offers x86_64 dedicated CPU boxes for €4–8/month. Provision one, ssh in, docker there. Faster than local emulation and matches production.

For your 13-month roadmap: emulation is fine through Phase 5. If you hit performance-testing needs in Phase 6, spin up Hetzner.


§8. GitHub Actions parity — what to match

Your GitHub Actions matrix (documented in 05_phase_4_build_test_tooling/) should mirror this container. Concrete parity check:

# .github/workflows/ci.yml — snippet
jobs:
  linux-clang:
    runs-on: ubuntu-24.04
    container: cxx-dev:2026   # if you push the image to ghcr.io
    steps:
      - uses: actions/checkout@v4
      - run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
      - run: cmake --build build
      - run: ctest --test-dir build --output-on-failure

Push your Dockerfile-built image to GitHub Container Registry (ghcr.io/<user>/cxx-dev:2026) so CI pulls the exact same image you develop against. Local + CI in perfect sync. This is the reproducibility guarantee that eliminates 90% of “works on my machine”.


§9. India-specific notes

  • Image pulls from Docker Hub / GHCR are slow from India. ~2–5 MB/s is typical on Airtel/Jio fiber. First-time Ubuntu 24.04 + LLVM 20 pulls will take ~10–15 minutes. Do it once, cache the layers.

  • Docker Hub rate limits hit unauthenticated pulls (100/6 hrs from a single IP). docker login docker.io with a free Docker Hub account fixes this — first thing to do after installing.

  • OrbStack / Colima do not require Docker Hub login. Once you have your image built locally, no further pulls needed for daily dev.

  • Corp VPN. If Zoho VPN blocks GHCR or Docker Hub, disconnect the VPN before pulling, then reconnect. Or push your image to a private Zoho registry if your work requires it.


§10. Troubleshooting

Symptom

Cause

Fix

docker run hangs on macOS

OrbStack/Colima not running

Launch OrbStack app or colima start

Build super slow (~30 min for LLVM apt)

Building x86_64 on ARM via QEMU

Use --platform linux/arm64 (default) or accept emulation cost

#include <expected> in container fails

Ubuntu clang-20 defaults to libstdc++; your Dockerfile forces libc++ but a file overrides it

Check the file has no -stdlib=libstdc++

Dev Container “port already in use”

Previous container still running

docker psdocker stop <id>

Bind mount slow on macOS

Default consistency is consistent; use cached (already in devcontainer.json)

Restart container

Container UID mismatch on files

USER_ID build arg didn’t match your Mac UID

Rebuild image with --build-arg USER_ID=$(id -u)

apt says “release file is not valid”

LLVM or Kitware apt key expired

Rebuild image (keys refreshed on rebuild)

Docker Desktop uses 100% CPU idle

Docker Desktop bug (existed since 2023)

Switch to OrbStack or Colima


§11. When to use what — decision matrix

Situation

Tool

Solo learner, cost-averse

Colima

Solo learner, time-averse

OrbStack (~$8/mo worth it)

Zoho-corporate work

Whatever Zoho standardizes on

GitHub Actions parity

Dev Containers using the Dockerfile in §4

Multi-service integration test

docker compose from §6

Testing on x86_64 explicitly

Hetzner or Vast.ai VM, not local emulation

Learning k8s (Phase 6 stretch)

OrbStack’s built-in k8s or minikube inside a VM