Skip to content

Feature request: Docker/Podman-compatible socket via launchd socket activation (mocker system service) #7

@kaovilai

Description

@kaovilai

Summary

This is a feature request for a Docker/Podman-compatible Unix socket backed by launchd socket activation — the macOS equivalent of how Podman works on Linux with systemd. The goal is zero idle overhead: no always-running daemon, no persistent VM. The socket exists at all times (owned by launchd), but the process that serves it is only spawned on demand when a client connects, and exits automatically after a configurable inactivity timeout.

This is the same model Podman uses on Linux: podman system service is started on-demand by systemd socket activation, handles requests, then exits. See also: https://podman-desktop.io/docs/migrating-from-docker/managing-docker-compatibility

Motivation

Today mocker is a CLI shim — it translates docker-compatible command invocations into Apple Containerization calls. That covers shell-script-based workflows, but it does not help tools that speak to unix:///var/run/docker.sock or unix:///run/podman/podman.sock directly.

Examples of affected tools:

  • Podman remote client (podman --remote)
  • Docker SDK clients (Go, Python, etc.)
  • DOCKER_HOST-aware CI systems and GitHub Actions runners
  • Tools like lazydocker, k9s's docker provider, ctlptl, lima, etc.

How Podman solves this on Linux (the model to follow)

On Linux, Podman is truly daemonless at the execution layer — containers are child processes of conmon, not of any global daemon. The REST API socket is served by podman system service, a short-lived process activated on demand:

  1. systemd creates and owns the socket file (/run/user/$UID/podman/podman.sock) — passively, with essentially zero overhead.
  2. When a client connects, systemd wakes up podman system service, passing it the inherited socket file descriptor.
  3. podman system service serves the Docker Engine / Podman REST API, translating calls into direct container operations.
  4. After a configurable inactivity timeout (default: 5 seconds), podman system service exits — systemd resumes owning the socket.

No persistent daemon. No idle VM. The socket is always present, but the process behind it is ephemeral.

Proposed design: mocker system service + launchd socket activation

macOS launchd supports the identical socket-activation pattern. The proposed implementation:

1. mocker system service subcommand

A new subcommand that:

  • Accepts an inherited socket file descriptor from launchd (via the standard launchd socket activation protocol)
  • Serves the Docker Engine REST API (and optionally the Podman-compatible REST API) over that socket
  • Translates each API call into Apple Containerization framework calls — the same backend mocker CLI already uses (/usr/local/bin/container, ImageStore, etc.)
  • Exits after a configurable --timeout of inactivity (e.g. --timeout 5s)

2. launchd plist (installed by mocker socket install)

A launchd user agent plist (installed to ~/Library/LaunchAgents/io.mocker.socket.plist) that:

  • Declares the socket path (e.g. ~/.mocker/mocker.sock, optionally symlinked to /var/run/docker.sock)
  • Configures launchd to spawn mocker system service when a client connects
  • Keeps the socket alive across reboots via launchctl enable

Example plist structure:

<key>Sockets</key>
<dict>
  <key>MockerSocket</key>
  <dict>
    <key>SockPathName</key>
    <string>/Users/YOU/.mocker/mocker.sock</string>
  </dict>
</dict>
<key>ProgramArguments</key>
<array>
  <string>/usr/local/bin/mocker</string>
  <string>system</string>
  <string>service</string>
  <string>--timeout</string>
  <string>5s</string>
</array>

3. Helper commands

  • mocker socket install — writes the plist and runs launchctl bootstrap
  • mocker socket uninstall — removes the plist and runs launchctl bootout
  • mocker socket status — shows whether the launchd agent is loaded and the socket path

Resource footprint

State What is running
Idle (no client connected) Only launchd holding the socket fd — effectively zero CPU/memory
Active (client connected) mocker system service process only — no VM, no daemon
Container running Apple Containerization manages the VM per-container, as today

Constraints and notes

  • Apple Containerization is one-VM-per-container, which differs from Docker's shared-kernel model. The API implementation should document where semantics diverge (e.g. --pid sharing, --network host).
  • Podman's REST API is a superset of the Docker Engine API in most common paths, so implementing the Docker Engine API first covers both use cases for the majority of tools.
  • The existing ~/.mocker/ JSON state store serves as the source of truth, keeping socket and CLI state consistent.
  • mocker system service inherits the socket fd from launchd using the standard launch_activate_socket() API (available in macOS SDK, no third-party deps required).

Acceptance criteria

  • mocker system service starts, accepts a launchd-inherited socket fd, and serves the Docker Engine REST API.
  • mocker socket install installs the launchd plist and activates the socket.
  • DOCKER_HOST=unix://~/.mocker/mocker.sock docker ps works.
  • podman --remote --url unix://~/.mocker/mocker.sock ps works.
  • Docker SDK clients (Go, Python) can connect and perform basic container lifecycle operations.
  • With no active clients, zero additional processes are running beyond launchd.
  • mocker system service exits after the inactivity timeout and launchd resumes socket ownership.

Current workarounds (and why they fall short)

Users who need socket-compatible tooling today can try the following, but each hits a wall quickly:

1. Shell alias or docker-symlink to mocker

alias docker=mocker
# or
ln -sf $(which mocker) /usr/local/bin/docker

Where this works: Shell scripts, docker-prefixed CLI invocations.

Where this hits a wall:

  • Any tool that connects to DOCKER_HOST or unix:///var/run/docker.sock directly fails immediately — the socket does not exist.
  • docker context, docker buildx, all SDKs, and podman --remote all require a live socket; an alias cannot satisfy that.

2. socat pipe fronting the mocker CLI

# This does NOT work — mocker is not a stream-protocol server.
socat UNIX-LISTEN:/tmp/mocker.sock,fork EXEC:"mocker ..."

Where this hits a wall:

  • The Docker Engine API is a full HTTP/1.1 REST protocol with persistent connections, multiplexed streams (for attach/exec/logs), and hijacked TCP for TTY. A subprocess-per-connection socat bridge cannot implement this.
  • Each connection spawns a fresh stateless mocker process — no shared state between connections.

3. Running Docker Desktop or Podman Machine alongside mocker

Where this hits a wall:

  • Defeats the purpose: two runtimes with split image stores and container state.
  • Both Docker Desktop and podman machine run a persistent Linux VM — the exact idle overhead this feature aims to eliminate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions