Skip to content

feat(serve): Docker Engine API daemon foundation (mocker serve)#45

Merged
us merged 2 commits into
mainfrom
feat/docker-api-serve
Jun 28, 2026
Merged

feat(serve): Docker Engine API daemon foundation (mocker serve)#45
us merged 2 commits into
mainfrom
feat/docker-api-serve

Conversation

@us

@us us commented Jun 28, 2026

Copy link
Copy Markdown
Owner

What

Adds mocker serve — a Docker Engine API server on a Unix socket so the whole DOCKER_HOST ecosystem (the docker CLI, testcontainers, IDE plugins, CI, language SDKs) can talk to mocker. This is Phase 1 (read-only foundation) of a phased plan; it proves the approach end-to-end.

Why

mocker is Docker-CLI-compatible but had no socket/API. Tools that talk to the daemon socket (not the CLI) couldn't use it. Socket compatibility is the feature that makes a "real Docker alternative" (it's OrbStack's actual moat).

Changes

  • fix(engine): runCLI drained stdout/stderr inside terminationHandler, which deadlocks when output exceeds the ~64KB pipe buffer (the child blocks on write before it can exit). Now drains both pipes concurrently. Prerequisite for the API server issuing large container ls/inspect.
  • feat(serve): DockerAPIServer (SwiftNIO over a unix socket) + the serve command.
    • GET/HEAD /_ping, GET /version with correct version negotiation (advertise API 1.47, MinAPIVersion 1.24, lenient /vX.YY path stripping — never enforce min/max, since testcontainers pins low versions into the path).
    • Socket at $HOME/.docker/run/docker.sock, owner-only 0600, safe lstat preflight that refuses to clobber a non-socket / symlink.
    • JSON with unescaped slashes for Docker parity; Api-Version header on every response.

Verification (tested from all angles)

  • swift test: 239/239 (7 new). Includes a test proving the drain fix reads 1.3 MB without deadlocking (the exact scenario the old code hung on), path-leniency (/v1.32/, /v9.99/, /volumes edge case), and socket-safety (refuses a non-socket file).
  • Real docker CLI: docker version negotiates 1.54 -> 1.47 and connects (Server: mocker / OS/Arch: linux/arm64).
  • Functional matrix against the live socket: HEAD-ping empty body, version leniency, 404 {"message":...}, 10 concurrent requests, keep-alive, docker info/ps fail cleanly (no hang), socket-safety.

Scope / next

Read-only only. Engine-backed endpoints (/info, /containers/json, /images/json, /containers/{id}/json) come next (they add the NIO↔actor bridge). docker run + testcontainers are Phase 2/3. Full phased plan (plan-loop reviewed, 2 iterations): .context/docker-api-plan.md.

us added 2 commits June 28, 2026 15:36
…eadlock

runCLI read stdout/stderr inside terminationHandler, which only fires after the
process exits. When output exceeds the ~64KB pipe buffer the child blocks on
write before it can exit, so the handler never fires and the call hangs forever
(surfaced by the API server issuing large `container ls`/`inspect`). Drain both
pipes on detached tasks while the process runs; readDataToEndOfFile returns at
EOF when the process exits and closes the pipe.
Add `mocker serve`, a Docker Engine API server on a Unix socket so DOCKER_HOST
tooling can talk to mocker. Phase 1 read-only foundation: GET/HEAD /_ping and
GET /version with correct version negotiation (advertise API 1.47, MinAPIVersion
1.24, lenient /vX.YY path stripping — never enforce min/max). SwiftNIO over a
unix domain socket at $HOME/.docker/run/docker.sock, owner-only (0600) with a
safe lstat preflight that refuses to clobber a non-socket. JSON emitted with
unescaped slashes for Docker parity.

Verified: real `docker version` negotiates (1.54 -> 1.47) and connects.
See .context/docker-api-plan.md for the full phased plan (plan-loop reviewed).
@us us merged commit b110ad2 into main Jun 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant