Docker 75 🐳 Docker API: Interacting with Docker Engine Daemon REST API via HTTP / Unix Socket
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
| Transport | Address | Use Case |
|---|---|---|
| Unix socket | /var/run/docker.sock | Local, default |
| TCP plaintext | tcp://host:2375 | Never in production |
| TCP TLS | tcp://host:2376 | Remote, secured |
Common Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /containers/json | List containers |
| GET | /images/json | List images |
| GET | /info | Daemon information |
| GET | /version | Version information |
| POST | /containers/create | Create container |
| POST | /containers/{id}/start | Start container |
| GET | /containers/{id}/logs | Container logs |
| DELETE | /containers/{id} | Remove container |
| GET | /events | Stream events |
curl Flags
| Flag | Purpose |
|---|---|
--unix-socket | Connect to Unix socket |
-X POST | HTTP method |
-H "Content-Type: application/json" | JSON body |
-d '{"key": "value"}' | Request body |
--no-buffer | Stream response |
Version Negotiation
| Mechanism | Effect |
|---|---|
| Version prefix | Forces specific API version |
DOCKER_API_VERSION | Forces version for CLI/SDK |
| Auto-negotiation | Daemon 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Permission denied | User not in docker group | sudo usermod -aG docker $USER |
curl hangs | GET /events is a stream | Add --no-buffer |
| Connection refused | Daemon not running | systemctl start docker |
| Version mismatch | Client older than daemon | Use version prefix or upgrade |
| Container not started | Create and start are separate | Call 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
| Item | Value |
|---|---|
| API type | RESTful HTTP |
| Default transport | Unix socket /var/run/docker.sock |
curl flag | --unix-socket |
| Version prefix | /v1.55/ |
| List containers | GET /containers/json |
| Create container | POST /containers/create |
| Start container | POST /containers/{id}/start |
| Stream events | GET /events with --no-buffer |
| Remote transport | TCP with TLS |
| Security | Socket is root-equivalent |
Key takeaways:
- The Docker CLI is a client of the API. Every
dockercommand translates to an HTTP request to the daemon over the Unix socket. The API can be called directly withcurlor 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
dockergroup or root can access it. Exposing the TCP socket without TLS is a critical security error . curl --unix-socketis the direct interface. The--unix-socketflag 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/createreturns the container ID, andPOST /containers/{id}/startstarts it. The CLI does both in onedocker runcommand, but the API exposes them separately . - Streaming endpoints require
--no-buffer.GET /eventsis a long-lived stream, andcurlwill buffer the output unless--no-bufferis 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!