---
title: "Apple container vs Colima: local datastore benchmarks on an M4 Mac"
description: "A practical benchmark comparing Apple's container runtime and Colima across Redis, Postgres, ClickHouse, DuckDB, and Neo4j on one M4 Mac."
publishedAt: 2026-06-25
updatedAt: 2026-06-25
tags:
  - containers
  - colima
  - cloudflare
  - databases
  - benchmarks
coverImage: /images/container-benchmark-cover-1200w.webp
coverImageWidth: 1200
coverImageHeight: 675
answerSummary:
  - Apple container was faster for the long-running datastore services in this local benchmark.
  - Colima stayed smoother operationally and was much faster for the short-lived DuckDB workload.
  - The practical choice depends on workload shape, Docker compatibility needs, and volume behavior.
faqs:
  - question: Is this Docker Desktop vs Apple container?
    answer: No. Docker Desktop was not installed on this machine and was not part of the benchmark.
  - question: Did Apple container win?
    answer: Apple container won the long-running service workloads in this run, but Colima was faster for the short-lived DuckDB workload and smoother operationally.
  - question: Why use 2 CPUs?
    answer: The Colima VM was capped at 2 CPUs, so both runtimes were normalized to 2 CPUs for the shared Redis, Postgres, ClickHouse, and DuckDB run.
  - question: Why was DuckDB so different?
    answer: DuckDB ran as a short-lived CLI workload against a mounted workspace, while the other databases ran as long-running services over localhost TCP.
draft: false
---

import BenchmarkTable from "../../components/BenchmarkTable.astro";

Apple's new `container` CLI made me curious about something practical: should I keep using Colima for local infrastructure, or is Apple's runtime already good enough to use for real development services?

I did not want to answer that with a hello-world container. Most of my local container usage is not hello-world. It is databases, cache services, graph stores, and small analytical jobs. So I benchmarked Apple `container` against Colima across a few datastore shapes:

- Redis for cache / key-value
- Postgres for OLTP-style relational work
- ClickHouse for OLAP server work
- DuckDB for embedded analytics
- Neo4j for graph queries

The benchmark shape looked like this:

```d2 title="Benchmark shape" width=920
direction: right

runtimes: {
  label: "Runtimes"
  apple: "Apple container"
  colima: "Colima"
}

services: {
  label: "Long-running services"
  redis: "Redis"
  postgres: "Postgres"
  clickhouse: "ClickHouse"
  neo4j: "Neo4j"
}

duckdb: "DuckDB short-lived CLI"
workspace: "Mounted workspace"
client: "localhost benchmark client"
readout: "Throughput, latency, startup, setup friction"

runtimes.apple -> services: "service containers"
runtimes.colima -> services: "service containers"
runtimes.apple -> duckdb: "CLI container"
runtimes.colima -> duckdb: "CLI container"
services -> client: "TCP"
duckdb -> workspace: "bind mount"
workspace -> client: "query batch"
client -> readout
```

This is a personal benchmark on one machine, not a universal claim about container runtimes. Still, the results were useful because they were not one-dimensional.

The short version:

- Apple `container` was faster for the long-running services I tested over localhost TCP.
- Colima was easier operationally and much faster for the short-lived DuckDB workload.
- The interesting difference was not just throughput. It was also startup behavior, volume behavior, and how much runtime-specific setup each image needed.

My takeaway:

> Apple `container` is worth testing seriously for long-running local services on Apple Silicon. Colima is still the smoother Docker-compatible baseline. The right answer depends on the workload.

## What I tested

Test machine:

<BenchmarkTable
  caption="Test machine"
  description="The local machine and runtime versions used for this run."
  columns={[
    { key: "item", label: "Item" },
    { key: "value", label: "Value", align: "right" },
  ]}
  rows={[
    { cells: { item: "Mac", value: { value: "Apple M4", strong: true } } },
    { cells: { item: "Memory", value: "16 GiB" } },
    { cells: { item: "OS", value: "macOS 26.5.1, build 25F80" } },
    { cells: { item: "Apple container", value: { value: "1.0.0", mono: true } } },
    { cells: { item: "Colima", value: { value: "0.10.3", mono: true } } },
    { cells: { item: "Docker via Colima", value: { value: "client 29.6.0, server 29.2.1", mono: true } } },
  ]}
/>

Docker Desktop was not installed and was not part of this test.

For Redis, Postgres, ClickHouse, and DuckDB, both runtimes were capped at:

- 2 CPUs
- 4 GiB memory

I used 2 CPUs because my Colima VM was capped at 2 CPUs. The first broad run failed when I tried to use 4 CPUs, so I normalized both runtimes to the lower available cap.

Neo4j was the first benchmark I ran, before the later 2 CPU normalization. I kept it in the article because it is a useful graph-database data point, but I treat it separately from the normalized Redis/Postgres/ClickHouse/DuckDB suite.

## Workloads

<BenchmarkTable
  caption="Workload matrix"
  description="Each datastore represented a different local development shape."
  columns={[
    { key: "store", label: "Store" },
    { key: "shape", label: "Shape" },
    { key: "image", label: "Image" },
    { key: "workload", label: "Workload" },
  ]}
  rows={[
    {
      cells: {
        store: { value: "Redis", strong: true },
        shape: "KV/cache",
        image: { value: "redis:7.4-alpine", mono: true },
        workload: "mixed GET, SET, INCR, MGET, MSET over 50k keys",
      },
    },
    {
      cells: {
        store: { value: "Postgres", strong: true },
        shape: "OLTP",
        image: { value: "postgres:16-alpine", mono: true },
        workload: "20k accounts, 200k events, mixed reads/writes/updates/aggregates",
      },
    },
    {
      cells: {
        store: { value: "ClickHouse", strong: true },
        shape: "OLAP server",
        image: { value: "clickhouse/clickhouse-server:latest", mono: true },
        workload: "1M-row MergeTree, mixed count, rollup, filtered aggregate, top-N queries",
      },
    },
    {
      cells: {
        store: { value: "DuckDB", strong: true },
        shape: "Embedded OLAP",
        image: { value: "duckdb/duckdb:latest", mono: true },
        workload: "1M-row DuckDB table plus Parquet scan in short-lived containers",
      },
    },
    {
      cells: {
        store: { value: "Neo4j", strong: true },
        shape: "Graph",
        image: { value: "neo4j:5-community", mono: true },
        workload: "9,485 graph nodes, 35,332 relationships, mixed graph read queries",
      },
    },
  ]}
/>

Redis, Postgres, ClickHouse, and Neo4j ran as services. The host benchmark client connected over localhost TCP.

DuckDB is different. I did not run DuckDB as a service. I ran the DuckDB CLI inside short-lived containers against a mounted workspace. That matters, because DuckDB ended up showing the opposite result from the long-running services.

## Headline results

At concurrency 4, Apple `container` was faster for every long-running service workload in this run.

<BenchmarkTable
  caption="Headline service results at concurrency 4"
  description="Long-running services connected over localhost TCP."
  columns={[
    { key: "store", label: "Store" },
    { key: "metric", label: "Metric" },
    { key: "colima", label: "Colima", numeric: true },
    { key: "apple", label: "Apple container", numeric: true },
    { key: "faster", label: "Faster" },
  ]}
  rows={[
    {
      highlight: true,
      cells: {
        store: { value: "Redis", strong: true },
        metric: "ops/s",
        colima: "15,378.75",
        apple: { value: "27,434.62", strong: true },
        faster: { value: "Apple", badge: "winner", tone: "winner" },
      },
    },
    {
      highlight: true,
      cells: {
        store: { value: "Postgres", strong: true },
        metric: "ops/s",
        colima: "15,141.07",
        apple: { value: "26,371.10", strong: true },
        faster: { value: "Apple", badge: "winner", tone: "winner" },
      },
    },
    {
      highlight: true,
      cells: {
        store: { value: "ClickHouse", strong: true },
        metric: "ops/s",
        colima: "200.90",
        apple: { value: "230.34", strong: true },
        faster: { value: "Apple", badge: "winner", tone: "winner" },
      },
    },
    {
      highlight: true,
      cells: {
        store: { value: "Neo4j", strong: true },
        metric: "ops/s",
        colima: "372.09",
        apple: { value: "627.69", strong: true },
        faster: { value: "Apple", badge: "winner", tone: "winner" },
      },
    },
  ]}
/>

DuckDB went the other direction.

<BenchmarkTable
  caption="DuckDB short-lived command results"
  description="DuckDB ran as a short-lived CLI workload against a mounted workspace."
  columns={[
    { key: "metric", label: "DuckDB metric" },
    { key: "colima", label: "Colima", numeric: true },
    { key: "apple", label: "Apple container", numeric: true },
    { key: "faster", label: "Faster" },
  ]}
  rows={[
    {
      highlight: true,
      cells: {
        metric: { value: "Setup", strong: true },
        colima: { value: "0.342 s", strong: true },
        apple: "0.883 s",
        faster: { value: "Colima", badge: "winner", tone: "winner" },
      },
    },
    {
      highlight: true,
      cells: {
        metric: { value: "Query batch p50", strong: true },
        colima: { value: "0.180 s", strong: true },
        apple: "0.806 s",
        faster: { value: "Colima", badge: "winner", tone: "winner" },
      },
    },
    {
      highlight: true,
      cells: {
        metric: { value: "Query batch p95", strong: true },
        colima: { value: "0.188 s", strong: true },
        apple: "0.841 s",
        faster: { value: "Colima", badge: "winner", tone: "winner" },
      },
    },
    {
      highlight: true,
      cells: {
        metric: { value: "Full workload command", strong: true },
        colima: { value: "1.690 s", strong: true },
        apple: "6.571 s",
        faster: { value: "Colima", badge: "winner", tone: "winner" },
      },
    },
  ]}
/>

That split is the main point of the benchmark. If I had only tested services, Apple would look like the clear answer. If I had only tested DuckDB, Colima would look like the clear answer. Testing both made the result more useful.

## Service concurrency detail

Each service workload ran at concurrency 1, 4, and 8. Each level ran for 10 seconds.

<BenchmarkTable
  caption="Service concurrency detail"
  description="Each concurrency level ran for 10 seconds."
  dense
  columns={[
    { key: "store", label: "Store" },
    { key: "runtime", label: "Runtime" },
    { key: "c1", label: "c=1 ops/s", numeric: true },
    { key: "c4", label: "c=4 ops/s", numeric: true },
    { key: "c8", label: "c=8 ops/s", numeric: true },
    { key: "p50", label: "c=4 p50", numeric: true },
    { key: "p95", label: "c=4 p95", numeric: true },
  ]}
  rows={[
    { cells: { store: { value: "Redis", strong: true }, runtime: "Colima", c1: "5,062.47", c4: "15,378.75", c8: "19,687.21", p50: "0.253 ms", p95: "0.341 ms" } },
    { highlight: true, cells: { store: { value: "Redis", strong: true }, runtime: { value: "Apple", badge: "faster c=4", tone: "winner" }, c1: "10,555.34", c4: { value: "27,434.62", strong: true }, c8: "22,310.45", p50: "0.139 ms", p95: "0.213 ms" } },
    { cells: { store: { value: "Postgres", strong: true }, runtime: "Colima", c1: "5,386.89", c4: "15,141.07", c8: "19,190.96", p50: "0.243 ms", p95: "0.409 ms" } },
    { highlight: true, cells: { store: { value: "Postgres", strong: true }, runtime: { value: "Apple", badge: "faster c=4", tone: "winner" }, c1: "11,370.94", c4: { value: "26,371.10", strong: true }, c8: "27,644.91", p50: "0.120 ms", p95: "0.287 ms" } },
    { cells: { store: { value: "ClickHouse", strong: true }, runtime: "Colima", c1: "129.92", c4: "200.90", c8: "221.68", p50: "17.942 ms", p95: "42.390 ms" } },
    { highlight: true, cells: { store: { value: "ClickHouse", strong: true }, runtime: { value: "Apple", badge: "faster c=4", tone: "winner" }, c1: "135.80", c4: { value: "230.34", strong: true }, c8: "238.54", p50: "14.030 ms", p95: "42.035 ms" } },
    { cells: { store: { value: "Neo4j", strong: true }, runtime: "Colima", c1: "194.62", c4: "372.09", c8: "401.92", p50: "7.787 ms", p95: "26.957 ms" } },
    { highlight: true, cells: { store: { value: "Neo4j", strong: true }, runtime: { value: "Apple", badge: "faster c=4", tone: "winner" }, c1: "251.75", c4: { value: "627.69", strong: true }, c8: "499.03", p50: "4.623 ms", p95: "13.996 ms" } },
  ]}
/>

All final workload summaries reported zero operation errors.

Redis and Postgres showed the strongest Apple wins in the normalized suite. ClickHouse also favored Apple, but by a smaller margin. Neo4j favored Apple as well, though again, it came from the earlier graph run with a different resource cap.

## Startup and readiness

Throughput was not the only thing I measured. I also tracked pull time, detached start-command time, service readiness, and full workload command duration.

<BenchmarkTable
  caption="Startup and readiness"
  description="Lifecycle timings collected alongside the workload runs."
  dense
  columns={[
    { key: "store", label: "Store" },
    { key: "runtime", label: "Runtime" },
    { key: "pull", label: "Pull", numeric: true },
    { key: "start", label: "Start command", numeric: true },
    { key: "ready", label: "Ready", numeric: true },
    { key: "workload", label: "Workload command", numeric: true },
  ]}
  rows={[
    { cells: { store: { value: "Redis", strong: true }, runtime: "Colima", pull: "2.601 s", start: { value: "0.157 s", strong: true }, ready: "0.007 s", workload: "30.948 s" } },
    { cells: { store: { value: "Redis", strong: true }, runtime: "Apple", pull: { value: "1.068 s", strong: true }, start: "0.795 s", ready: "0.007 s", workload: "31.084 s" } },
    { cells: { store: { value: "Postgres", strong: true }, runtime: "Colima", pull: "1.945 s", start: { value: "0.145 s", strong: true }, ready: "1.056 s", workload: "31.610 s" } },
    { cells: { store: { value: "Postgres", strong: true }, runtime: "Apple", pull: { value: "1.130 s", strong: true }, start: "0.688 s", ready: { value: "1.031 s", strong: true }, workload: "31.713 s" } },
    { cells: { store: { value: "ClickHouse", strong: true }, runtime: "Colima", pull: { value: "1.937 s", strong: true }, start: { value: "0.125 s", strong: true }, ready: { value: "4.096 s", strong: true }, workload: { value: "30.346 s", strong: true } } },
    { cells: { store: { value: "ClickHouse", strong: true }, runtime: { value: "Apple", badge: "slow pull", tone: "warning" }, pull: "87.784 s", start: "0.761 s", ready: "4.554 s", workload: "30.398 s" } },
    { cells: { store: { value: "DuckDB", strong: true }, runtime: { value: "Colima", badge: "faster", tone: "winner" }, pull: { value: "1.240 s", strong: true }, start: "n/a", ready: "n/a", workload: { value: "1.690 s", strong: true } } },
    { cells: { store: { value: "DuckDB", strong: true }, runtime: "Apple", pull: "11.199 s", start: "n/a", ready: "n/a", workload: "6.571 s" } },
  ]}
  note="Pull timings include warmed and partially warmed reruns, so they are not treated as the headline result."
/>

I do not treat pull time as the headline result. Some images were already warm because I reran parts of the suite while fixing benchmark issues. The clearest cold-ish Apple pull/unpack measurement in this session was ClickHouse, which took 87.784 seconds.

The more consistent lifecycle observation was this:

- Colima returned from detached `docker run -d` faster.
- Apple `container run -d` took roughly 0.7 to 0.8 seconds for these service containers.
- Once the service process was starting, readiness was similar for Redis, Postgres, and ClickHouse.

## What broke

The failures were useful. They showed the operational differences more clearly than the happy path.

### CPU caps

The first broad run requested 4 CPUs and failed under Colima:

```text
range of CPUs is from 0.01 to 2.00, as there are only 2 CPUs available
```

I changed the suite to use 2 CPUs for both runtimes. That made the comparison fairer.

### Apple `container` CPU argument parsing

Apple `container` rejected `--cpus 2.0`:

```text
The value '2.0' is invalid for '--cpus <cpus>'
```

Passing `2` fixed it.

### Neo4j bind mounts

Colima worked with direct host bind mounts for Neo4j data, logs, import, and plugins.

Apple `container` did not work with the same Neo4j bind-mounted setup. The official Neo4j image tried to change ownership of `/logs` and got:

```text
Operation not permitted
```

The working Apple path used named volumes instead.

### Postgres named volumes

Postgres initially failed under Apple `container`:

```text
initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
initdb: detail: It contains a lost+found directory, perhaps due to it being a mount point.
```

Apple named volumes are ext4 images, and the root contains `lost+found`. Postgres does not want to initialize directly into a non-empty data directory.

The fix was:

```text
PGDATA=/var/lib/postgresql/data/pgdata
```

### ClickHouse auth

ClickHouse required explicit credentials in this image. I set:

```text
CLICKHOUSE_DB=bench
CLICKHOUSE_USER=bench
CLICKHOUSE_PASSWORD=bench
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
```

### DuckDB invocation

The DuckDB image needed the binary invoked explicitly:

```text
duckdb /workspace/bench.duckdb
```

That was a benchmark-runner fix, not really a runtime finding.

## What Apple `container` did well

Apple's strongest result was service throughput.

Redis, Postgres, ClickHouse, and Neo4j all had higher throughput under Apple `container` in this run. The advantage was largest for Redis, Postgres, and Neo4j. ClickHouse was closer but still favored Apple.

That makes Apple `container` interesting for local development workflows where:

- the service runs for a while
- the client connects over localhost TCP
- the image works cleanly with Apple's volume model
- Docker CLI compatibility is not the main requirement

## What Colima did well

Colima was smoother.

It used the normal Docker CLI. It was easier to script. Detached service starts returned faster. Bind mounts behaved more like I expected from Docker-shaped workflows.

And DuckDB was not close: Colima was much faster for the short-lived embedded analytics job.

That makes Colima still attractive for:

- Docker-compatible local workflows
- tools that expect Docker behavior
- scripts built around `docker run`
- mounted-file workloads
- short-lived containerized commands

Colima being boring is a feature.

## What surprised me

The interesting result is not that one runtime is faster than the other. The interesting result is that workload shape changed the answer.

If I had only tested Redis and Postgres, Apple `container` would look like the obvious choice.

If I had only tested DuckDB, Colima would look like the obvious choice.

If I had only tested Neo4j, I would have seen Apple's graph-query throughput and missed the volume-model friction.

Testing several datastore shapes made the split clearer:

- long-running services favored Apple
- short-lived file-backed analytics favored Colima
- operational simplicity favored Colima
- service throughput favored Apple

That is the result I trust most from this session.

## How I would use this today

For my own local development, this does not replace Colima outright.

Colima remains my default compatibility runtime because it maps cleanly to Docker workflows and existing tooling.

Apple `container` is the runtime I would test selectively for long-running local services where:

- the image is known to work
- the volume setup is understood
- the service benefits from the throughput profile
- Docker CLI compatibility is not required

For the graph-database case specifically, both paths make sense:

- Colima for the smoother Docker-compatible path
- Apple `container` for faster query throughput, using named volumes by default

## Caveats

These are personal local tests on one Apple M4 machine with 16 GiB RAM.

This is not a production benchmark.

This is not a Docker Desktop benchmark.

This does not measure multi-day reliability, Compose workflows, Kubernetes behavior, backup/restore, memory pressure under larger datasets, or production durability.

Some pull timings were warm or partially warmed by reruns, so pull time is not the main result.

ClickHouse and DuckDB used `latest` images in this pass. That is fine for this personal test, but not ideal for a fully reproducible benchmark suite.

The Neo4j result was carried forward from the earlier graph-database run and used a different resource cap than the later Redis/Postgres/ClickHouse/DuckDB suite.

## FAQ

### Is this Docker Desktop vs Apple `container`?

No. Docker Desktop was not installed on this machine and was not part of the benchmark.

### Did Apple `container` win?

For long-running service workloads in this run, yes, Apple `container` had higher throughput.

For the short-lived DuckDB embedded workload, no. Colima was much faster.

For operational simplicity, Colima was smoother.

That is why I do not reduce the result to a single winner.

### Why use 2 CPUs?

Because my Colima VM was capped at 2 CPUs. The first broad-suite run failed when the runner requested 4 CPUs. I changed the suite to use 2 CPUs for both runtimes so Apple would not get a higher CPU cap than Colima.

### Why is Neo4j treated differently?

Neo4j was the original benchmark that started the session. It was run before the later 2 CPU normalization. I kept it in the article because it is a useful graph-database data point, but I keep it separate from the normalized datastore suite.

### Why was DuckDB so different?

DuckDB was measured as an embedded CLI workload inside short-lived containers using a mounted workspace. Redis, Postgres, ClickHouse, and Neo4j were long-running services over localhost TCP. Different shape, different result.

### Would larger datasets change the result?

Possibly. Larger datasets, longer runs, heavier write pressure, different volume modes, and memory pressure could all change the shape.

## Final read

Apple `container` looks genuinely strong for long-running local datastore services on Apple Silicon. In my tests, Redis, Postgres, ClickHouse, and Neo4j all had better throughput under Apple `container`.

Colima remains the easier operational baseline. It is Docker-compatible, predictable, and better for the short-lived DuckDB embedded workload I tested.

So I do not frame this as a replacement story.

I frame it this way:

> Apple `container` is now worth testing seriously for local services. Colima is still the compatibility baseline. The right answer depends on the workload.

That is a more useful result than a single winner.

## References

- Apple `container`: https://github.com/apple/container
- Apple Open Source `container`: https://opensource.apple.com/projects/container
- Colima: https://github.com/abiosoft/colima
- Redis image: https://hub.docker.com/_/redis
- Postgres image: https://hub.docker.com/_/postgres
- ClickHouse image: https://hub.docker.com/r/clickhouse/clickhouse-server
- DuckDB image: https://hub.docker.com/r/duckdb/duckdb
- Neo4j image: https://hub.docker.com/_/neo4j

## Disclosure

This article was written with assistance from ChatGPT/Codex based on my local benchmark session, commands, outputs, and review direction.
