Version: 0.3.0
Date: April 2026
Status: Core specification. Written at Phase 35, updated at Phase 85+. For implementation details, see the companion specs below.
Mission: A Docker-compatible REST API daemon that executes containers on cloud serverless backends (Amazon Elastic Container Service (ECS), Google Cloud Run, Azure Container Apps, and others) instead of a local Docker Engine.
| Spec | Description |
|---|---|
| CONFIG.md | Unified config file, per-backend env vars, validation |
| BACKENDS.md | All 7 backends, server structs, method override matrix |
| DRIVERS.md | Exec, Filesystem, Stream, Network driver interfaces |
| API_SURFACE.md | All 65 api.Backend methods by category |
| IMAGE_MANAGEMENT.md | ImageManager, AuthProvider, per-cloud registry sync |
| IMAGE_BUILD.md | docker build/buildx → cloud build services (CodeBuild, Cloud Build, ACR Tasks) |
| IMAGE_REGISTRY.md | OCI Distribution v2 protocol, per-cloud registry features, auth caching |
| IMAGE_SCANNING.md | Vulnerability scanning, image signing, supply chain security |
| BACKEND_STATE.md | Stateless backends, cloud tags as source of truth, resource tagging conventions |
| CLOUD_RESOURCE_MAPPING.md | Per-cloud authoritative mapping (docker container/pod/network/image/exec → cloud resource), state-derivation rules, stateless recovery contract |
| FAAS_PODS.md | FaaS pod support boundaries for Lambda, GCF, and AZF |
| SIMULATOR_PERSISTENCE.md | SQLite state persistence, Store interface, process tracking |
| SIMULATOR_RECOVERY.md | Recovery on restart, PID re-attachment, backend re-sync, tagging conventions |
| DOCKER_REST_API.md | Docker API compatibility analysis |
| COMPARISONS.md | Comparison with alternatives |
For project status see STATUS.md. For open bugs see BUGS.md.
- Project Overview
- Design Goals and Non-Goals
- Target Docker API Surface
- Endpoint Specifications
- Protocol Requirements
- System Architecture
- Internal API — Frontend ↔ Backend
- Agent Protocol
- Cloud Backend Mapping
- Volume Emulation
- Network Emulation
- Docker Compose Support
- CI Runner Compatibility
- Limitations and Exclusions
- Configuration
- Implementation Phases
Sockerless is a daemon that:
- Listens on a Unix socket (or TCP) and speaks the Docker Engine REST API
- Translates Docker API calls into cloud provider operations (AWS ECS tasks, Google Cloud Run jobs, Azure Container Apps jobs, etc.)
- Maintains an in-memory (or lightweight persistent) state of containers, networks, and volumes it manages
- Implements the multiplexed stream protocol for attach/exec I/O
A standard Docker CLI (docker run, docker ps, docker logs, etc.) or Docker SDK connects to sockerless exactly as it would to a real Docker daemon. The difference is that containers run on cloud serverless infrastructure rather than on the local machine.
| Priority | Use Case | What It Requires |
|---|---|---|
| P0 | GitLab Runner with docker-executor | Containers, exec, attach (hijacked + multiplexed), volumes, networks, image pull, logs, wait |
| P0 | GitHub Actions Runner with container jobs | Containers, exec (primary mechanism), image pull, logs, networks, health checks |
| P1 | docker compose up/down/ps/logs |
Multi-container orchestration, networks, volumes, label-based grouping |
| P2 | General docker run / docker exec usage |
Interactive developer use |
No existing project fills this niche. As documented in DOCKER_REST_API.md:
- Docker Engine, Docker Desktop, Colima, Rancher Desktop all run containers locally
- Podman reimplements the API but still runs containers locally
- Testcontainers Cloud proxies the API to remote Docker Engines (still Docker under the hood)
- Fly.io Machines has a proprietary API (0% Docker compat)
- No cloud service (AWS ECS, GCR, Azure ACA) exposes a Docker-compatible REST API
Sockerless bridges this gap: Docker API on top, cloud serverless capacity on the bottom.
| # | Goal |
|---|---|
| G1 | GitLab Runner docker-executor works without modification |
| G2 | GitHub Actions Runner container jobs work without modification |
| G3 | docker compose up/down/ps/logs works for basic service stacks |
| G4 | Backend-agnostic API layer with pluggable cloud adapters |
| G5 | Standard Docker CLI works as the client (no custom CLI needed) |
| G6 | Multiplexed stream protocol (8-byte header framing) for attach/exec I/O |
| G7 | Emulate volumes via cloud storage and networks via cloud VPC/service mesh |
| # | Non-Goal | Reason | Status |
|---|---|---|---|
docker build / POST /build |
Implemented (Phase 34). Dockerfile parser supports FROM, COPY, ADD, ENV, CMD, ENTRYPOINT, WORKDIR, ARG, LABEL, EXPOSE, USER. RUN instructions are no-op (sufficient for CI Dockerfile patterns). Multi-stage builds supported. | ||
| N2 | Swarm endpoints (/swarm/*, /services/*, /tasks/*, /nodes/*, /secrets/*, /configs/*) |
Neither CI runner uses Swarm. No cloud backend maps to it. | Not implemented |
| N3 | Plugin endpoints (/plugins/*) |
Docker plugin system is not relevant to cloud backends. | Not implemented |
| N4 | Session/BuildKit endpoint (POST /session) |
BuildKit-specific. | Not implemented |
| N5 | Distribution endpoint (GET /distribution/{name}/json) |
Not used by target CI runners. | Not implemented |
GET/PUT/HEAD /containers/{id}/archive) |
Implemented (Phase 25). Required for docker cp before docker start pattern used by gitlab-ci-local. Pre-start staging directories support archive extraction before container start. |
||
| N7 | Image push (POST /images/{name}/push) |
Users push images to registries externally. | Not implemented |
| N8 | Sub-second container startup | Cloud backends have inherent startup latency (5-60s). Accepted tradeoff. | Accepted |
Implemented (Phase 19). Lambda, Cloud Functions, and Azure Functions backends are fully operational with reverse agent support for exec/attach. FaaS backends use reverse agent exclusively — the agent inside the function dials back to the backend via SOCKERLESS_CALLBACK_URL. Helper and cache containers auto-stop after 500ms. Not CI-runner compatible but useful for short-lived workloads. |
50+ endpoints across 8 categories. This started as the union of endpoints required by GitLab Runner docker-executor and GitHub Actions Runner, and has been extended to cover additional Docker API operations including build, archive, and lifecycle management.
| Category | Count | Endpoints |
|---|---|---|
| System | 6 | _ping (GET, HEAD), version, info, events, system/df |
| Images | 9 | pull, inspect, load, tag, auth, build, list, remove, history, prune |
| Containers | 17 | create, start, inspect, list, logs, attach, wait, stop, kill, remove, restart, rename, pause, unpause, top, stats, prune |
| Exec | 3 | create, start, inspect |
| Networks | 7 | create, list, inspect, connect, disconnect, remove, prune |
| Volumes | 5 | create, list, inspect, remove, prune |
| Archive | 3 | put, head, get |
Each endpoint is classified by which CI runner requires it. Endpoints marked "Impl" were not originally in scope but have been implemented.
| Endpoint | GitLab Runner | GitHub Runner | Docker Compose | Priority | Status |
|---|---|---|---|---|---|
| System | |||||
GET /_ping |
Yes | — | Yes | P0 | Done |
HEAD /_ping |
Yes | — | Yes | P0 | Done |
GET /version |
Yes | Yes | Yes | P0 | Done |
GET /info |
Yes | — | Yes | P0 | Done |
GET /events |
— | — | Yes | P2 | Done |
GET /system/df |
— | — | — | P2 | Done |
| Images | |||||
POST /images/create (pull) |
Yes | Yes | Yes | P0 | Done |
GET /images/{name}/json |
Yes | — | Yes | P0 | Done |
POST /images/load |
Yes | — | — | P1 | Done |
POST /images/{name}/tag |
Yes | — | — | P1 | Done |
POST /auth |
— | Yes | Yes | P1 | Done |
POST /build |
— | — | Yes | Impl | Done (Phase 34) |
GET /images/json |
— | — | Yes | Impl | Done |
DELETE /images/{name} |
— | — | — | Impl | Done |
GET /images/{name}/history |
— | — | — | Impl | Done |
POST /images/prune |
— | — | — | Impl | Done |
| Containers | |||||
POST /containers/create |
Yes | Yes | Yes | P0 | Done |
POST /containers/{id}/start |
Yes | Yes | Yes | P0 | Done |
GET /containers/{id}/json |
Yes | Yes | Yes | P0 | Done |
GET /containers/json |
Yes | Yes | Yes | P0 | Done |
GET /containers/{id}/logs |
Yes | Yes | Yes | P0 | Done |
POST /containers/{id}/attach |
Yes | — | — | P0 | Done |
POST /containers/{id}/wait |
Yes | Yes | — | P0 | Done |
POST /containers/{id}/stop |
Yes | — | Yes | P0 | Done |
POST /containers/{id}/kill |
Yes | — | Yes | P0 | Done |
DELETE /containers/{id} |
Yes | Yes | Yes | P0 | Done |
POST /containers/{id}/restart |
— | — | Yes | Impl | Done |
POST /containers/{id}/rename |
— | — | — | Impl | Done |
POST /containers/{id}/pause |
— | — | Yes | Impl | Done |
POST /containers/{id}/unpause |
— | — | Yes | Impl | Done |
GET /containers/{id}/top |
— | — | — | Impl | Done |
GET /containers/{id}/stats |
— | — | — | Impl | Done |
POST /containers/prune |
— | — | Yes | Impl | Done |
| Archive | |||||
PUT /containers/{id}/archive |
— | — | — | Impl | Done (Phase 25) |
HEAD /containers/{id}/archive |
— | — | — | Impl | Done (Phase 25) |
GET /containers/{id}/archive |
— | — | — | Impl | Done (Phase 25) |
| Exec | |||||
POST /containers/{id}/exec |
Yes | Yes | — | P0 | Done |
POST /exec/{id}/start |
Yes | Yes | — | P0 | Done |
GET /exec/{id}/json |
— | — | — | P2 | Done |
| Networks | |||||
POST /networks/create |
Yes | Yes | Yes | P0 | Done |
GET /networks |
Yes | — | Yes | P0 | Done |
GET /networks/{id} |
Yes | — | Yes | P0 | Done |
POST /networks/{id}/connect |
— | — | Yes | Impl | Done |
POST /networks/{id}/disconnect |
Yes | — | — | P1 | Done |
DELETE /networks/{id} |
Yes | Yes | Yes | P0 | Done |
POST /networks/prune |
— | Yes | — | P1 | Done |
| Volumes | |||||
POST /volumes/create |
Yes | — | Yes | P0 | Done |
GET /volumes |
Yes | — | Yes | P0 | Done |
GET /volumes/{name} |
Yes | — | Yes | P0 | Done |
DELETE /volumes/{name} |
Yes | — | Yes | P0 | Done |
POST /volumes/prune |
— | — | Yes | Impl | Done |
Sockerless targets Docker API v1.44 (Docker 25.0). This is chosen because:
- GitLab Runner uses API version negotiation and has specific code paths for v1.44 (MAC address field moved from
ConfigtoNetworkingConfig.EndpointsConfig) - GitHub Actions Runner requires minimum v1.35
- v1.44 is modern enough to satisfy all current clients but avoids v1.45-specific features we don't need
The GET /_ping response will include API-Version: 1.44 and Docker-Experimental: false.
Returns OK (plain text). Headers:
API-Version: 1.44
Docker-Experimental: false
Ostype: linux
Builder-Version: 0
Used by Docker SDK for API version negotiation. Must respond fast (no backend calls).
{
"Platform": { "Name": "Sockerless" },
"Version": "0.1.0",
"ApiVersion": "1.44",
"MinAPIVersion": "1.44",
"Os": "linux",
"Arch": "amd64",
"KernelVersion": "",
"BuildTime": "2026-02-15T00:00:00.000000000+00:00",
"Components": [
{
"Name": "Sockerless",
"Version": "0.1.0",
"Details": {
"Backend": "<configured-backend-name>"
}
}
]
}Returns system information. Key fields that CI runners read:
| Field | Sockerless Value | Why |
|---|---|---|
OSType |
"linux" |
GitLab Runner uses this to detect container OS type |
Architecture |
"x86_64" |
Informational |
NCPU |
Backend-dependent | |
MemTotal |
Backend-dependent | |
ServerVersion |
"0.1.0" |
|
Swarm.LocalNodeState |
"inactive" |
No Swarm support |
Runtimes |
{"sockerless": {"path": "sockerless"}} |
|
DefaultRuntime |
"sockerless" |
|
SecurityOptions |
[] |
Query parameters:
fromImage(required): Image reference (e.g.,docker.io/library/nginx:latest)tag: Tag (often included infromImage)platform: OS/arch (e.g.,linux/amd64)
Headers:
X-Registry-Auth: Base64-encoded JSON credentials ({"username":"...","password":"...","serveraddress":"..."})
Response: Streaming JSON (one JSON object per line), Docker pull progress format:
{"status":"Pulling from library/nginx","id":"latest"}
{"status":"Pulling fs layer","progressDetail":{},"id":"abc123"}
{"status":"Download complete","progressDetail":{},"id":"abc123"}
{"status":"Pull complete","progressDetail":{},"id":"abc123"}
{"status":"Digest: sha256:..."}
{"status":"Status: Downloaded newer image for nginx:latest"}Sockerless behavior:
- Records the image reference in the internal state store
- Does NOT actually pull the image locally — the cloud backend will pull it when a container is created
- Validates that the image exists in the registry (HEAD request to registry API) if credentials are provided
- Progress output reflects registry/cloud resolution work where the backend can observe it; backends must not report fabricated layer metadata as if a local pull occurred
Returns image metadata. For sockerless, this will be a minimal response constructed from the registry manifest (or cached from a previous pull):
{
"Id": "sha256:<digest>",
"RepoTags": ["nginx:latest"],
"RepoDigests": ["nginx@sha256:..."],
"Created": "2026-01-01T00:00:00Z",
"Size": 0,
"VirtualSize": 0,
"Config": {
"Env": ["PATH=/usr/local/sbin:..."],
"Cmd": ["nginx", "-g", "daemon off;"],
"Entrypoint": null,
"ExposedPorts": {"80/tcp": {}},
"Labels": {},
"WorkingDir": ""
},
"Os": "linux",
"Architecture": "amd64"
}GitLab Runner reads Config.Env and Config.Cmd from this. If registry metadata retrieval is not available, return a minimal valid response. Image config can be fetched from the registry's manifest API without pulling layers.
Accepts a tar stream containing a Docker image. For sockerless:
- Extract image metadata from the tar
- Push the image to a configured registry (cloud backends pull from registries, not local stores)
- Or: record the image in internal state for later use
- GitLab Runner uses this for its helper image
Records a new tag for an existing image in the internal state. No cloud backend interaction needed.
Validates credentials against a registry. Used by GitHub Actions Runner's docker login.
Request body: {"username":"...","password":"...","serveraddress":"..."}
Response: {"Status": "Login Succeeded"}
Store credentials for later use with POST /images/create.
This is the most complex endpoint. Query parameter: name (optional container name).
Request body structure (fields actually used by CI runners):
{
"Image": "nginx:latest",
"Hostname": "runner-abc-project-1-concurrent-0",
"Cmd": ["sh", "-c", "echo hello"],
"Entrypoint": ["/bin/sh"],
"Env": ["CI=true", "GITLAB_CI=true"],
"Labels": {
"com.gitlab.gitlab-runner.managed": "true",
"com.docker.compose.project": "myapp"
},
"User": "",
"Tty": false,
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"OpenStdin": true,
"StdinOnce": true,
"ExposedPorts": {"80/tcp": {}},
"WorkingDir": "/app",
"HostConfig": {
"Binds": ["/builds:/builds", "/cache:/cache"],
"Memory": 536870912,
"NanoCPUs": 1000000000,
"NetworkMode": "<network-id>",
"PortBindings": {"80/tcp": [{"HostPort": "8080"}]},
"RestartPolicy": {"Name": "no"},
"ExtraHosts": ["service:10.0.0.2"],
"Dns": ["8.8.8.8"],
"Privileged": false,
"VolumesFrom": [],
"Tmpfs": {}
},
"NetworkingConfig": {
"EndpointsConfig": {
"<network-name>": {
"Aliases": ["postgres", "db"],
"MacAddress": "02:42:ac:11:00:02"
}
}
}
}Response:
{
"Id": "<64-char-hex-id>",
"Warnings": []
}Sockerless behavior:
- Generate a unique 64-character hex container ID
- Store the full container configuration in internal state
- Resolve
Bindsto cloud volume mappings (see Volume Emulation) - Resolve
NetworkModeandNetworkingConfigto cloud network mappings (see Network Emulation) - Do NOT start the cloud backend task yet (that happens on
POST /containers/{id}/start)
Fields that must be preserved in state (read back by inspect):
Image,Hostname,Cmd,Entrypoint,Env,Labels,User,TtyExposedPorts,WorkingDirHostConfig.*(all fields listed above)NetworkingConfig.*AttachStdin,AttachStdout,AttachStderr,OpenStdin,StdinOnce
Fields that can be preserved as inspect metadata only (not relevant to cloud backends unless the backend explicitly supports them):
MacAddress(v1.44 location: insideEndpointsConfig)CgroupParent,DeviceCgroupRules,PidMode,UTSModeSecurityOpt,CapAdd,CapDrop,UsernsModeShmSize,Sysctls,OomKillDisable,OomScoreAdjRuntime,Isolation,Init
Silently-ignored fields should still be stored and returned in inspect responses for client compatibility, but they do not need to affect cloud backend behavior.
Sockerless behavior:
- Translate the stored container configuration into a cloud backend task/job:
- Image → cloud task definition / job spec
- Env → cloud task environment variables
- Cmd/Entrypoint → cloud task command
- Resource limits → cloud task resource allocation
- Network → cloud VPC / network configuration
- Volume binds → cloud storage mounts
- Launch the cloud backend task
- Start the exec/attach agent inside the container (see Agent Protocol)
- Update container state to
Running
Response: 204 No Content (same as Docker)
Returns full container metadata. CI runners rely on specific fields:
| Field | GitLab Runner Reads | GitHub Runner Reads |
|---|---|---|
State.Status ("created", "running", "exited") |
Yes | — |
State.Running (bool) |
Yes | — |
State.ExitCode (int) |
Yes | — |
State.Health.Status ("healthy", "unhealthy", "starting") |
— | Yes |
Config.Env (array) |
— | Yes (PATH extraction) |
Config.Healthcheck (object) |
— | Yes (presence check) |
NetworkSettings.IPAddress |
Yes | — |
NetworkSettings.Networks.<name>.IPAddress |
Yes | — |
NetworkSettings.Ports |
— | Yes (port mapping) |
Response structure (key fields):
{
"Id": "<64-char-hex>",
"Created": "2026-02-15T12:00:00Z",
"Name": "/container-name",
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 1,
"ExitCode": 0,
"Error": "",
"StartedAt": "2026-02-15T12:00:01Z",
"FinishedAt": "0001-01-01T00:00:00Z",
"Health": {
"Status": "healthy",
"FailingStreak": 0,
"Log": []
}
},
"Config": {
"Image": "nginx:latest",
"Env": ["PATH=/usr/local/sbin:..."],
"Cmd": ["nginx"],
"Entrypoint": null,
"Hostname": "...",
"Labels": {},
"ExposedPorts": {},
"Tty": false,
"OpenStdin": false,
"Healthcheck": null,
"WorkingDir": ""
},
"HostConfig": { "..." },
"NetworkSettings": {
"IPAddress": "10.0.0.5",
"Ports": {
"80/tcp": [{"HostIp": "0.0.0.0", "HostPort": "8080"}]
},
"Networks": {
"bridge": {
"IPAddress": "10.0.0.5",
"Aliases": ["db"],
"NetworkID": "..."
}
}
}
}Query parameters:
all(bool): Include stopped containers (default: only running)filters(JSON): Label, status, id, name filters
Filter formats used by CI runners:
# GitLab Runner - find containers by label
filters={"label":["com.gitlab.gitlab-runner.managed=true"]}
# GitHub Actions Runner - find by label
filters={"label":["<instance-label>"]}
# GitHub Actions Runner - find by id + status
filters={"id":["<container-id>"],"status":["running"]}
Response: Array of container summary objects:
[
{
"Id": "<64-char-hex>",
"Names": ["/container-name"],
"Image": "nginx:latest",
"ImageID": "sha256:...",
"Command": "nginx -g 'daemon off;'",
"Created": 1708000000,
"State": "running",
"Status": "Up 5 minutes",
"Ports": [{"PrivatePort": 80, "PublicPort": 8080, "Type": "tcp"}],
"Labels": {"com.gitlab.gitlab-runner.managed": "true"},
"NetworkSettings": {
"Networks": {
"bridge": {"IPAddress": "10.0.0.5"}
}
}
}
]Sockerless must support filtering by: label, id, name, status.
Query parameters:
stdout(bool): Include stdoutstderr(bool): Include stderrfollow(bool): Stream logs (long-poll)timestamps(bool): Add RFC3339Nano timestampstail(string): Number of lines from end ("all" or number)details(bool): Include extra attributes
Response format: Multiplexed stream (when Tty: false, which is the CI runner case):
[stream_type: 1 byte][0x00 0x00 0x00][size: 4 bytes big-endian][payload: size bytes]
Where stream_type = 1 (stdout) or 2 (stderr).
Sockerless behavior:
- Fetch logs from the cloud backend's logging service (CloudWatch, Cloud Logging, Azure Monitor)
- Format into Docker's multiplexed stream protocol
- For
follow=true, keep the connection open and stream new log entries
GitLab Runner reads logs with: stdout=true, stderr=true, timestamps=true, follow=false (one-shot, 64KB limit).
GitHub Runner reads logs with: details=true, stdout=true, stderr=true.
This is a connection-hijacking endpoint. After the HTTP response headers, the connection is "hijacked" — it becomes a raw bidirectional byte stream.
Query parameters:
stream(bool): Stream attachedstdin(bool): Attach stdinstdout(bool): Attach stdoutstderr(bool): Attach stderr
Response: HTTP 101 Switching Protocols (or 200 with hijacked connection), then multiplexed stream.
How GitLab Runner uses attach:
- Creates container with
Cmdset to the script,OpenStdin: true - Calls
POST /containers/{id}/attachwithstream=true, stdin=true, stdout=true, stderr=trueBEFORE starting the container - Calls
POST /containers/{id}/start - Streams stdin (the build script) over the hijacked connection
- Reads stdout/stderr from the hijacked connection using the 8-byte multiplexed frame protocol
- Connection closes when container exits
This is the most critical endpoint for GitLab Runner compatibility. See Section 8 for implementation strategy.
Query parameter:
condition(string):not-running(used by GitLab Runner),next-exit,removed
Response (when container exits):
{
"StatusCode": 0,
"Error": null
}Long-polling endpoint. Blocks until the container reaches the specified condition. Sockerless polls the cloud backend task status and returns when the task completes.
Query parameter:
t(int): Seconds to wait before killing (default: 10)
Sends SIGTERM, waits t seconds, then SIGKILL. For sockerless, translates to the cloud backend's stop/terminate mechanism.
Response: 204 No Content
Query parameter:
signal(string): Signal to send (default:SIGKILL)
Immediate termination. For sockerless, translates to force-stopping the cloud backend task.
Response: 204 No Content
Query parameters:
v(bool): Remove associated anonymous volumesforce(bool): Kill and remove even if running
Response: 204 No Content
Sockerless: Remove the container from internal state. If the cloud backend task is still running and force=true, stop it first. Clean up any associated cloud resources (volumes, network attachments).
Request body:
{
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"Tty": false,
"Cmd": ["sh", "-c", "echo hello"],
"Env": ["FOO=bar"],
"WorkingDir": "/app"
}Response:
{
"Id": "<exec-id>"
}Records the exec configuration. Does not execute anything yet.
Request body:
{
"Detach": false,
"Tty": false
}Response: Connection hijack with multiplexed stream (same as attach). This is another connection-hijacking endpoint.
When Detach: false, the response hijacks the HTTP connection and streams stdin/stdout/stderr bidirectionally. The exec command runs inside the already-running container.
This is the primary mechanism for GitHub Actions Runner — every workflow step is docker exec into the job container.
See Section 8 for implementation strategy.
{
"ID": "<exec-id>",
"Running": false,
"ExitCode": 0,
"Pid": 0
}Request body:
{
"Name": "github_network_abc123",
"Labels": {"com.gitlab.gitlab-runner.managed": "true"},
"EnableIPv6": false,
"Driver": "bridge",
"Options": {
"com.docker.network.driver.mtu": "1500"
}
}Response:
{
"Id": "<64-char-hex>",
"Warning": ""
}Sockerless: Creates a logical network in internal state. The actual cloud networking is configured when containers join this network. See Network Emulation.
Query parameter: filters (JSON)
Filter formats used: {"label":["key=value"]}, {"name":["name"]}
Returns network details including connected containers:
{
"Name": "build-network",
"Id": "<hex>",
"Created": "2026-02-15T12:00:00Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Config": [{"Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1"}]
},
"Containers": {
"<container-id>": {
"Name": "postgres",
"IPv4Address": "172.18.0.2/16"
}
},
"Labels": {}
}{"Container": "<container-id>", "Force": true}Response: 204 No Content
Query parameter: filters (JSON, e.g., {"label":["key"]})
Response: {"NetworksDeleted": ["net1", "net2"]}
{
"Name": "runner-cache-sha256abc",
"Driver": "local",
"Labels": {"com.gitlab.gitlab-runner.managed": "true"}
}Response:
{
"Name": "runner-cache-sha256abc",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/runner-cache-sha256abc/_data",
"Labels": {"com.gitlab.gitlab-runner.managed": "true"},
"Scope": "local",
"CreatedAt": "2026-02-15T12:00:00Z"
}Sockerless: Creates cloud storage (see Volume Emulation) and records volume metadata.
Response:
{
"Volumes": [ ... ],
"Warnings": []
}Returns volume metadata (same shape as create response).
Query parameter: force (bool)
Response: 204 No Content
Sockerless: Remove cloud storage. If force=true, remove even if in use.
All requests may include a version prefix: /v1.44/containers/json. Sockerless accepts v1.44 and treats unversioned requests as v1.44. Requests for versions > 1.44 return 400 Bad Request with {"message":"client version 1.XX is too new. Maximum supported API version is 1.44"}.
When Tty: false (which is always the case for CI runners), attach and exec use Docker's multiplexed stream format:
+--------+--------+--------+--------+--------+--------+--------+--------+
| STREAM | 0x00 | 0x00 | 0x00 | SIZE1 | SIZE2 | SIZE3 | SIZE4 |
+--------+--------+--------+--------+--------+--------+--------+--------+
- Byte 0 (STREAM):
0= stdin,1= stdout,2= stderr - Bytes 1-3: Padding (zeros)
- Bytes 4-7: Payload size as big-endian uint32
- Following bytes: Payload (exactly
SIZEbytes)
GitLab Runner uses stdcopy.StdCopy from the Docker Go SDK to demultiplex this. Any incorrect framing breaks CI job output.
The attach and exec/start endpoints use HTTP connection hijacking:
- Client sends HTTP POST request
- Server responds with
101 Switching Protocols(or200 OKwithConnection: Upgrade) - The TCP connection is now a raw bidirectional byte stream
- Server writes multiplexed frames for stdout/stderr
- Client writes raw bytes for stdin
- Connection closes when the command/container exits
This is implemented differently from WebSocket — it uses Docker's custom protocol. The Content-Type response header is application/vnd.docker.raw-stream (or application/vnd.docker.multiplexed-stream).
All errors follow Docker's format:
{"message": "No such container: abc123"}Standard HTTP status codes:
200/201/204: Success304: Not modified (container already started/stopped)400: Bad request (invalid parameters)404: Not found (no such container/network/volume/image)409: Conflict (container already running, name in use)500: Server error
Sockerless is composed of three logical component types — frontend, backend, and agent. The backend and the agent are the shipped binaries (each its own Go module); the frontend — the Docker REST API surface — is served in-process by each backend on the same HTTP mux, not as a separate binary or process (see the packaging note in §6.3).
Any Docker-compatible client Cloud Provider API
(Docker CLI, Podman, SDKs, │
Compose, Testcontainers, etc.) │
│ │
│ Docker REST API v1.44 (Unix socket / TCP) │
▼ │
┌────────────────────────────┐ Internal HTTP/JSON ┌──────────────────────┐
│ Frontend │ ◄──────────────────► │ Backend │
│ (in-process layer) │ + WebSocket (stream) │ (separate binary) │
│ │ Unix socket / TCP │ │
│ • Stateless │ │ • Stateful │
│ • OpenAPI-generated types │ │ • Cloud SDK types │
│ • Mux stream protocol │ │ • Persistent state │
│ • Connection hijacking │ │ • Capability report │
│ • Bridges client ↔ agent │ │ • Launches tasks │
└──────────┬─────────────────┘ └──────────────────────┘
│
│ WebSocket (frontend connects TO agent, on demand)
│ Triggered by exec / attach / logs operations
│ Frontend must have network access to cloud containers
▼
┌──────────────────────────┐
│ Agent │ (inside cloud container, PID 1 or sidecar)
│ (separate binary) │
│ │
│ • WebSocket SERVER │
│ • Listens on port 9111 │
│ • Exec: fork+exec │
│ • Attach: pipe stdio │
│ • Health check runner │
│ • Standalone, no shared │
│ imports │
└──────────────────────────┘
Each component is a separate Go module with its own go.mod, enforcing dependency isolation at the build level. No component imports another component's code.
sockerless/
├── specs/ # Specifications and research docs
│ ├── SOCKERLESS_SPEC.md
│ └── DOCKER_REST_API.md
│
├── api/ # Module: shared internal API types
│ └── go.mod # Zero external dependencies
│
├── backends/
│ ├── core/ # Module: shared backend library
│ │ └── go.mod # Deps: api/, agent/, gorilla/websocket
│ ├── docker/ # Module: real Docker daemon backend
│ │ └── go.mod # Deps: api/, Docker client SDK
│ ├── ecs/ # Module: AWS ECS Fargate backend
│ │ └── go.mod # Deps: api/, AWS SDK v2
│ ├── lambda/ # Module: AWS Lambda backend
│ │ └── go.mod # Deps: api/, AWS SDK v2
│ ├── cloudrun/ # Module: Google Cloud Run backend
│ │ └── go.mod # Deps: api/, GCP SDK
│ ├── cloudrun-functions/ # Module: Google Cloud Run Functions
│ │ └── go.mod # Deps: api/, GCP SDK
│ ├── aca/ # Module: Azure Container Apps backend
│ │ └── go.mod # Deps: api/, Azure SDK
│ └── azure-functions/ # Module: Azure Functions backend
│ └── go.mod # Deps: api/, Azure SDK
│
├── agent/ # Module: sockerless-agent binary
│ └── go.mod # Deps: gorilla/websocket only
│ # NO api/ import — fully standalone
│
├── simulators/
│ ├── aws/ # Module: AWS API simulator (Lambda, ECS, ECR, CloudWatch)
│ ├── gcp/ # Module: GCP API simulator (Cloud Run, GCF, Artifact Registry, Logging)
│ └── azure/ # Module: Azure API simulator (ACA, AZF, ACR, Monitor)
│
├── tests/ # Module: black-box API tests
│ └── go.mod # Deps: Docker client SDK (as test client)
│
├── terraform/
│ └── modules/ # Terraform modules for cloud infrastructure
│ ├── ecs/
│ ├── lambda/
│ ├── cloudrun/
│ ├── gcf/
│ ├── aca/
│ └── azf/
│
└── Makefile
Sockerless Go modules and binaries:
| # | Module | Binary Name | External Dependencies |
|---|---|---|---|
| 1 | api/ |
(library — no binary) | None (stdlib only) |
| 3 | backends/core/ |
(library — no binary) | api/, agent/, gorilla/websocket |
| 4 | backends/docker/ |
sockerless-backend-docker |
api/, github.com/docker/docker client SDK |
| 6 | backends/ecs/ |
sockerless-backend-ecs |
api/, core/, AWS SDK v2 |
| 7 | backends/lambda/ |
sockerless-backend-lambda |
api/, core/, AWS SDK v2 |
| 8 | backends/cloudrun/ |
sockerless-backend-cloudrun |
api/, core/, GCP SDK |
| 9 | backends/cloudrun-functions/ |
sockerless-backend-gcf |
api/, core/, GCP SDK |
| 10 | backends/aca/ |
sockerless-backend-aca |
api/, core/, Azure SDK |
| 11 | backends/azure-functions/ |
sockerless-backend-azf |
api/, core/, Azure SDK |
| 12 | agent/ |
sockerless-agent |
gorilla/websocket only |
| 13 | simulators/aws/ |
sim-aws |
AWS SDK types (for request/response formats) |
| 15 | simulators/gcp/ |
sim-gcp |
GCP protobuf types |
| 16 | simulators/azure/ |
sim-azure |
Azure SDK types |
| 18 | tests/ |
(test binary — go test) |
Docker client SDK |
Packaging note: the Docker-API frontend is no longer a separate
sockerless-docker-frontendprocess. Each backend serves this Docker REST API surface in-process on its own HTTP mux (:3375) alongside its internal management endpoints — see ARCHITECTURE.md. This section describes that logical layer; it is not a separately-deployed binary.
The frontend is a stateless HTTP server that:
- Listens on a Unix socket (or TCP) and speaks Docker Engine REST API v1.44
- Accepts connections from any Docker-compatible client (Docker CLI, Podman remote, Docker SDKs in Python/Go/Node/Rust/.NET, Docker Compose, Testcontainers, etc.)
- Translates Docker REST API requests into internal API calls to the backend
- Handles the Docker-specific protocol features: multiplexed streams, connection hijacking,
X-Registry-Authheaders - On exec/attach requests, opens a WebSocket connection to the agent inside the cloud container and bridges the Docker client's hijacked connection to the agent's WebSocket
Types: Auto-generated from Docker's published OpenAPI/Swagger specification. No dependency on Docker's Go SDK. This ensures spec-compliance without coupling to any particular client implementation.
State: None persistent. Only ephemeral in-memory data: active WebSocket connections to agents, in-flight exec sessions.
Client compatibility: Because the frontend implements the Docker REST API from the OpenAPI spec (not from Docker's Go SDK), it is a standards-compliant server that works with any client speaking the Docker HTTP API — Docker CLI, Podman, any language SDK, or custom HTTP clients.
Each backend is a stateful daemon that:
- Listens on a Unix socket (or TCP) for internal API calls from the frontend
- Owns all state (containers, networks, volumes, images) in thread-safe in-memory maps (
sync.MapandStateStore[T]). State is ephemeral — lost on restart. No persistent database layer (SQLite was considered but not implemented). - Translates internal API calls into cloud provider operations using the provider's native SDK and types
- Reports its capabilities so the frontend can return
501 Not Implementedfor unsupported operations
State ownership: The backend is the single source of truth for all resource state:
| Object | State Fields |
|---|---|
| Container | ID, name, config, status, cloud task ID, agent address, IP, ports, timestamps, exit code, labels |
| Network | ID, name, labels, IPAM subnet, connected containers |
| Volume | ID, name, labels, cloud storage ID, mount references |
| Image | Reference, digest, config (env/cmd/entrypoint), pulled timestamp |
| Exec | ID, container ID, cmd, env, workdir, running, exit code |
Capability reporting: Each backend declares what it supports:
{
"exec": true,
"attach": true,
"logs_follow": true,
"volumes": true,
"networks": true,
"max_timeout_seconds": 0,
"health_checks": true
}FaaS backends (Lambda, Cloud Functions, Azure Functions) report exec: false, attach: false, max_timeout_seconds: 900 (or similar). The frontend uses this to return 501 for unsupported operations.
Docker backend special case: The Docker backend (backends/docker/) talks to a real Docker daemon. It does NOT need the agent — it uses Docker's native exec and attach APIs directly. This makes it the ideal reference backend for testing: run the same test suite against the Docker backend and cloud backends to verify behavior consistency.
The agent is a standalone binary that runs inside cloud containers. It is completely independent — no shared imports with the frontend, backend, or api/ module.
| Property | Value |
|---|---|
| Role | WebSocket server/client inside the cloud container |
| Port | Dials back to backend in reverse mode; ECS uses its cloud access path for exec |
| Auth | Token-based (SOCKERLESS_AGENT_TOKEN) |
| Lifetime | Runs for the container's lifetime as PID 1 (wrapping the user process) or as a sidecar |
| Binary size | Static binary, ~5-10 MB |
Exec transport modes:
| Mode | Direction | Used By |
|---|---|---|
| ECS SSM / cloud access path | Backend bridges Docker exec through ECS cloud APIs | ECS |
| Reverse agent | Agent dials back to backend via SOCKERLESS_CALLBACK_URL |
Cloud Run Services, ACA Apps, Lambda, GCF, Azure Functions |
Reverse agent is mandatory for FaaS/Service/App backends because those platforms do not expose a Docker exec endpoint. Missing callback registration is a startup/exec error, not a fallback trigger.
When the agent is NOT needed:
- Docker backend: uses Docker's native exec/attach
- No agent available: operations return errors
When the agent IS needed:
- All cloud backends (ECS, Cloud Run, ACA, Lambda, GCF, Azure Functions)
The backend uses a driver set for dispatching exec, filesystem, streaming, and network operations. Agent drivers route operations to connected agents (forward or reverse).
4 Driver Interfaces:
| Interface | Methods | Purpose |
|---|---|---|
ExecDriver |
Exec(containerID, execConfig) → (exitCode, error) |
Execute commands inside containers |
FilesystemDriver |
PutArchive, GetArchive, StatPath, RootPath |
Container filesystem access (docker cp) |
StreamDriver |
LogBytes, Attach |
Log retrieval and container attach |
NetworkDriver |
Create, Connect, Disconnect, Remove, List, Prune |
Container network isolation |
DriverSet on BaseServer holds the active drivers. InitDrivers() sets up agent drivers for all backends:
- Cloud backends: Agent drivers route to forward or reverse agent connections
- Docker backend: Does not use drivers (direct Docker SDK passthrough)
When no agent is connected, cloud backend operations that require an agent return errors. Cloud backends never auto-spawn local agents; the only accepted execution path is the cloud-deployed forward or reverse agent for that backend. The Docker passthrough backend remains the only backend allowed to use local auto-agent helpers.
┌─────────────────────────────────────────────────────────────────┐
│ Import Rules │
│ │
│ frontend ──imports──► api/ │
│ backend ──imports──► api/ │
│ agent ──imports──► nothing shared (fully standalone) │
│ tests ──imports──► Docker SDK (as test client, not api/) │
│ │
│ frontend NEVER imports backend or agent │
│ backend NEVER imports frontend or agent │
│ agent NEVER imports frontend, backend, or api/ │
│ No circular dependencies possible │
└─────────────────────────────────────────────────────────────────┘
The internal API mirrors the Docker REST API structure but without Docker-specific protocol quirks (no multiplexed streams, no connection hijacking, no X-Registry-Auth headers). It is defined by the api/ module using its own types (zero external dependencies).
The frontend translates: Docker types (OpenAPI-generated) ↔ api/ types ↔ internal HTTP/JSON
The backend translates: api/ types ↔ cloud SDK types
| Operation Type | Transport | Examples |
|---|---|---|
| CRUD (request-response) | HTTP/JSON | create/start/stop/inspect/list/remove containers, networks, volumes, images |
| Streaming (long-lived) | WebSocket | logs with follow=true, container wait |
Both share the same Unix socket / TCP address. WebSocket upgrades happen on specific endpoints.
All routes are prefixed with /internal/v1/.
| Method | Path | Maps to Docker API |
|---|---|---|
GET |
/internal/v1/capabilities |
(no Docker equivalent) |
GET |
/internal/v1/info |
GET /info |
GET |
/internal/v1/version |
GET /version |
| Method | Path | Maps to Docker API |
|---|---|---|
POST |
/internal/v1/containers |
POST /containers/create |
GET |
/internal/v1/containers |
GET /containers/json |
GET |
/internal/v1/containers/{id} |
GET /containers/{id}/json |
POST |
/internal/v1/containers/{id}/start |
POST /containers/{id}/start |
POST |
/internal/v1/containers/{id}/stop |
POST /containers/{id}/stop |
POST |
/internal/v1/containers/{id}/kill |
POST /containers/{id}/kill |
DELETE |
/internal/v1/containers/{id} |
DELETE /containers/{id} |
GET |
/internal/v1/containers/{id}/logs |
GET /containers/{id}/logs |
WS |
/internal/v1/containers/{id}/logs/stream |
GET /containers/{id}/logs?follow=true |
WS |
/internal/v1/containers/{id}/wait |
POST /containers/{id}/wait |
| Method | Path | Maps to Docker API |
|---|---|---|
POST |
/internal/v1/containers/{id}/exec |
POST /containers/{id}/exec |
GET |
/internal/v1/exec/{id} |
GET /exec/{id}/json |
Note: POST /exec/{id}/start is NOT proxied through the backend. The frontend handles exec streaming directly by connecting to the agent. The backend only stores exec metadata.
| Method | Path | Maps to Docker API |
|---|---|---|
POST |
/internal/v1/images/pull |
POST /images/create |
GET |
/internal/v1/images/{name} |
GET /images/{name}/json |
POST |
/internal/v1/images/load |
POST /images/load |
POST |
/internal/v1/images/{name}/tag |
POST /images/{name}/tag |
POST |
/internal/v1/auth |
POST /auth |
| Method | Path | Maps to Docker API |
|---|---|---|
POST |
/internal/v1/networks |
POST /networks/create |
GET |
/internal/v1/networks |
GET /networks |
GET |
/internal/v1/networks/{id} |
GET /networks/{id} |
POST |
/internal/v1/networks/{id}/disconnect |
POST /networks/{id}/disconnect |
DELETE |
/internal/v1/networks/{id} |
DELETE /networks/{id} |
POST |
/internal/v1/networks/prune |
POST /networks/prune |
| Method | Path | Maps to Docker API |
|---|---|---|
POST |
/internal/v1/volumes |
POST /volumes/create |
GET |
/internal/v1/volumes |
GET /volumes |
GET |
/internal/v1/volumes/{name} |
GET /volumes/{name} |
DELETE |
/internal/v1/volumes/{name} |
DELETE /volumes/{name} |
GET /internal/v1/capabilities returns the backend's supported features:
{
"backend": "ecs",
"version": "0.1.0",
"capabilities": {
"exec": true,
"attach": true,
"logs": true,
"logs_follow": true,
"volumes": true,
"networks": true,
"health_checks": true,
"image_pull": true,
"image_load": false,
"max_timeout_seconds": 0,
"agent_required": true
}
}The frontend calls this once at startup (and caches it). When a Docker API request maps to an unsupported capability, the frontend returns 501 Not Implemented with {"message":"This backend (lambda) does not support exec"}.
When the backend starts a container, it provisions the agent and records the agent's address. The container inspect response includes an agent_address field:
{
"id": "abc123...",
"agent_address": "10.0.1.47:9111",
"status": "running",
"...": "..."
}The frontend reads this address when it needs to connect to the agent for exec/attach operations.
The agent runs inside the cloud container and listens for WebSocket connections from the frontend. The frontend connects on demand — only when a user triggers an exec, attach, or streaming logs operation.
User: docker exec container-1 sh
│
▼
Frontend Agent (in cloud container)
│ │
├── GET /internal/v1/containers/{id} ──→ Backend │
│ (gets agent_address: 10.0.1.47:9111) │
│ │
├── WebSocket CONNECT ──────────────────────────→ │ ws://10.0.1.47:9111/
│ Authorization: Bearer <token> │
│ │
├── {"type":"exec","cmd":["sh"],...} ───────────→ │
│ │ (agent fork+exec "sh")
│ │
│ ←── {"type":"stdout","data":"$ "} ──────────── │
│ ──── {"type":"stdin","data":"ls\n"} ──────────→ │
│ ←── {"type":"stdout","data":"file1 file2\n"} ─ │
│ │
│ ←── {"type":"exit","code":0} ────────────────── │
│ │
├── WebSocket CLOSE ────────────────────────────→ │
The backend generates a one-time token when launching the cloud task. This token is:
- Injected into the container as env var
SOCKERLESS_AGENT_TOKEN - Returned to the frontend as part of the container's inspect data
When the frontend connects to the agent, it presents the token in the Authorization: Bearer <token> header. The agent validates the token and rejects unauthorized connections.
All messages are JSON. Each message has a type field.
Frontend → Agent:
| Type | Fields | Purpose |
|---|---|---|
exec |
id, cmd, env, workdir, tty |
Fork a new process |
attach |
id |
Attach to the main process (PID 1's child) |
stdin |
id, data (base64) |
Pipe bytes to process stdin |
signal |
id, signal |
Send signal (SIGTERM, SIGKILL) to process |
close_stdin |
id |
Close process stdin (EOF) |
resize |
id, width, height |
Resize TTY (future) |
Agent → Frontend:
| Type | Fields | Purpose |
|---|---|---|
stdout |
id, data (base64) |
Process stdout bytes |
stderr |
id, data (base64) |
Process stderr bytes |
exit |
id, code |
Process exited with code |
error |
id, message |
Error (process not found, etc.) |
health |
status, log |
Health check result |
The id field identifies the exec/attach session (supports multiple concurrent sessions over the same WebSocket).
GitLab Runner attaches BEFORE starting the container. The frontend buffers the attach until the container starts and the agent becomes reachable:
GitLab Runner Frontend Backend Agent
│ │ │ │
├── POST /containers/create → │ ── POST /internal/... → │ │
│ ←── {Id: "abc"} ────────── │ ←── {id: "abc"} ──────── │ │
│ │ │ │
├── POST /containers/abc/ │ │ │
│ attach ─────────────────→ │ (buffer hijacked conn) │ │
│ (hijacked connection) │ │ │
│ │ │ │
├── POST /containers/abc/ │ │ │
│ start ──────────────────→ │ ── POST .../start ────→ │ ── launch task ─→ │
│ │ │ │
│ │ ←── {agent_address} ──── │ │
│ │ │ │
│ │ ── ws://agent:9111 ────────────────────────→ │
│ │ ── {"type":"attach"} ──────────────────────→ │
│ │ │ │
│ ←── mux stdout ─────────── │ ←── {"type":"stdout"} ────────────────────── │
│ ──── stdin ───────────────→ │ ──── {"type":"stdin"} ────────────────────→ │
│ │ │ │
│ (connection closes on exit)│ │ │
The frontend is responsible for:
- Buffering the attach request until the agent is reachable
- Translating between Docker's multiplexed stream protocol (8-byte header framing) and the agent's WebSocket JSON messages
- Bridging the hijacked HTTP connection to the WebSocket connection
GitHub Runner Frontend Backend Agent
│ │ │ │
│ (container already running with "tail -f /dev/null") │ │
│ │ │ │
├── POST /containers/abc/ │ │ │
│ exec ──────────────────→ │ ── POST .../exec ──────→ │ (store exec meta) │
│ ←── {Id: "exec-1"} ────── │ ←── {id: "exec-1"} ───── │ │
│ │ │ │
├── POST /exec/exec-1/start → │ │ │
│ (hijacked connection) │ │ │
│ │ ── ws://agent:9111 ────────────────────────→ │
│ │ ── {"type":"exec", │ │
│ │ "cmd":["sh","-c", │ │
│ │ "script"]} ────────────────────────────→ │
│ │ │ │
│ ←── mux stdout ─────────── │ ←── {"type":"stdout"} ────────────────────── │
│ ──── stdin ───────────────→ │ ──── {"type":"stdin"} ────────────────────→ │
│ ←── mux stderr ─────────── │ ←── {"type":"stderr"} ────────────────────── │
│ │ │ │
│ ←── exit code ──────────── │ ←── {"type":"exit","code":0} ────────────── │
The backend is responsible for injecting the agent into cloud containers:
| Method | Description | Best For |
|---|---|---|
| Volume mount | Mount agent binary from cloud storage (EFS, GCS, Azure Files) and prepend to entrypoint | ECS, ACA |
| Init container | Run init container that copies agent binary to shared volume | ECS, ACA |
| Image layer | Backend builds a wrapper image with agent included | Cloud Run (no volume mount at startup) |
| Not needed | Backend has native exec (Docker backend) | Docker, Memory |
The backend modifies the container's entrypoint to: ["/sockerless-agent", "--", <original-entrypoint>]
The agent starts the original command as a child process and serves WebSocket connections for exec/attach.
These support long-running containers with logging. Best suited for CI runner workloads.
| Docker Concept | ECS Mapping |
|---|---|
| Container create | PendingCreates until start; task definition materialized from cloud-backed config |
| Container start | RunTask (Fargate launch type); polls until RUNNING |
| Container stop | StopTask |
| Container kill | StopTask (ECS has no SIGKILL; tasks get 30s SIGTERM then forced stop) |
| Container remove | StopTask (if running) + DeregisterTaskDefinition |
| Container inspect | ECS task/task-definition/tags via cloud state provider |
| Container list | ECS ListTasks / DescribeTasks filtered by tags/labels |
| Container logs | CloudWatch Logs (GetLogEvents / FilterLogEvents) |
| Exec / Attach | ECS ExecuteCommand / configured cloud access path |
| Image pull | Records ref in state; ECS pulls from ECR / Docker Hub at RunTask time |
| Network | VPC Security Groups + Cloud Map |
| Volume | EFS-backed volumes where configured |
| Health check | Agent-executed health checks reported to backend |
Exec networking: Fargate tasks must have ECS ExecuteCommand prerequisites configured (SSM permissions, task platform support, and network egress to SSM endpoints).
Startup latency: 10-45 seconds (image pull + Fargate capacity allocation).
| Docker Concept | Cloud Run Mapping |
|---|---|
| Container create | PendingCreates until start |
| Container start | CreateJob + RunJob for one-shot containers, or Cloud Run Service create/update for runner workloads |
| Container stop/kill | CancelExecution |
| Container remove | DeleteJob or DeleteService |
| Container inspect/list | Cloud Run Jobs/Services queried by labels |
| Container logs | Cloud Logging via Log Admin API |
| Exec / Attach | Reverse agent WebSocket for Service-backed workloads |
| Image pull | Records ref; Cloud Run pulls from Artifact Registry/GCR/Docker Hub at job creation |
| Network | Cloud DNS / Service materialization and VPC connector where configured |
| Volume | GCS-backed storage or memory tmpfs depending on backing |
Requires SOCKERLESS_CALLBACK_URL for the Service-backed reverse-agent path.
Startup latency: 5-30 seconds.
| Docker Concept | ACA Mapping |
|---|---|
| Container create | PendingCreates until start |
| Container start | Job create/start for one-shot containers, or ACA App create/update for runner workloads |
| Container stop/kill | BeginStopExecution |
| Container remove | BeginDelete (Job) |
| Container inspect/list | ACA Jobs/Apps queried by tags |
| Container logs | Azure Monitor Log Analytics (QueryWorkspace) |
| Exec / Attach | Reverse agent WebSocket for App-backed workloads |
| Image pull | Records ref; ACA pulls from ACR/Docker Hub at job creation |
| Network | ACA managed environment networking + Private DNS |
| Volume | Azure Files-backed volumes where configured |
Requires SOCKERLESS_CALLBACK_URL for the App-backed reverse-agent path.
Startup latency: 10-60 seconds.
FaaS backends use reverse agent exclusively: the agent inside the function dials back to the backend via SOCKERLESS_CALLBACK_URL (WebSocket). This enables exec and attach for the duration of the function invocation. Helper and cache containers auto-stop after 500ms.
FaaS backends route container variants differently:
container-action→container-action-faas(reverse agent lifecycle)
| Docker Concept | Lambda Mapping |
|---|---|
| Container create | CreateFunction (container image from ECR) |
| Container start | Invoke (async); agent inside function calls back |
| Container stop | Disconnects reverse agent (function runs to completion) |
| Container kill | Disconnects reverse agent |
| Container remove | DeleteFunction |
| Container logs | CloudWatch Logs GetLogEvents |
| Exec / Attach | Via reverse agent (agent dials back to backend) |
| Image pull | Records ref; Lambda pulls from ECR at function create |
| Max timeout | 15 minutes |
Capabilities: exec: true (via reverse agent), attach: true (via reverse agent), logs: true, logs_follow: false, max_timeout_seconds: 900
| Docker Concept | Cloud Run Functions Mapping |
|---|---|
| Container create | CreateFunction (2nd gen, Docker runtime) — synchronous, 1-3 min |
| Container start | HTTP POST invoke (async); agent inside function calls back |
| Container stop | No-op (runs to completion) |
| Container kill | Disconnects reverse agent |
| Container remove | DeleteFunction |
| Container logs | Cloud Logging via Log Admin API |
| Exec / Attach | Via reverse agent |
| Image pull | Records ref; Cloud Functions pulls from Artifact Registry |
| Max timeout | 60 minutes (2nd gen) |
Capabilities: exec: true (via reverse agent), attach: true (via reverse agent), logs: true, logs_follow: false, max_timeout_seconds: 3600
| Docker Concept | Azure Functions Mapping |
|---|---|
| Container create | Create App Service Plan + Function App (custom container) |
| Container start | Start Function App; agent inside function calls back |
| Container stop | Stop Function App |
| Container kill | Disconnects reverse agent |
| Container remove | Delete Function App + App Service Plan |
| Container logs | Azure Monitor Log Analytics query |
| Exec / Attach | Via reverse agent |
| Image pull | Records ref; Functions pull from ACR at deploy |
| Max timeout | Function timeout configurable (default 600s) |
Capabilities: exec: true (via reverse agent), attach: true (via reverse agent), logs: true, logs_follow: false, max_timeout_seconds: 600
Note: While FaaS backends now support exec/attach via reverse agent, they are still not compatible with CI runners due to timeout limits, lack of persistent shared volumes, and the reverse agent lifecycle model. They are useful for short-lived, fire-and-forget container workloads.
| Docker Concept | Docker Mapping |
|---|---|
| Container create + start | POST /containers/create + POST /containers/{id}/start on real Docker daemon |
| Exec / Attach | Docker's native exec and attach (no agent needed) |
| Everything else | 1:1 passthrough to Docker daemon |
Capabilities: exec: true, attach: true, logs: true, logs_follow: true, volumes: true, networks: true, health_checks: true, agent_required: false
The Docker backend is the reference implementation for testing. Running the test suite against the Docker backend validates that sockerless produces responses identical to a real Docker daemon.
| Capability | Docker | ECS | Cloud Run | ACA | Lambda | CR Func | Az Func |
|---|---|---|---|---|---|---|---|
| Long-running | Yes | Yes | Yes (24h) | Yes | No (15m) | No (60m) | No (10m) |
| Exec | Yes | Yes* | Yes* | Yes* | Yes** | Yes** | Yes** |
| Attach | Yes | Yes* | Yes* | Yes* | Yes** | Yes** | Yes** |
| Log stream | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Log follow | Yes | Yes | Yes | Yes | No | No | No |
| Volumes | Yes | Yes | Partial | Yes | No | No | No |
| Networks | Yes | Yes | Yes | Yes | No | No | No |
| Health checks | Yes | Yes | Yes | Yes | No | No | No |
| Docker build | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Archive (docker cp) | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Agent mode | N/A | SSM/cloud access | Reverse | Reverse | Reverse | Reverse | Reverse |
| Agent needed | No | Yes | Yes | Yes | Yes | Yes | Yes |
| Startup latency | <1s | 10-45s | 5-30s | 10-60s | 1-5s | 1-5s | 1-5s |
* ECS uses ExecuteCommand / cloud access; Cloud Run and ACA use reverse-agent Service/App paths. ** Via reverse agent (agent inside function dials back to backend)
GitLab Runner creates volumes for:
/builds/<namespace>/<project>— shared between helper and build containers (git clone, build artifacts)/cache— persistent cache across jobs- Additional user-configured volumes
Both containers mount the same volume for data sharing. This is critical for the helper→build container handoff pattern.
| Volume Type | Cloud Emulation | Used For |
|---|---|---|
Named volumes (Docker VolumeCreate) |
Cloud shared filesystem (EFS, GCS, Azure Files) | Caches |
Bind mounts (from HostConfig.Binds) |
Cloud shared filesystem mounted at specified path | Build data sharing |
VolumesFrom (inherit from another container) |
Same shared filesystem mount applied to multiple tasks | Helper↔Build sharing |
| Anonymous volumes | Ephemeral task storage | Temp data |
Note: Cloud backends map Docker volumes to cloud storage primitives through their storage drivers. The cloud resource or configured storage backing is the source of truth; core bookkeeping is not authoritative for cloud state.
- Pre-provisioned EFS filesystem referenced in task definitions
- EFS access points configured at infrastructure level
- Multiple Fargate tasks mount the same access point → shared data
- Pre-provisioned GCS bucket or Filestore instance
- Mounted via Cloud Storage FUSE in the container at infrastructure level
- Pre-provisioned Azure Files share
- Mounted in container app job at infrastructure level
| Docker Operation | Sockerless Action |
|---|---|
POST /volumes/create |
Create cloud storage resource (or logical directory in shared filesystem) |
Bind mount in POST /containers/create |
Record mount mapping; apply when launching cloud task |
VolumesFrom in POST /containers/create |
Copy mount list from referenced container's config |
DELETE /volumes/{name} |
Delete cloud storage resource |
CI runners create per-job networks so that:
- Service containers (postgres, redis, etc.) and the build container can communicate
- DNS resolution works for service aliases (e.g.,
postgresresolves to the postgres container's IP) - Job isolation — containers from different jobs cannot communicate
Note: Cloud backends map Docker networks to cloud networking and discovery primitives where the cloud exposes them.
specs/CLOUD_RESOURCE_MAPPING.mdis authoritative for the per-backend mapping. Backends that cannot provide a Docker network primitive fail explicitly or expose only the cloud-supported subset.
| Cloud Backend | Network Emulation |
|---|---|
| AWS ECS | VPC security groups + Cloud Map/private DNS when configured |
| Google Cloud Run | Cloud Run Service materialization + Cloud DNS/VPC connector where configured |
| Azure Container Apps | ACA managed environment + Private DNS/NSG where configured |
| Lambda / AZF | No native Docker bridge; service-mesh/private-DNS options only where configured |
Sockerless reports network metadata from the backend's cloud mapping. Cloud-assigned addresses, service DNS names, or explicit unsupported fields are preferred over invented Docker-bridge addresses.
For DNS-based service discovery (which is what CI runners actually rely on), the container aliases (e.g., postgres, redis) are registered in the cloud's DNS service so they resolve to the actual cloud IPs.
| Docker Operation | Sockerless Action |
|---|---|
POST /networks/create |
Create or record the backend's cloud network/discovery resource |
Container create with NetworkMode/EndpointsConfig |
Record aliases and attach to the backend's cloud network/discovery mapping |
POST /networks/{id}/disconnect |
Detach from the cloud network/discovery mapping where supported |
DELETE /networks/{id} |
Remove the cloud network/discovery resource where supported |
POST /networks/prune |
Remove networks with no connected containers |
GitLab Runner uses ExtraHosts when FF_NETWORK_PER_BUILD is disabled (legacy mode). Sockerless passes these as environment entries or injects them into /etc/hosts via the exec agent.
Docker Compose v2 communicates with the Docker daemon via the same REST API. It does NOT use any special endpoints. The compose operations map to our supported endpoints:
| Compose Command | Docker API Calls |
|---|---|
docker compose up |
Pull images → Create networks → Create volumes → Create containers → Start containers |
docker compose down |
Stop containers → Remove containers → Remove networks → (optionally remove volumes) |
docker compose ps |
List containers filtered by com.docker.compose.project label |
docker compose logs |
Get container logs for each service |
Docker Compose sets these labels on all objects. Sockerless must store and filter by them:
| Label | Purpose |
|---|---|
com.docker.compose.project |
Project name (directory name or -p flag) |
com.docker.compose.service |
Service name from compose file |
com.docker.compose.container-number |
Instance number (for scale) |
com.docker.compose.oneoff |
"True" for docker compose run containers |
com.docker.compose.project.config_files |
Compose file paths |
com.docker.compose.project.working_dir |
Project working directory |
docker compose up with depends_on and health checks uses:
POST /containers/createwithHealthcheckconfig from compose fileGET /containers/{id}/jsonpolling.State.Health.Statusuntil"healthy"- Sequential container startup based on dependency graph
Sockerless must support the health check polling pattern. The sockerless agent can run health check commands inside the container and report status.
This section provides a deep analysis of how GitLab Runner docker-executor and GitHub Actions Runner interact with the Docker API, identifies every gap that arises when translating these interactions to cloud backends, and proposes concrete solutions. The analysis is based on source code examination of both runners.
GitLab Runner's Docker client interface (helpers/docker/client.go) defines exactly 32 methods. The complete list of Docker API calls the runner can make:
| Category | API Calls |
|---|---|
| Version | ClientVersion(), ServerVersion() |
| Images | ImageInspectWithRaw, ImagePullBlocking, ImageImportBlocking, ImageLoad, ImageTag |
| Containers | ContainerList, ContainerCreate, ContainerStart, ContainerKill, ContainerStop, ContainerInspect, ContainerAttach, ContainerRemove, ContainerWait, ContainerLogs, ContainerExecCreate, ContainerExecAttach |
| Networks | NetworkCreate, NetworkRemove, NetworkDisconnect, NetworkList, NetworkInspect |
| Volumes | VolumeCreate, VolumeRemove, VolumeInspect, VolumeList |
| System | Info() |
Not used: ContainerRestart, ContainerPause/Unpause, ContainerRename, ContainerUpdate, ContainerTop, ContainerStats, ContainerArchive/CopyTo/CopyFrom, ContainerExecStart (uses ContainerExecAttach instead), ContainerExecInspect, ImageList, ImageRemove, ImagePush, ImageBuild, any Swarm/Plugin/Secret/Config endpoints.
1. API Version Negotiation
GET /_ping → reads API-Version header
WithAPIVersionNegotiation() adjusts all subsequent calls
2. Image Pull Phase
ImageInspectWithRaw(helperImage) # check if helper exists locally
ImageLoad(helperTar) → ImageTag(id, ref) # or: load from embedded tar
ImagePullBlocking(buildImage) # pull build image
ImagePullBlocking(serviceImage) # pull each service image
3. Network Setup
NetworkCreate("runner-net-<guid>") # per-build network (FF_NETWORK_PER_BUILD)
4. Volume Setup
VolumeCreate("runner-<hash>-cache-<hash>") # cache volume
VolumeCreate("runner-<hash>-build-<hash>") # build dir volume (if cache_dir unset)
5. Service Containers
For each service:
ContainerCreate(serviceImage, network, volumes, aliases)
ContainerStart(serviceID)
ContainerInspect(serviceID) # poll State.Status != "created"
# Health check: create SEPARATE helper container running
# ["gitlab-runner-helper", "health-check"] to TCP-probe service ports
6. Build Container
ContainerCreate(buildImage, cmd=[shell], entrypoint, network, volumes, stdin=true)
ContainerAttach(buildID, stream=true, stdin=true, stdout=true, stderr=true) # BEFORE START
ContainerStart(buildID)
# Stdin: pipe build script
# Stdout/Stderr: read via multiplexed stream (stdcopy.StdCopy)
ContainerWait(buildID, condition="not-running")
7. Helper Container (same as build, used for git clone, artifacts, cache)
ContainerCreate(helperImage, cmd=["gitlab-runner-build"], network, SAME volumes)
ContainerAttach(helperID, ...) # BEFORE START
ContainerStart(helperID)
ContainerWait(helperID, ...)
8. Cleanup (5-minute timeout, parallel)
For each container:
ContainerKill(id, "SIGTERM") # or: execScriptOnContainer to send SIGTERM
ContainerStop(id, timeout=0)
NetworkDisconnect(networkID, id, force=true)
ContainerRemove(id, force=true, removeVolumes=true)
NetworkRemove(networkID)
VolumeRemove(volumeID)
Attach-before-start pattern:
The Exec method in executors/docker/internal/exec/exec.go calls ContainerAttach() BEFORE ContainerStart(). Docker's API returns the hijacked connection immediately — it registers interest in the stream before the container is running. The runner then calls ContainerStart(), and the attach stream begins receiving output once the process starts. There is no explicit timeout on the attach call beyond the job-level timeout (from .gitlab-ci.yml). The connection can sit idle for minutes waiting for container output.
Build script execution via attach (NOT exec):
In the normal flow, the runner pipes the build script through the attach stdin connection. The container's Cmd is set to the shell command, and the script is streamed as stdin. This means attach is the primary execution mechanism, not exec.
Exec usage is limited:
Exec is used only for: (1) sending SIGTERM to container processes during cleanup (execScriptOnContainer runs sh -c <sigterm-script>), and (2) the newer CI Steps/Functions mode (ContainerExecCreate + ContainerExecAttach). Note: the runner uses ContainerExecAttach (which combines start + attach into one hijacked connection), NOT ContainerExecStart as a separate call.
Helper image loading:
The runner tries three strategies in order: (1) ImageInspectWithRaw to check if image exists, (2) ImageLoad from embedded .docker.tar.zst file + ImageTag, (3) ImagePullBlocking from registry.gitlab.com. After loading from tar, it reads the JSON response stream for "Loaded image:" to get the image ID, then tags it. The helper_image config setting bypasses tar loading entirely and pulls from a registry.
Volume sharing:
ALL containers in the job (helper, build, services) get the same Binds list from e.volumesManager.Binds(). This includes named volumes for /builds/<namespace>/<project> and /cache. The helper container clones the repo into the build volume; the build container reads it from the same volume.
Service readiness:
The runner does NOT use Docker's built-in HEALTHCHECK / State.Health. Instead, it creates a SEPARATE health-check container using the helper image, running ["gitlab-runner-helper", "health-check"]. This container TCP-probes the service's exposed ports. Before probing, it polls ContainerInspect until State.Status is no longer "created". The wait_for_services_timeout setting (default 30s) controls the overall timeout.
Network aliases:
With FF_NETWORK_PER_BUILD enabled (recommended), the runner creates a per-build user-defined bridge network. Service aliases (e.g., ["postgres", "db"]) are set via NetworkingConfig.EndpointsConfig[networkName].Aliases. Docker's embedded DNS resolves these aliases within the network. Without this flag (legacy), aliases use ExtraHosts entries injected into /etc/hosts.
API version negotiation:
Uses WithAPIVersionNegotiation() which sends GET /_ping, reads the API-Version header, and uses the min of client/server versions. Has version-aware code paths: v1.44+ puts MAC address in EndpointsConfig, pre-v1.44 puts it in Config.MacAddress. HTTP timeouts: TLS handshake 60s, response headers 120s, dialer 300s.
Cleanup:
Uses a dedicated context with 5-minute timeout. Removes all tracked containers in parallel via goroutines + sync.WaitGroup. Sequence: kill → stop → network disconnect → remove → remove volumes → remove network.
| Requirement | Priority | Sockerless Solution |
|---|---|---|
GET /_ping with API-Version: 1.44 header |
P0 | Frontend responds immediately, no backend call |
WithAPIVersionNegotiation() |
P0 | Return correct header; frontend adapts if client requests older version |
ImagePullBlocking with X-Registry-Auth |
P0 | Record image ref + creds; cloud backend pulls from registry at start |
ImageLoad (tar) + ImageTag |
P1 | Accept tar, push to configured registry (or recommend helper_image config) |
ImageImportBlocking (.tar.xz) |
P1 | Same as ImageLoad |
ImageInspectWithRaw with Config.Env, Config.Cmd, Config.Entrypoint, Config.ExposedPorts |
P0 | Fetch image config from registry manifest API (no layer download needed) |
ContainerCreate with full HostConfig, NetworkingConfig, stdin options |
P0 | Store all fields; translate to cloud task config on start |
ContainerAttach before ContainerStart |
P0 | Frontend returns hijacked connection immediately; buffers until agent reachable |
ContainerStart |
P0 | Launch cloud task with agent; block until container is running |
ContainerWait with condition=not-running |
P0 | Frontend long-polls backend until cloud task exits |
ContainerInspect with State.Status, State.Running, State.ExitCode, NetworkSettings.IPAddress, NetworkSettings.Networks |
P0 | Backend returns current state from cloud task status |
ContainerLogs with stdout=true, stderr=true, timestamps=true |
P0 | Fetch from cloud logging (CloudWatch, Cloud Logging, Azure Monitor) |
ContainerExecCreate + ContainerExecAttach |
P1 | Agent handles exec with hijacked bidirectional stream |
ContainerKill / ContainerStop / ContainerRemove (force) |
P0 | Translate to cloud task stop/delete |
NetworkCreate with labels, NetworkRemove, NetworkDisconnect |
P0 | Cloud network emulation (VPC, service discovery) |
NetworkList, NetworkInspect (with Containers map) |
P0 | Backend tracks container-to-network membership |
VolumeCreate (named), VolumeRemove, VolumeInspect, VolumeList |
P0 | Cloud shared filesystem (EFS, GCS, Azure Files) |
EndpointsConfig.Aliases for service DNS resolution |
P0 | Cloud DNS service discovery (Cloud Map, Cloud DNS, ACA DNS) |
Info() with OSType: "linux" |
P0 | Frontend returns static info |
| Multiplexed stream protocol (8-byte header framing) | P0 | Frontend handles framing; agent sends raw stdout/stderr |
Recommended GitLab Runner configuration for sockerless:
[runners.docker]
host = "unix:///var/run/sockerless.sock" # Point to sockerless frontend
helper_image = "registry.example.com/sockerless/gitlab-runner-helper:latest" # Avoid tar loading
pull_policy = "always" # Cloud backends always pull
network_mtu = 1500 # Explicit MTU
wait_for_services_timeout = 120 # Allow for cloud startup latency
[runners.feature_flags]
FF_NETWORK_PER_BUILD = true # Use user-defined networks (required)GitHub Actions Runner shells out to the docker CLI (not the REST API directly). The runner's DockerCommandManager.cs wraps these CLI commands, which map to REST API calls:
| CLI Command | REST API Endpoint |
|---|---|
docker version |
GET /version |
docker pull <image> |
POST /images/create |
docker login |
POST /auth |
docker create --entrypoint ... --network ... -v ... -e ... --label ... <image> <args> |
POST /containers/create |
docker start <id> |
POST /containers/{id}/start |
docker ps --filter ... |
GET /containers/json?filters=... |
docker inspect <id> (full JSON + --format templates) |
GET /containers/{id}/json |
docker exec -i --workdir ... -e ... <id> <cmd> |
POST /containers/{id}/exec + POST /exec/{id}/start |
docker logs --details --stdout --stderr <id> |
GET /containers/{id}/logs |
docker port <id> |
Derived from GET /containers/{id}/json → NetworkSettings.Ports |
docker rm --force <id> |
DELETE /containers/{id}?force=true |
docker network create --label ... <name> |
POST /networks/create |
docker network rm <name> |
DELETE /networks/{id} |
docker network prune --filter label=... |
POST /networks/prune |
docker wait <id> (container actions only) |
POST /containers/{id}/wait |
docker logs --follow <id> (container actions only) |
GET /containers/{id}/logs?follow=true |
Not used: docker stop, docker kill, docker attach, docker volume *, docker build, docker push, any Swarm/Plugin commands.
1. Version Check
docker version → GET /version (requires Server.APIVersion ≥ 1.35)
2. Network Setup
docker network create --label <hash> github_network_<GUID>
# Failure is FATAL — no fallback to default bridge
3. Image Pull (with retries, 3 attempts)
docker pull <job-image>
docker pull <service-image> # for each service
4. Container Creation
# Job container (long-lived):
docker create --entrypoint "tail" --name <name> --network <net> \
-v "/var/run/docker.sock:/var/run/docker.sock" \
-v <workspace-mounts> -e <env-vars> --label <labels> \
<image> "-f" "/dev/null"
# Service containers:
docker create --name <name> --network <net> \
-e <env-vars> --label <labels> --health-cmd ... \
<service-image>
5. Container Start
docker start <id>
docker ps --filter id=<id> --filter status=running # verify running
6. Inspect & Setup
docker inspect <id> # full JSON
# Parse Config.Env → extract PATH value
# Parse Config.Healthcheck → determine if health polling needed
7. Health Check Polling (for services with HEALTHCHECK)
docker inspect --format '{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}'
# Exponential backoff: 2s, 3s, 7s, 13s... (~5-6 retries)
# If no HEALTHCHECK defined → container considered ready immediately
8. Port Discovery (for services)
docker port <id>
# Parses: "5432/tcp -> 0.0.0.0:32768"
# Populates job.services.<name>.ports[<containerPort>] context
9. Step Execution (for each workflow step)
docker exec -i --workdir <path> -e VAR1=val1 -e VAR2=val2 \
<container-id> bash -e /path/to/step-script.sh
# Step script is volume-mounted, not piped via stdin
# Exit code from `docker exec` process = step result
# NO retry on exec failure — immediate step failure
10. Cleanup
docker rm --force <job-container> # kill + remove
docker rm --force <service-container> # for each
docker network rm <network-name>
docker network prune --filter label=<hash>
Container actions use a fundamentally different pattern from container jobs:
1. docker create with ORIGINAL entrypoint (NOT overridden to tail)
2. docker start
3. docker logs --follow (stream output)
4. docker wait (get exit code from container's own process)
5. docker rm
The container runs its own entrypoint to completion. Exit code comes from docker wait (the container's exit code, not exec). Volume mounts are limited to workspace and temp directories. --workdir is set to /github/workspace.
tail -f /dev/null entrypoint override:
The runner ALWAYS overrides the entrypoint for container jobs and service containers: --entrypoint "tail" <image> "-f" "/dev/null". The original entrypoint is discarded. If the image lacks tail in $PATH (e.g., distroless, FROM scratch), the container fails with exec: "tail": executable file not found. There is NO fallback.
PATH extraction from Config.Env:
After starting a container, the runner calls docker inspect and reads Config.Env — an array of KEY=VALUE strings. It iterates through the array looking for entries starting with PATH=. This PATH is used when constructing docker exec commands to find binaries inside the container. If Config.Env is empty or missing PATH, the runner may fail to locate executables.
Exec with stdin (-i):
The runner uses docker exec -i for ALL step executions. The -i flag keeps stdin open — the runner pipes the step script into the container. The exec creates a temporary script file, volume-mounts the temp directory into the container, and executes via docker exec -i --workdir <path> -e <env>... <container> bash -e /path/to/script.sh. Exit codes are read from the docker exec process return code (not from docker inspect).
No exec retry:
If docker exec fails (container not running, connection lost, etc.), the step fails immediately. There is NO exponential backoff or retry logic for exec. This means the exec facility must be reliable from the first attempt.
Docker socket mount:
The runner ALWAYS adds -v "/var/run/docker.sock:/var/run/docker.sock" to the job container. This is hardcoded. Cloud backends must provide a real socket contract or reject the mount clearly; they must not silently expose a non-functional socket.
Health check polling:
Uses docker inspect --format '{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}'. If .Config.Healthcheck is absent (no HEALTHCHECK instruction), the template outputs empty string and the runner skips health polling — container is considered ready immediately. Polling uses exponential backoff: 2s, 3s, 7s, 13s... with ~5-6 retries (total ~30-60s).
Network creation is mandatory:
The runner ALWAYS creates a network (github_network_<GUID>) when container jobs or services are present. Network creation failure is FATAL — no fallback. Cleanup includes docker network rm + docker network prune --filter label=<hash>.
No startup grace period:
After docker start, the runner immediately checks docker ps --filter id=<id> --filter status=running. Docker containers start in <1s, but cloud containers take 5-60s. If the container is not "running" when checked, the job may fail.
| Requirement | Priority | Sockerless Solution |
|---|---|---|
GET /version with ApiVersion ≥ 1.35 |
P0 | Return 1.44 |
POST /images/create (pull) with retries |
P0 | Record image ref; cloud pulls at container start |
POST /auth (docker login) |
P1 | Store credentials for later pull operations |
POST /containers/create with Entrypoint: ["tail"], Cmd: ["-f", "/dev/null"] |
P0 | Accept; agent substitutes as keep-alive mechanism |
POST /containers/create with Binds including /var/run/docker.sock |
P0 | Provide a real socket/dispatch contract or reject clearly |
POST /containers/{id}/start → container is "running" immediately |
P0 | Block start until cloud task is actually running |
GET /containers/json with filters id, status, label |
P0 | Backend supports all filter types |
GET /containers/{id}/json with Config.Env (merged image + user env, including PATH) |
P0 | Fetch image config from registry; merge with user env |
GET /containers/{id}/json with Config.Healthcheck (presence) and State.Health.Status |
P0 | Agent runs health check commands; reports status |
GET /containers/{id}/json with NetworkSettings.Ports |
P0 | Backend tracks port mappings for service containers |
POST /containers/{id}/exec with AttachStdin: true |
P0 | Agent handles exec with stdin pipe |
POST /exec/{id}/start with hijacked bidirectional stream |
P0 | Frontend bridges hijacked connection to agent WebSocket |
| Exit code from exec (not from container inspect) | P0 | Agent reports exec exit code; frontend returns to client |
DELETE /containers/{id}?force=true |
P0 | Stop cloud task + remove from state |
POST /networks/create (failure is fatal) |
P0 | Must succeed reliably |
DELETE /networks/{id} |
P0 | Tear down cloud networking |
POST /networks/prune with label filter |
P1 | Clean up orphaned cloud networks |
GET /containers/{id}/logs?details=true&stdout=true&stderr=true |
P0 | Fetch from cloud logging service |
| Multiplexed stream framing for exec I/O | P0 | Frontend handles framing |
This section identifies every gap between what CI runners expect and what cloud backends provide, with concrete solutions.
Problem: GitLab Runner calls ContainerAttach before ContainerStart. Docker returns the hijacked connection instantly because the daemon holds it. Cloud backends don't have a container to attach to yet.
Solution: The frontend returns 101 Switching Protocols immediately and holds the hijacked connection. When ContainerStart is called, the backend launches the cloud resource. Once the reverse-agent session or backend-specific exec transport is ready, the frontend bridges the buffered hijacked connection to that real stream.
Risk: If the job timeout is very short and cloud startup takes too long, the attach will sit idle until the job times out. No mitigation needed — this is the correct behavior (the runner's context controls the timeout).
Problem: After docker start, the runner immediately runs docker ps --filter status=running to verify the container started. Docker containers start in <1s. Cloud backends take 5-60s.
Solution: The POST /containers/{id}/start endpoint MUST block until the cloud resource is actually running and the required exec transport is ready. Only then return 204 No Content. This way, the subsequent docker ps check sees "running" status. The trade-off is that docker start takes 5-60s instead of <1s, but the runner doesn't have a timeout on the start call itself — only the overall job timeout applies.
Implementation: Backend launches cloud task/service/app → polls cloud API and/or waits for reverse-agent registration → returns 204 only when the Docker-facing resource is ready. Frontend forwards the 204 to the Docker client.
Problem: The runner reads Config.Env from docker inspect to extract the PATH environment variable. Sockerless doesn't pull images locally, so it doesn't have the image config.
Solution: When POST /images/create (pull) is called, the backend fetches the image's manifest and config blob from the registry API WITHOUT downloading any layers. The image config contains Env, Cmd, Entrypoint, ExposedPorts, WorkingDir, Labels, and Healthcheck. These are stored in the backend's image table and returned in both GET /images/{name}/json and merged into GET /containers/{id}/json → Config.Env.
Registry API calls needed: GET /v2/<name>/manifests/<tag> → GET /v2/<name>/blobs/<config-digest>. These are lightweight (config blob is typically <10KB). Authentication uses the credentials from X-Registry-Auth or POST /auth.
Problem: The runner overrides the entrypoint to tail -f /dev/null. On cloud backends, the sockerless agent needs to be PID 1 (or sidecar) to serve exec/attach.
Solution: The frontend detects the tail -f /dev/null entrypoint pattern in POST /containers/create and flags it as a keep-alive container. When the backend launches a Service/App/function runner path, it materializes the bootstrap overlay as the entrypoint. The bootstrap keeps the workload alive, registers the reverse-agent session, and serves exec/attach requests.
For containers with real entrypoints (non-tail), the bootstrap preserves the original command and runs it through the backend's cloud-specific overlay contract.
Problem: GitLab Runner tries to load the helper image from an embedded .docker.tar.zst via ImageLoad before falling back to registry pull. Sockerless has no local image store.
Solution (recommended): Configure helper_image in GitLab Runner's config.toml to point to a registry-hosted copy of the helper image. This bypasses tar loading entirely. The runner will use ImagePullBlocking instead.
Image-load implementation path: POST /images/load accepts the tar stream, extracts the image manifest and layers, and pushes them to a configured staging registry (ECR, Artifact Registry, or ACR). POST /images/{name}/tag records the cloud registry reference for later use. Backends that do not implement this path must fail explicitly rather than pretending to have a local image store.
Problem: Helper and build containers share the same Docker volumes (for git clone → build handoff). Cloud containers are isolated tasks — they don't share local filesystems.
Solution: Cloud shared filesystems:
- ECS: EFS filesystem with access points. Multiple Fargate tasks mount the same EFS access point → shared data.
- Cloud Run: GCS bucket mounted via Cloud Storage FUSE. Multiple jobs access the same bucket path.
- ACA: Azure Files share. Multiple container app jobs mount the same file share.
Volume creation (POST /volumes/create) provisions a directory/access-point in the shared filesystem. Container creation records the volume mount mapping. Container start applies the mount to the cloud task definition.
Performance consideration: Cloud shared filesystems add latency vs. local Docker volumes. EFS latency is 0.5-5ms per operation. For CI workloads (git clone, build), this is acceptable.
Problem: The runner always mounts /var/run/docker.sock:/var/run/docker.sock. This path doesn't exist on cloud backends.
Solution: Do not pretend the Docker socket exists. A backend must either wire a real socket/dispatch contract into the workload or reject the mount clearly so nested-Docker requirements surface at create time.
Problem: GitLab Runner creates a SEPARATE health-check container (using the helper image) that TCP-probes the service container's ports. This health-check container also needs to run on the cloud backend and be able to reach the service container's network.
Solution: The health-check container runs as another cloud task on the same network (same VPC, same security group/Cloud Map namespace). It receives the same Binds as other containers. The cloud DNS service discovery ensures it can resolve the service container's alias. The health-check container runs gitlab-runner-helper health-check, which probes TCP ports. When the service is ready, the health-check container exits with code 0.
Latency consideration: Creating a health-check cloud task adds 5-60s of startup latency. To mitigate: start the health-check container in parallel with the service container (both at ContainerStart time). The health-check container's own startup latency overlaps with the service container's startup.
Problem: The runner polls docker inspect for .Config.Healthcheck (presence) and .State.Health.Status (one of "starting", "healthy", "unhealthy"). This requires health check execution inside the container.
Solution: If the image defines a HEALTHCHECK instruction (detected during image config fetch from registry), the agent runs the health check command inside the container at the specified interval. The agent reports health status to the backend. The backend stores it and returns it in GET /containers/{id}/json → State.Health.Status.
If no HEALTHCHECK is defined, Config.Healthcheck is absent/null and State.Health is omitted entirely (matching Docker's behavior). The runner skips health polling in this case.
Problem: Both runners rely on DNS resolution for service aliases. GitLab uses EndpointsConfig.Aliases; GitHub uses network-scoped container names. Docker provides this via its embedded DNS server on user-defined networks.
Solution per backend:
- ECS: AWS Cloud Map service discovery. Create a private DNS namespace. Register each container as a service instance with its aliases. Cloud Map DNS resolves aliases to task ENI IPs.
- Cloud Run: Cloud DNS private zone. Create A records for each container's aliases pointing to the container's internal IP.
- ACA: Container Apps environment provides internal DNS. Container names and configured aliases resolve within the environment's VNet.
Problem: The runner does NOT retry failed exec calls. If exec fails once, the step fails. The agent must be ready to accept exec on the first attempt.
Solution: The agent starts its WebSocket server as the first thing on startup (before running the user process). The backend waits for agent readiness (health probe to agent's WebSocket endpoint) before returning from POST /containers/{id}/start. By the time the runner calls docker exec, the agent is guaranteed to be listening.
Agent readiness check: Backend polls ws://agent:9111/health (a simple HTTP GET on the same port). Agent responds with 200 OK once its WebSocket server is accepting connections. Backend only returns 204 from start once this check passes.
Problem: Cloud backends have 5-60s startup latency. Both runners assume near-instant starts. This affects:
- Time-to-first-exec (GitHub)
- Attach stream responsiveness (GitLab)
- Service readiness timeout (both)
- Overall job duration
Solutions:
- Absorb latency in
docker start: Block the start API call until the container is fully running and the agent is accepting connections. The runner doesn't time the start call separately. - Parallel container startup: When multiple containers are created (services + job container), start them in parallel on the cloud backend rather than sequentially. The Docker API is sequential (create, start, create, start...), but the backend can begin provisioning eagerly on create.
- Pre-warming (future): Keep warm capacity in the cloud backend (e.g., pre-provisioned Fargate tasks, Cloud Run minimum instances) to reduce cold start time.
- Increased service timeouts: Recommend
wait_for_services_timeout = 120(GitLab) to account for cloud startup.
Problem: Both runners use Docker's 8-byte header multiplexed stream protocol for attach and exec I/O. GitLab Runner uses stdcopy.StdCopy to demultiplex. Any incorrect framing breaks CI job output.
Solution: The frontend is responsible for correct framing. The agent sends raw stdout/stderr bytes via WebSocket JSON messages. The frontend wraps each chunk in the 8-byte header format:
- Byte 0: stream type (1=stdout, 2=stderr)
- Bytes 1-3: padding (0x00)
- Bytes 4-7: payload length (big-endian uint32)
- Followed by payload bytes
For stdin (client → agent), the frontend reads raw bytes from the hijacked connection and forwards them as {"type":"stdin","data":"<base64>"} WebSocket messages.
Problem: After ImageLoad, the runner parses the response for "Loaded image: <id>" or "Loaded image ID: <id>", then calls ImageTag(id, name+":"+tag). Sockerless must return a compatible response format.
Solution: POST /images/load response must stream JSON objects including a line matching {"stream":"Loaded image: sha256:<digest>\n"} or {"stream":"Loaded image ID: sha256:<digest>\n"}. The subsequent POST /images/{name}/tag records the new tag in the backend's image table.
FaaS backends (Lambda, Cloud Run Functions, Azure Functions) now support exec and attach via reverse agent, but are still NOT compatible with CI runners due to other limitations:
| CI Runner Requirement | FaaS Capability | Gap |
|---|---|---|
| Long-running container (minutes-hours) | Max 15min (Lambda), 60min (CR Functions), 10min (Az Functions) | Timeout too short for most CI jobs |
| Exec into running container | Supported (via reverse agent) | No gap (but timeout-limited) |
| Attach to container I/O | Supported (via reverse agent) | No gap (but timeout-limited) |
| Volume sharing between containers | No persistent shared storage | Can't share build artifacts |
| Network aliases for service discovery | No user-defined networking | Can't reach service containers |
| Helper binary execution | Binaries can't run in FaaS context | gitlab-runner-helper won't work |
Reverse agent mode: The agent inside the function dials back to the backend via SOCKERLESS_CALLBACK_URL (WebSocket). This enables exec and attach for the duration of the function invocation. The FaaS invoke goroutine waits for agent disconnect before stopping the container.
FaaS backends ARE useful for: Short-lived container workloads (batch processing, webhooks, container actions, scheduled tasks) that can complete within the function timeout. They support docker run (create + start + exec + attach + logs + wait + remove) via reverse agent.
| Feature | Docker | ECS | Cloud Run | ACA | Lambda | CR Func | Az Func |
|---|---|---|---|---|---|---|---|
| GitLab Runner | Yes | Yes | Yes | Yes | No | No | No |
| GitHub Actions Runner | Yes | Yes | Yes | Yes | No | No | No |
GitHub act Runner |
Yes | Yes | Yes | Yes | Yes‡ | Yes‡ | Yes‡ |
| gitlab-ci-local | Yes | Yes | Yes | Yes | Yes‡ | Yes‡ | Yes‡ |
| Attach-before-start | Yes | Yes* | Yes* | Yes* | Yes** | Yes** | Yes** |
| Exec with stdin | Yes | Yes* | Yes* | Yes* | Yes** | Yes** | Yes** |
| Volume sharing | Yes | Yes | Yes | Yes | No | No | No |
| Health check polling | Yes | Yes* | Yes* | Yes* | No | No | No |
| Container actions | Yes | Yes* | Yes* | Yes* | Yes** | Yes** | Yes** |
| Docker build | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Docker cp (archive) | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Startup latency | <1s | 10-45s | 5-30s | 10-60s | 1-5s | 1-5s | 1-5s |
* ECS uses its cloud access path; Cloud Run and ACA use reverse-agent Service/App paths. ** Via reverse agent ‡ FaaS backends limited by function timeout
Recommendation: For CI runner workloads, use container-based backends (ECS, Cloud Run, ACA). FaaS backends work with lightweight CI tools (act, gitlab-ci-local) but not with full CI runners.
| Limitation | Impact | Mitigation |
|---|---|---|
| Startup latency (5-60s per container) | CI jobs take longer to start | Pre-warm containers; parallel startup; accept latency for serverless benefits |
| No local filesystem | bind mounts from host paths don't exist |
Map to cloud storage; agent-based file injection |
No --privileged |
Can't run Docker-in-Docker in privileged mode | Use alternative DinD approaches (sysbox, rootless Docker) or kaniko for builds |
No device access (--device) |
Can't pass GPU or other devices | Some backends support GPU (ECS with GPU instances); not serverless in that case |
No process namespace sharing (--pid=host) |
Can't see host processes | Not relevant in cloud context |
| Cold start variability | Container startup time is not deterministic | Implement readiness tracking; retry logic in the daemon |
These endpoints return 501 Not Implemented with a descriptive message:
| Category | Endpoints | Reason |
|---|---|---|
| Swarm | All /swarm/*, /services/*, /tasks/*, /nodes/*, /secrets/*, /configs/* |
Not applicable to cloud backends |
| Plugins | All /plugins/* |
Not applicable |
| Session | POST /session |
BuildKit-specific |
| Distribution | GET /distribution/{name}/json |
Not needed |
| Build prune | POST /build/prune |
Not used |
| Container export | GET /containers/{id}/export |
Not used |
| Container commit | POST /commit |
Not used |
| Container changes | GET /containers/{id}/changes |
Not used |
| Container update | POST /containers/{id}/update |
Not used |
| Container resize | POST /containers/{id}/resize |
Future (TTY support) |
| Container attach/ws | GET /containers/{id}/attach/ws |
Future (WebSocket support) |
| Image search | POST /images/search |
Not used |
| Image save/get | GET /images/get |
Not used |
| Image push | POST /images/{name}/push |
Out of scope |
These were initially excluded from the spec but have been implemented:
| Category | Endpoints | Added In |
|---|---|---|
| Build | POST /build |
Phase 34 — Dockerfile parser (FROM, COPY, ADD, ENV, CMD, ENTRYPOINT, WORKDIR, ARG, LABEL, EXPOSE, USER). RUN is no-op. |
| Container archive | PUT/HEAD/GET /containers/{id}/archive |
Phase 25 — Pre-start staging for docker cp before docker start |
| Container lifecycle | POST .../restart, POST .../rename, POST .../pause, POST .../unpause |
Extended endpoints |
| Container info | GET .../top, GET .../stats |
Extended endpoints |
| Container prune | POST /containers/prune |
Extended endpoint |
| Image lifecycle | GET /images/json, DELETE /images/{name}, GET .../history, POST /images/prune |
Extended endpoints |
| Volume prune | POST /volumes/prune |
Extended endpoint |
| Network connect | POST /networks/{id}/connect |
Extended endpoint |
| System | GET /events, GET /system/df |
Extended endpoints |
These fields must either map to a real cloud primitive or fail clearly when they would affect behavior. They may be preserved as inspect metadata only when they are irrelevant to the requested cloud operation:
MacAddress, CgroupParent, DeviceCgroupRules, PidMode, UTSMode,
SecurityOpt, CapAdd, CapDrop, UsernsMode, ShmSize, Sysctls,
OomKillDisable, OomScoreAdj, Runtime, Isolation, Init,
VolumesFrom (partially supported), Devices, DeviceRequests,
LogConfig, CpusetCpus, CpusetMems, CPUShares, BlkioWeight,
BlkioDeviceReadBps, BlkioDeviceWriteBps, KernelMemory,
IpcMode, GroupAdd, Ulimits, ReadonlyRootfs, StorageOpt
Cloud backends must not silently drop behavior-bearing fields. If a field cannot be honored by the target cloud, the backend should return an explicit error rather than pretend the setting took effect.
All components use command-line flags with environment variable overrides. Each backend binary has its own set of environment variables prefixed with its cloud provider abbreviation.
An optional unified configuration file (~/.sockerless/config.yaml) provides a structured alternative to environment variables. It defines named environments (backend configurations) and simulator definitions. The CLI reads config.yaml and exports the values as environment variables before starting backend binaries.
Priority order: CLI flags > config.yaml environment values > context env vars (legacy JSON) > process environment variables > Defaults
See cmd/sockerless/README.md for the full config.yaml format.
The Docker REST API is served in-process by each backend (see §6.3), so there is no separate frontend binary or -backend address to configure. The listen address is a backend flag (--addr, default :3375); see §15.3 and the per-backend READMEs.
Each backend binary reads its own environment variables. All backends share common variables:
| Variable | Default | Description |
|---|---|---|
SOCKERLESS_CALLBACK_URL |
Backend URL for reverse agent connections | |
SOCKERLESS_ENDPOINT_URL |
Custom cloud API endpoint, commonly a local simulator/cloud-slice endpoint. This changes routing only; cloud image refs and API semantics remain the same. | |
SOCKERLESS_FETCH_IMAGE_CONFIG |
false |
Fetch image config from registry on pull |
Backend-specific variables use prefixes: SOCKERLESS_ECS_*, SOCKERLESS_LAMBDA_*, SOCKERLESS_GCR_*, SOCKERLESS_GCF_*, SOCKERLESS_ACA_*, SOCKERLESS_AZF_*, AWS_REGION.
See each backend's README.md for the full list of configuration variables.
State: All backends use in-memory state (sync.Map + StateStore[T]). No persistent database. State is lost on restart.
The agent is configured entirely via environment variables (injected by the backend):
| Env Var | Description |
|---|---|
SOCKERLESS_AGENT_PORT |
Port to listen on (default: 9111) |
SOCKERLESS_AGENT_TOKEN |
Auth token for validating frontend connections |
SOCKERLESS_CALLBACK_URL |
Backend URL for reverse agent mode |
Goal: Frontend + Docker backend pass the test suite. docker run, docker ps, docker logs, docker exec work end-to-end with the Docker backend.
| Component | Deliverable |
|---|---|
api/ module |
Internal API types, capability model |
| Frontend | Docker REST API server (OpenAPI-generated types), system + container + exec + image endpoints |
| Docker backend | Passthrough to real Docker daemon (reference implementation) |
| Tests | Black-box REST test suite (system, containers, exec, images) |
| Protocol | Multiplexed stream framing, connection hijacking |
Goal: Agent works. ECS backend runs containers on Fargate. GitLab Runner docker-executor completes a CI job.
| Component | Deliverable |
|---|---|
| Agent | WebSocket server, exec, attach, process management, health checks |
| ECS backend | Fargate tasks, CloudWatch logs, EFS volumes, VPC networking, agent injection |
| Network endpoints | create, inspect, list, disconnect, remove, prune |
| Volume endpoints | create, inspect, list, remove |
| Frontend streaming | WebSocket bridge: Docker client ↔ agent |
| GitLab Runner validation | End-to-end CI job with docker-executor |
Goal: GitHub Actions Runner works. Docker Compose core lifecycle works. Cloud Run and ACA backends.
| Component | Deliverable |
|---|---|
| GitHub Runner validation | Container jobs with exec-based step execution |
| Compose support | up/down/ps/logs with label-based filtering |
| Cloud Run backend | Cloud Run Jobs, Cloud Logging, GCS FUSE, agent via ingress |
| ACA backend | Container Apps Jobs, Azure Monitor, Azure Files, agent via VNet |
| Health check support | Image-defined health checks, polling, status reporting |
| Image load/tag | For GitLab Runner helper images |
Goal: Lambda, Cloud Functions, Azure Functions backends (capability-limited). Production hardening.
| Component | Deliverable |
|---|---|
| Lambda backend | Container image functions, CloudWatch logs, capability reporting |
| Cloud Run Functions backend | 2nd gen functions, Cloud Logging |
| Azure Functions backend | Custom container functions, Azure Monitor |
| Capability enforcement | Frontend returns 501 based on backend capabilities |
| Error handling | Retries, timeouts, graceful degradation |
| Monitoring | Metrics, health endpoint |
| Events | GET /events streaming endpoint |
| Documentation | User guide, backend setup guides, configuration reference |
Phases 5–35 expanded the project from the initial 4-phase MVP to a fully tested, multi-backend system. Key milestones include core library extraction (Phase 5), FaaS reverse agent (Phase 7/19), driver architecture (Phase 30), Docker build (Phase 34), and bleephub — a GitHub Actions runner service API (Phase 35). For full phase-by-phase details, see PLAN.md and WHAT_WE_DID.md.
Test Coverage (Phase 35 — see STATUS.md for current counts)
| Test Type | Count (at Phase 35) | Status |
|---|---|---|
| Simulator-backend integration | 129 | All pass |
| E2E GitHub (act) | 154 (22 × 7) | All pass |
| E2E GitLab (gitlab-ci-local) | 175 (25 × 7) | All pass |
| bleephub integration | 1 | Pass |
For the full per-backend command mapping with cloud service details, see FEATURE_MATRIX.md. All Docker CLI commands listed in the original spec (containers, images, networks, volumes, exec, compose, system) are now supported.
- Docker Engine API v1.44: https://docs.docker.com/engine/api/v1.44/
- GitLab Runner source: https://gitlab.com/gitlab-org/gitlab-runner
- GitHub Actions Runner source: https://github.com/actions/runner
- AWS ECS Exec: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
- Google Cloud Run Jobs: https://cloud.google.com/run/docs/create-jobs
- Azure Container Apps Jobs: https://learn.microsoft.com/en-us/azure/container-apps/jobs