| |

Docker 75 🐳 Docker API: Interacting with Docker Engine Daemon REST API via HTTP / Unix Socket

⏱️ Estimated read: 12 min | |

The Docker CLI is a client. When you run docker ps or docker run, the CLI does not execute the operation itself. It sends an HTTP request to the Docker daemon (dockerd) over a socket, receives the response, and formats it for the terminal. The API that the CLI uses is the Docker Engine API, a RESTful HTTP interface that any HTTP client can call directly . The default transport is a Unix domain socket at /var/run/docker.sock, which is local-only and requires no network configuration. The same API can also be exposed over TCP with TLS for remote access, but the Unix socket is the default and the one that matters for most operational work.

The API is versioned. Every endpoint is prefixed with an API version, such as /v1.55/containers/json. The daemon and the client negotiate the highest version they both support, and the API is backward-compatible, meaning an older client can talk to a newer daemon by using an older version prefix . The docker version command shows the client’s API version, the server’s API version, and the minimum supported version. If the client does not specify a version, the daemon uses its own version, but the Docker documentation recommends specifying the version explicitly when calling the API directly with curl .

Key point: The Docker Engine API is a versioned RESTful HTTP API exposed over a Unix socket (/var/run/docker.sock) by default or over TCP with optional TLS. The curl command can call it directly using the --unix-socket flag for the local socket, or a TCP URL for remote access. Every endpoint is prefixed with the API version, and the daemon supports backward compatibility through version negotiation.


Why the Unix socket is the default transport

The security problem. The Docker daemon runs as root. A TCP socket exposed to the network is a root-level interface that anyone who can reach it can use to spawn privileged containers, mount the host filesystem, and escalate to root. The Unix socket is protected by filesystem permissions: only users in the docker group or root can access it. This is why the socket is a well-known attack vector in misconfigured systems, and why exposing the TCP socket without TLS and authentication is a critical security error .

The local IPC problem. The Unix domain socket is a filesystem-based IPC mechanism. It uses the local filesystem for addressing, not the network stack. The Docker daemon listens on /var/run/docker.sock, and clients connect to that path. The HTTP protocol is still used over the socket, but the transport is the local filesystem, not TCP/IP . This is why curl --unix-socket /var/run/docker.sock http://localhost/containers/json works: curl connects to the socket path, sends an HTTP request with an arbitrary hostname, and the daemon responds over the same socket.

The TCP and TLS problem. For remote access, the daemon can listen on a TCP socket, typically port 2375 (plaintext) or 2376 (TLS). The plaintext port should never be exposed beyond localhost. The TLS port requires a CA certificate, a server certificate, and a client certificate for mutual authentication. The environment variables DOCKER_HOST, DOCKER_TLS_VERIFY, and DOCKER_CERT_PATH configure the CLI and SDKs to use the remote socket with TLS .


a. Calling the API with curl over the Unix socket

The curl command has a --unix-socket flag that connects to a Unix domain socket instead of a network address. The URL’s hostname is ignored; only the path matters.

# List all images
# --unix-socket connects to the Docker socket
# http://localhost/images/json is the API endpoint
# jq is used to format the JSON output
curl --unix-socket /var/run/docker.sock http://localhost/images/json | jq

# List running containers
# /containers/json returns only running containers by default
# add ?all=true to include stopped containers
curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq

# List all containers, including stopped ones
curl --unix-socket /var/run/docker.sock "http://localhost/containers/json?all=true" | jq

# Inspect a specific container
# /containers/{id}/json returns the full container configuration
curl --unix-socket /var/run/docker.sock http://localhost/containers/mycontainer/json | jq

The --unix-socket flag is the transport selector. Without it, curl would try to resolve localhost over TCP, and the daemon is not listening on TCP by default. With it, curl connects to the socket path, sends the HTTP request, and reads the response .

The endpoints follow the REST pattern. GET /containers/json lists containers. GET /images/json lists images. GET /info returns daemon information. GET /version returns version information. GET /events streams events, and it requires --no-buffer to stream the response without buffering .


b. Creating and starting a container via the API

Creating a container is a two-step process: POST /containers/create creates the container and returns its ID, then POST /containers/{id}/start starts it. The create request takes a JSON body with the container configuration.

# Create a container from the alpine image
# -X POST specifies the HTTP method
# -H "Content-Type: application/json" tells the daemon the body is JSON
# -d '{"Image": "alpine", "Cmd": ["echo", "hello"]}' is the request body
# The response contains the container ID
curl --unix-socket /var/run/docker.sock \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"Image": "alpine", "Cmd": ["echo", "hello"]}' \
  http://localhost/containers/create

# Response: {"Id":"1c6594faf5","Warnings":null}

# Start the container using the ID from the previous response
# The ID is passed in the URL path
curl --unix-socket /var/run/docker.sock \
  -X POST \
  http://localhost/containers/1c6594faf5/start

