Important
Both socktainer and Apple container are still under heavy development!
Note
socktainer maintains to be compatible with Docker Engine API v1.51.
- Socktainer 🚢
Socktainer is a CLI/daemon that exposes a Docker-compatible REST API on top of Apple's containerization libraries 🍏📦.
It allows common Docker clients (like the Docker CLI) to interact with local containers on macOS using the Docker API surface 🐳💻.
Podman Desktop Apple Container extension uses socktainer to visualize Apple containers/images in Podman Desktop.
Get started with socktainer CLI in just a few commands:
./socktainer
FolderWatcher] Started watching $HOME/Library/Application Support/com.apple.container
[ NOTICE ] Server started on http+unix: $HOME/.socktainer/container.sock
...Socktainer automatically registers a socktainer Docker context on startup.
Activate it once:
docker context use socktainerThen use Docker normally — no DOCKER_HOST needed:
docker ps # List running containers
docker ps -a # List all containers
docker images # List available imagesSwitch back to another runtime at any time:
docker context use colima # or "default", etc.Opt out of automatic context creation
Pass --no-docker-context to skip writing the context file on startup — useful
in CI or when managing Docker contexts manually:
socktainer --no-docker-contextNote: this flag skips creating the context but does not remove one that was
already created. To remove it: docker context rm socktainer.
Alternative: set DOCKER_HOST manually
export DOCKER_HOST=unix://$HOME/.socktainer/container.sock
docker ps
docker imagesOr inline without exporting:
DOCKER_HOST=unix://$HOME/.socktainer/container.sock docker ps
DOCKER_HOST=unix://$HOME/.socktainer/container.sock docker images- Built on Apple’s Container Framework 🍏
- Provides Docker REST API compatibility 🔄 (partial)
- Listens on a Unix domain socket
$HOME/.socktainer/container.sockand auto-registers asocktainerDocker context - Supports container lifecycle operations: inspect, stop, remove 🛠️
- Supports image listing, pulling, deletion, logs, health checks, container stats. Exec without interactive mode 📄
docker statsreports memory against the Apple Container VM limit (configurable via--memory, default 1 GiB per container), not the host RAM- Broadcasts container events for client liveness monitoring 📡
- macOS 26 (Tahoe) on Apple Silicon (arm64) Apple’s container APIs only work on arm64 Macs 🍏💻
- Apple Container 0.6.0
socktainer is included in Homebrew Formulae:
brew install socktainerIf you want to install the latest head version of socktainer:
brew install socktainer --HEADNote
Formerly we have provided our own Homebrew tap. You may want to integrate it with the Homebrew official, and you can run the following command:
brew untap socktainer/tapDownload from socktainer releases page the zip or binary. Ensure the binary has execute permissions (+x) before running it.
Refer to Quick Start above for immediate usage examples.
Named volumes default to nosync — guest fsync() calls are not flushed to the
host disk on demand, matching Colima's behavior and giving ~1.5× speedup for
write-heavy workloads (postgres WAL, Kafka, Redis AOF).
Tradeoff: data written to a volume since the last OS page-cache flush could be
lost if the host (Mac) crashes or loses power. In a dev environment this is
acceptable; data is safe across normal docker stop / host restarts.
Override globally — apply the same mode to all named volumes (bind mounts and anonymous volumes are not affected):
socktainer --volume-sync=fsync # honor guest fsyncs (durable)
socktainer --volume-sync=full # fully synchronous writes (slowest)
socktainer --volume-sync=nosync # defaultOverride per volume — docker volume create -o sync=<mode> persists the
choice for that volume regardless of the global flag:
docker volume create -o sync=fsync my-pgdata
docker run -v my-pgdata:/var/lib/postgresql/data postgresOr using Docker Compose with driver_opts:
services:
postgres:
image: postgres:latest
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
driver: local
driver_opts:
sync: fsyncValid modes: nosync · fsync · full
Each container runs in its own Apple Container VM with a fixed memory allocation.
Socktainer honors Docker's --memory flag and mem_limit: / deploy.resources.limits.memory: in Compose files.
docker run --memory 2g postgres # 2 GiB VM
docker run --memory 512m redis # 512 MiB VM
docker run postgres # 1 GiB VM (Apple Container default)Note: Apple Container allocates VM RAM at creation time — this is not a cgroup soft limit. Setting
--memorytoo low will cause the process to OOM inside the VM.There is no "unlimited" mode: Docker's
--memory 0(no limit) maps to the Apple Container default of 1 GiB, not host RAM. To give a container more than 1 GiB, always pass an explicit--memoryvalue.
In Docker Compose:
services:
kafka:
image: confluentinc/cp-kafka
mem_limit: 2g
redis:
image: redis:alpine
mem_limit: 256mApple Container's EXT4 volumes always contain /lost+found, which causes
initdb to fail with "directory exists but is not empty". Socktainer
automatically removes it when a Postgres container is created, before
initdb runs.
Opt out — set SOCKTAINER_CLEAN_VOLUMES=false globally, or label a
specific volume with socktainer.clean-volumes=false.
- Swift 6.2 (requirements from Apple container)
- Xcode 26 (select the correct toolchain if installed in a custom location)
sudo xcode-select --switch /Applications/Xcode_26.0.0.app/Contents/Developer
# or
sudo xcode-select -s /Applications/Xcode-26.app/Contents/Developer- Build the project:
make- (Optional) Format the code:
make fmt- Run the debug binary:
.build/arm64-apple-macosx/debug/socktainerThe server will create the socket at
$HOME/.socktainer/container.sock.
Run unit tests:
make testWe welcome contributions!
- Fork the repository and create a feature branch 🌿
- Open a PR against
mainwith a clear description 📝 - Add or update tests for new behavior (see
Tests/socktainerTests) ✔️ - Keep changes small and focused. Document API or behavioral changes in the PR description 📚
- Code organization under
Sources/socktainer/:Routes/— Route handlers 🛣️Clients/— Client integrations 🔌Utilities/— Helper utilities 🧰
- Document any public API or CLI changes in this README 📝
When passing I/O to ContainerClient.createProcess(stdio:) or ContainerClient.bootstrap(id:stdio:), do not use Foundation's Pipe(). Use StdioPipes from Sources/socktainer/Utilities/DockerConnectionUtility.swift instead.
Background: on Unix, every open file/socket/pipe is identified by a small integer called a file descriptor (fd). Apple's APIs dup the fds you pass into the container and then immediately close your originals. Foundation's Pipe doesn't know this happened — when it's eventually garbage-collected, it tries to close() the same fd number again. By then, that number may have been recycled for a NIO HTTP socket, so the double-close silently kills an active connection, corrupting the event loop and causing hard-to-reproduce crashes under concurrent load (issue #107).
StdioPipes centralises allocation, EMFILE validation, and cleanup:
guard let pipes = StdioPipes.make([.stdin, .stdout, .stderr]) else { // or make(.all)
throw Abort(.internalServerError, reason: "Failed to create I/O pipes")
}
let process: ClientProcess
do {
process = try await ContainerClient().createProcess(..., stdio: pipes.stdioArray)
} catch {
pipes.closeAll() // Apple never received the fds — close all 6
throw error
}
do {
try await process.start()
} catch {
pipes.closeAfterHandoff() // Apple owns stdin.read, stdout.write, stderr.write
throw error
}
// Use pipes.stdout?.read, pipes.stderr?.read, pipes.stdin?.write in tasksOwnership rules:
- stdout/stderr: Apple closes
.write. You close.readwhen the reader task ends. - stdin: Apple closes
.read. You close.writewhen done sending input. StdioPipes.make()closes any partial pipes on EMFILE and returnsnil— alwaysguard let.
make test includes a lint-pipes check that fails if = Pipe() appears in any source file other than the one legitimate exception (ClientRegistryService.swift, which uses Foundation's own Process, not Apple Container APIs).
-
Intended for local development and experimentation 🏠
-
Running third-party container workloads carries inherent risks. Review sandboxing and container configurations 🔒
-
Docker API compatibility is partial, focused on commonly used endpoints. See
Sources/socktainer/Routes/for implemented routes -
Private registry auth currently depends on Apple
containerbehavior. If login succeeds but private pulls/builds still fail, a manual workaround may be required. See apple/container#816 comment 3534438608 and comment 3503618765. -
docker run --privilegedis not supported — Apple Container has no privileged mode. Use granular--cap-add/--cap-drop(and--read-only,--sysctl) instead;--privilegedis currently ignored rather than granting all capabilities. -
docker run --cpusis honored, but Apple Container allocates a whole vCPU count to each container's VM rather than throttling a shared kernel's CFS quota. A fractional value is floored to the nearest whole core (minimum 1) — e.g.--cpus=1.5gets 1 vCPU,--cpus=0.5still gets 1.--cpu-shares(relative weighting) and--cpu-period/--cpu-quotahave no equivalent and are not applied. -
Bind-mounting
/var/run/docker.sock(e.g.-v /var/run/docker.sock:/var/run/docker.sock, used by tools like Supabase'svectorlog collector) is transparently relayed to socktainer's own Docker-compatible API, rather than dropped. This matches Docker's own behavior for the same bind mount, and carries the same well-known risk: any container that requests this mount gets full control of every other container socktainer manages, not just itself — the same exposure Docker itself has always had with this idiom, not something new to socktainer. This scales with the number of containers that request the mount, since each gets its own fully-privileged, independent connection. -
docker exportstreams the container's root filesystem; exporting a running container yields a volatile snapshot, same as Docker. One visible difference: the tar contains no/.dockerenv— Docker's daemon fabricates that file inside every container at start, Apple Container does not. Tools that probe/.dockerenvto detect "am I inside a container" won't find it in filesystems exported from socktainer. -
docker run --restartis honored (no,always,unless-stopped,on-failure[:max-retries]), matching moby's restart-manager behavior closely, including its quirks:- Backoff between restart attempts doubles on rapid successive crashes (100ms → up to 1 minute), but resets back to 100ms once the container has managed to stay up for at least 10 seconds.
- An explicit
docker stop/docker killonly suppresses the next auto-restart forunless-stopped.alwaysrestarts the container anyway, andon-failurerestarts whenever the exit code is non-zero — regardless of whether a human or a crash caused the exit. Useunless-stoppedif you don't want a manual stop to be overridden. - Caveat: the policy is enforced only by the running
socktainerprocess — it does not survive asocktainerrestart or host reboot, unlike real Docker's daemon-level reconciliation.always/unless-stoppedcontainers are not automatically resumed onsocktainerstartup.
-
docker updatesupports restart policies only (--restart). CPU and memory limits cannot change after create — Apple Container runs each container in a VM whose resources are fixed at boot. Resource-only updates return an error; a restart-policy update combined with resource flags applies the policy and returns a warning for the ignored fields. Updated policies are stored durably, but the enforcement caveat above applies to them like any other restart policy. -
docker loadaccepts tarballs from real Docker (plain, gzip or zstd compressed). Index entries whose blobs are not in the tarball are dropped on load —docker saveon real Docker ships only the pulling platform's blobs while keeping the full multi-platform index, so only that platform is imported. -
docker saveworks for images that were loaded from a tarball, and thedocker save | docker loadround-trip is supported. It fails withContentStore missing blob datafor registry-pulled images: Apple's Containerization pull stores the image's full multi-platform index but downloads only the local platform's blobs, and save exports the whole index (platform limitation). -
docker pause/docker unpauseare not supported — there is no freezer/checkpoint equivalent for Apple Container VMs. -
docker network connect/disconnectare accepted as no-ops: Virtualization.framework offers no NIC hotplug and Apple Container has no post-create attach API, so network membership is fixed at container create. Containers on user-created networks reach each other by name through socktainer's DNS, which covers the common Compose use. -
Static container IPs (
--ip, IPAM per-container config) cannot be honored: Apple Container assigns addresses from a rotating allocator with no way to request a specific one. Name-based discovery via socktainer DNS is the supported alternative; addresses stay stable for a container's lifetime. -
Not yet implemented (endpoints answer an explicit error instead of pretending):
docker commit,docker diff,docker search,docker top, andGET /distribution/{name}/json.
Socktainer registers service names in its DNS server so Compose services can reach each other by name. Two aliases are created per service:
| Alias | Example | Description |
|---|---|---|
service |
db |
Short form — works within a single project |
service.project |
db.myapp |
Project-qualified — unique across concurrent projects |
Set the project name explicitly via name: at the top of your Compose file
(otherwise Docker Compose derives it from the directory name):
name: myapp # sets com.docker.compose.project=myapp
services:
web:
image: nginx:alpine
# can reach the database as 'db' or 'db.myapp'
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: secretNote: Apple Container uses a single global hostname namespace. Two Compose projects running simultaneously with identically-named services (e.g. both have a
dbservice) will share the short-form alias — last started wins. Use the qualified form (db.myapp) to resolve unambiguously.
If inter-container connections start failing with no route to host / EHOSTUNREACH after heavy use (many networks created and destroyed), Apple Container's vmnet state has degraded — reset it with container system stop && container system start, then restart socktainer.
Socktainer pins a stable subnet on each network it creates so that inter-container DNS keeps working across a container system restart (an unpinned network's subnet is reassigned by vmnet on restart, which would leave containers' DNS nameservers pointing at a dead address). An explicit --subnet / Compose ipam.config.subnet is honored; otherwise a free 192.168.x.0/24 is chosen automatically.
IPAM.Config fields other than Subnet — Gateway, IPRange, and AuxiliaryAddresses — are not supported by the Apple Container backend (the gateway is always the subnet's .1 and addresses are allocated by vmnet). They are ignored, and a WARNING is logged when requested.
Note: networks created before this behavior shipped are not pinned retroactively — recreate them (
docker compose down && docker compose up) to get a stable subnet.
Apple Container only accepts lowercase label keys matching [a-z0-9](?:[a-z0-9\-\.\/]*[a-z0-9])?. Docker allows mixed-case, underscores, and other characters. Socktainer automatically normalizes label keys at create time:
- Uppercase → lowercase (
sessionId→sessionid) - Underscores → hyphens (
my_key→my-key) - Other invalid characters are dropped
- An
INFOlog is emitted for every key that is changed - A
WARNINGis logged when a key becomes empty after normalization (dropped) or when two keys normalize to the same string (collision, last value wins)
Original keys are preserved via an internal mapping label (socktainer.label-original-keys) that is stored alongside the normalized keys and stripped from all API responses. As a result, docker inspect, filter lookups, and Go-template label access all return and match the original key exactly:
# Works — original key is restored transparently
docker inspect --format '{{index .Config.Labels "MyApp"}}' <container>
docker ps --filter label=MyApp=valueThe one edge case: if two keys normalize to the same string (e.g. MyKey and mykey), the last one wins — a WARNING is logged so the loss is visible in Socktainer's output.
Join the Socktainer community to ask questions, share ideas, or get help:
- Discord: discord.gg/Pw9VWKcUEt – chat in real time with contributors and users
- GitHub Discussions: socktainer/discussions – ask questions or propose features
- GitHub Issues: socktainer/issues – report bugs or request features
See the LICENSE file in the repository root.
- Built using Apple containerization libraries 🍏
- Enables Docker CLI and other Docker clients to interact with local macOS containers 🐳💻