The Image field is the image name or digest. The Cmd field is the command to run as an array. The create request accepts the same fields as the HostConfig in the Docker CLI, including port bindings, volume mounts, environment variables, and resource limits . The response includes the container ID and any warnings.

The start request has no body. The daemon starts the container and returns 204 No Content on success. If the container fails to start, the response includes an error message. The logs can be retrieved with GET /containers/{id}/logs, and the container can be stopped with POST /containers/{id}/stop.


c. Version negotiation and the API prefix

Every endpoint is prefixed with the API version. The prefix is optional, but the Docker documentation recommends it for direct API calls because it ensures the response format matches the version the client expects .

# Without version prefix: the daemon uses its own version
curl --unix-socket /var/run/docker.sock http://localhost/containers/json

# With version prefix: the client specifies the API version
# This ensures the response format matches the version
curl --unix-socket /var/run/docker.sock http://localhost/v1.55/containers/json

The docker version command shows the supported versions. The Server section shows the current API version and the minimum supported version. The Client section shows the client’s API version . The DOCKER_API_VERSION environment variable forces the CLI and SDKs to use a specific version, but it disables version negotiation and should only be used for debugging or compatibility .

The version negotiation works in both directions. A newer client can talk to an older daemon by negotiating down to a version the daemon supports. A newer daemon can talk to an older client by negotiating down to the client’s version. The negotiation is “best effort”—features introduced in later versions are disabled when the negotiated version is older .


Complete Example Session

The following session uses curl over the Unix socket to list images, create a container, start it, read its logs, and remove it.

# ============================================
# PART 1: LIST IMAGES
# ============================================
# List all images on the daemon
# --unix-socket selects the Docker socket
# jq formats the JSON for readability
curl --unix-socket /var/run/docker.sock http://localhost/images/json | jq

# ============================================
# PART 2: CREATE A CONTAINER
# ============================================
# Create an alpine container that prints "hello"
# The response contains the container ID
CREATE_RESPONSE=$(curl --unix-socket /var/run/docker.sock \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"Image": "alpine", "Cmd": ["echo", "hello"]}' \
  http://localhost/containers/create)

# Extract the container ID from the JSON response
CONTAINER_ID=$(echo $CREATE_RESPONSE | jq -r '.Id')
echo "Created container: $CONTAINER_ID"

# ============================================
# PART 3: START THE CONTAINER
# ============================================
# Start the container using its ID
curl --unix-socket /var/run/docker.sock \
  -X POST \
  http://localhost/containers/$CONTAINER_ID/start

# ============================================
# PART 4: READ THE LOGS
# ============================================
# Get the container logs
# The output should be "hello"
curl --unix-socket /var/run/docker.sock \
  http://localhost/containers/$CONTAINER_ID/logs?stdout=true

# ============================================
# PART 5: REMOVE THE CONTAINER
# ============================================
# Remove the container after it has exited
# -f forces removal even if the container is running
curl --unix-socket /var/run/docker.sock \
  -X DELETE \
  "http://localhost/containers/$CONTAINER_ID?force=true"

The five parts cover listing images, creating a container, starting it, reading logs, and removing it. What the session demonstrates is that the Docker CLI is a thin wrapper around these HTTP calls, and the same operations can be performed with curl and jq without the CLI.


Quick Reference

Transport Options

TransportAddressUse Case
Unix socket/var/run/docker.sockLocal, default
TCP plaintexttcp://host:2375Never in production
TCP TLStcp://host:2376Remote, secured

Common Endpoints

MethodEndpointPurpose
GET/containers/jsonList containers
GET/images/jsonList images
GET/infoDaemon information
GET/versionVersion information
POST/containers/createCreate container
POST/containers/{id}/startStart container
GET/containers/{id}/logsContainer logs
DELETE/containers/{id}Remove container
GET/eventsStream events

curl Flags

FlagPurpose
--unix-socketConnect to Unix socket
-X POSTHTTP method
-H "Content-Type: application/json"JSON body
-d '{"key": "value"}'Request body
--no-bufferStream response

Version Negotiation

MechanismEffect
Version prefixForces specific API version
DOCKER_API_VERSIONForces version for CLI/SDK
Auto-negotiationDaemon selects highest common version

Best Practices

✅ Do This:

# Use the Unix socket for local access
curl --unix-socket /var/run/docker.sock http://localhost/containers/json

# Use the version prefix for direct API calls
curl --unix-socket /var/run/docker.sock http://localhost/v1.55/containers/json

# Use TLS for remote access
export DOCKER_HOST=tcp://remote:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=/path/to/certs

❌ Don’t Do This:

# Expose the TCP socket without TLS
dockerd -H tcp://0.0.0.0:2375 # critical security error

# Use the socket without checking permissions
# /var/run/docker.sock is root-owned; access is root-equivalent

# Forget the version prefix in production scripts
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
# may break when the daemon version changes

Common Pitfalls

PitfallWhy It HappensFix
Permission deniedUser not in docker groupsudo usermod -aG docker $USER
curl hangsGET /events is a streamAdd --no-buffer
Connection refusedDaemon not runningsystemctl start docker
Version mismatchClient older than daemonUse version prefix or upgrade
Container not startedCreate and start are separateCall POST /start after create

Real-World Examples

1. List containers

curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq

2. List all containers

curl --unix-socket /var/run/docker.sock "http://localhost/containers/json?all=true" | jq

3. Inspect a container

curl --unix-socket /var/run/docker.sock http://localhost/containers/myapp/json | jq

4. Create a container

curl --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json" -d '{"Image":"alpine","Cmd":["echo","hi"]}' http://localhost/containers/create

5. Start a container

curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/$ID/start

6. Stop a container

curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/$ID/stop

7. Container logs

curl --unix-socket /var/run/docker.sock "http://localhost/containers/$ID/logs?stdout=true"

8. Stream events

curl --no-buffer --unix-socket /var/run/docker.sock http://localhost/events

9. Daemon info

curl --unix-socket /var/run/docker.sock http://localhost/info | jq

10. Version check

curl --unix-socket /var/run/docker.sock http://localhost/version | jq

Visual

The Socket Transport

┌──────────────────────────────────────────────┐
│  CURL                                        │
│  --unix-socket /var/run/docker.sock          │
│         │                                    │
│         ▼                                    │
│  UNIX DOMAIN SOCKET                          │
│  filesystem-based IPC                        │
│         │                                    │
│         ▼                                    │
│  DOCKER DAEMON                               │
│  listens on the socket                       │
│         │                                    │
│         ▼                                    │
│  HTTP RESPONSE                               │
│  JSON body                                   │
└──────────────────────────────────────────────┘

Create and Start

┌──────────────────────────────────────────────┐
│  POST /containers/create                     │
│  body: {"Image": "alpine", "Cmd": ["echo"]}  │
│         │                                    │
│         ▼                                    │
│  Response: {"Id": "abc123"}                  │
│         │                                    │
│         ▼                                    │
│  POST /containers/abc123/start               │
│         │                                    │
│         ▼                                    │
│  Container is running                        │
└──────────────────────────────────────────────┘

Version Prefix

┌──────────────────────────────────────────────┐
│  /containers/json                            │
│  → daemon uses its own version               │
│                                              │
│  /v1.55/containers/json                      │
│  → client specifies version                  │
│  → response format matches v1.55             │
└──────────────────────────────────────────────┘

Transport Security

┌──────────────────────────────────────────────┐
│  UNIX SOCKET (default)                       │
│  /var/run/docker.sock                        │
│  root-owned, local only                      │
│                                              │
│  TCP PLAINTEXT (never)                       │
│  tcp://0.0.0.0:2375                          │
│  root-level access over network              │
│                                              │
│  TCP TLS (remote)                            │
│  tcp://host:2376                             │
│  mutual TLS authentication                   │
└──────────────────────────────────────────────┘

Summary

ItemValue
API typeRESTful HTTP
Default transportUnix socket /var/run/docker.sock
curl flag--unix-socket
Version prefix/v1.55/
List containersGET /containers/json
Create containerPOST /containers/create
Start containerPOST /containers/{id}/start
Stream eventsGET /events with --no-buffer
Remote transportTCP with TLS
SecuritySocket is root-equivalent

Key takeaways:

  • The Docker CLI is a client of the API. Every docker command translates to an HTTP request to the daemon over the Unix socket. The API can be called directly with curl or any HTTP client .
  • The Unix socket is the default and the secure local transport. It is protected by filesystem permissions, and only users in the docker group or root can access it. Exposing the TCP socket without TLS is a critical security error .
  • curl --unix-socket is the direct interface. The --unix-socket flag connects to the socket path, and the HTTP request follows the REST endpoint structure. The hostname in the URL is ignored .
  • Every endpoint is versioned. The /v1.55/ prefix specifies the API version. The daemon negotiates the highest version both sides support, and the API is backward-compatible .
  • Container creation is two steps. POST /containers/create returns the container ID, and POST /containers/{id}/start starts it. The CLI does both in one docker run command, but the API exposes them separately .
  • Streaming endpoints require --no-buffer. GET /events is a long-lived stream, and curl will buffer the output unless --no-buffer is set .

Remember: The Docker Engine API is the interface that the Docker CLI uses, and it is available to any HTTP client. The Unix socket is the default transport, protected by filesystem permissions, and the --unix-socket flag is how curl reaches it. The API is versioned, and the version prefix is how the client tells the daemon which response format to use. The operations are the same as the CLI: list, create, start, stop, remove. The CLI is a convenience; the API is the contract.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!