Every docker command you type gets translated into an HTTP call against a REST API before it ever reaches the container runtime. Once you understand that, you stop thinking of Docker as “a CLI tool” and start thinking of it as “an HTTP service I can automate against” — which opens the door to CI/CD pipelines, custom dashboards, self-healing scripts, and integrations that would be painful to build by shelling out to the CLI.
Understanding the Docker Remote API
The Docker Engine API is versioned (currently around v1.47 at the time of writing, tracking recent Engine releases) and exposes essentially every capability of the daemon: container lifecycle, image management, networks, volumes, swarm operations, and system events. It’s reachable over:
- The default Unix socket,
/var/run/docker.sock, for local automation - A TCP socket, if you’ve enabled remote access (see the companion guide on securing the daemon)
Every endpoint follows REST conventions — GET to list/inspect, POST to create/act, DELETE to remove.
Exploring the API Directly with curl
Before automating anything, it helps to poke at the raw API. Against the local Unix socket:
curl --unix-socket /var/run/docker.sock http://localhost/version
{"Platform":{"Name":""},"Version":"27.3.1","ApiVersion":"1.47","MinAPIVersion":"1.24","GitCommit":"abcd123","GoVersion":"go1.22.5","Os":"linux","Arch":"amd64"}
List running containers:
curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq .
Against a remote, TLS-secured daemon:
curl --cert cert.pem --key key.pem --cacert ca.pem \
https://192.168.1.20:2376/containers/json
Automating Container Creation via the API
Create a container by POSTing a JSON configuration:
curl --unix-socket /var/run/docker.sock \
-H "Content-Type: application/json" \
-d '{
"Image": "nginx:latest",
"ExposedPorts": {"80/tcp": {}},
"HostConfig": {
"PortBindings": {"80/tcp": [{"HostPort": "8080"}]}
}
}' \
-X POST http://localhost/containers/create?name=automated-nginx
Expected response:
{"Id":"a1b2c3d4e5f6...","Warnings":[]}
Then start it:
curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/a1b2c3d4e5f6/start
A successful start returns HTTP 204 with no body.
Automating with a Shell Script
Here’s a practical automation script that deploys or redeploys a container idempotently — the kind of thing you’d run from a cron job or a deploy hook:
#!/usr/bin/env bash
set -euo pipefail
IMAGE="myregistry/web-app:$1"
NAME="web-app"
SOCKET="/var/run/docker.sock"
echo "Pulling $IMAGE..."
curl --unix-socket "$SOCKET" -X POST "http://localhost/images/create?fromImage=${IMAGE%:*}&tag=${IMAGE#*:}"
echo "Stopping and removing any existing container..."
curl --unix-socket "$SOCKET" -X POST "http://localhost/containers/$NAME/stop" || true
curl --unix-socket "$SOCKET" -X DELETE "http://localhost/containers/$NAME" || true
echo "Creating new container..."
curl --unix-socket "$SOCKET" -H "Content-Type: application/json" -X POST \
"http://localhost/containers/create?name=$NAME" \
-d "{\"Image\": \"$IMAGE\", \"HostConfig\": {\"PortBindings\": {\"80/tcp\": [{\"HostPort\": \"8080\"}]}}}"
echo "Starting container..."
curl --unix-socket "$SOCKET" -X POST "http://localhost/containers/$NAME/start"
echo "Deployment complete: $IMAGE"
Run it as ./deploy.sh 1.5.2.
Automating with Python (docker-py)
The same operations are far more readable with the official SDK:
import docker
client = docker.from_env()
def deploy(image_tag: str):
image = f"myregistry/web-app:{image_tag}"
print(f"Pulling {image}")
client.images.pull(image)
try:
old = client.containers.get("web-app")
print("Removing existing container")
old.remove(force=True)
except docker.errors.NotFound:
pass
print("Starting new container")
client.containers.run(
image,
name="web-app",
detach=True,
ports={"80/tcp": 8080},
restart_policy={"Name": "unless-stopped"}
)
print(f"Deployed {image}")
if __name__ == "__main__":
deploy("1.5.2")
Automating with Node.js (dockerode)
If your automation stack is JavaScript-based, dockerode is the equivalent library:
const Docker = require('dockerode');
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
async function deploy(imageTag) {
const image = `myregistry/web-app:${imageTag}`;
await docker.pull(image);
try {
const old = docker.getContainer('web-app');
await old.remove({ force: true });
} catch (e) {
if (e.statusCode !== 404) throw e;
}
const container = await docker.createContainer({
Image: image,
name: 'web-app',
ExposedPorts: { '80/tcp': {} },
HostConfig: {
PortBindings: { '80/tcp': [{ HostPort: '8080' }] },
RestartPolicy: { Name: 'unless-stopped' }
}
});
await container.start();
console.log(`Deployed ${image}`);
}
deploy('1.5.2').catch(console.error);
Integrating with CI/CD Pipelines
A common pattern is a GitLab CI or GitHub Actions job that builds an image, pushes it, then calls the remote API on a target host to redeploy. GitHub Actions example:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
docker build -t myregistry/web-app:${{ github.sha }} .
docker push myregistry/web-app:${{ github.sha }}
- name: Trigger remote deploy
env:
DOCKER_HOST: tcp://192.168.1.20:2376
DOCKER_TLS_VERIFY: "1"
run: |
echo "$DOCKER_CLIENT_CERT" > cert.pem
echo "$DOCKER_CLIENT_KEY" > key.pem
echo "$DOCKER_CA_CERT" > ca.pem
export DOCKER_CERT_PATH=$(pwd)
docker pull myregistry/web-app:${{ github.sha }}
docker stop web-app || true
docker rm web-app || true
docker run -d --name web-app -p 8080:80 --restart unless-stopped myregistry/web-app:${{ github.sha }}
Secrets (DOCKER_CLIENT_CERT, DOCKER_CLIENT_KEY, DOCKER_CA_CERT) would be stored in your CI provider’s encrypted secrets store, not committed to the repo.
Streaming Events for Reactive Automation
Rather than polling, you can subscribe to the daemon’s event stream and react in real time — useful for auto-restarting failed containers, alerting, or audit logging:
curl --unix-socket /var/run/docker.sock http://localhost/events
This is a long-lived HTTP connection that streams newline-delimited JSON events as they happen:
{"status":"die","id":"a1b2c3...","Type":"container","Action":"die","Actor":{"Attributes":{"exitCode":"1"}}}
A simple reactive script using docker-py:
import docker
client = docker.from_env()
for event in client.events(decode=True):
if event.get("Type") == "container" and event.get("Action") == "die":
name = event["Actor"]["Attributes"].get("name")
exit_code = event["Actor"]["Attributes"].get("exitCode")
print(f"Container {name} died with exit code {exit_code} — restarting")
client.containers.get(name).start()
Internal Working: Request Lifecycle
Every request to the Engine API passes through the daemon’s internal router, which dispatches to the relevant subsystem: containerd for container lifecycle operations (via gRPC internally), the image service for pulls/builds, and the libnetwork subsystem for network operations. The REST layer is essentially a thin, versioned façade over these internal components — which is also why API version compatibility matters: a client built against v1.24 semantics may not understand response fields introduced in v1.47.
Security Considerations for Automation
- Automation scripts often run with elevated CI/CD permissions — treat any script with Docker API access as equivalent to root access on that host.
- Store TLS certs and SSH keys in your CI/CD system’s secrets manager, never in plaintext repo files.
- Scope automation to the minimum required: a deploy script that only needs to restart one named container shouldn’t have unrestricted API access to every container on the host if you can avoid it (consider per-host automation users with their own client certs).
- Never mount
/var/run/docker.sockinto a container unless that container is fully trusted — doing so effectively grants it root on the host, since it can create privileged containers and mount the host filesystem. - Log every automated API call (image, action, timestamp, initiating pipeline) to a central location so remote changes are auditable after the fact.
Best Practices
- Prefer official SDKs (
docker-py,dockerode) over rawcurlfor anything beyond quick diagnostics — they handle API versioning, error parsing, and streaming responses correctly. - Pin the Engine API version your automation targets (
client.api_versionin docker-py, or the?version=query param) so a daemon upgrade doesn’t silently break your scripts. - Make deploy scripts idempotent — check for existing resources before creating new ones, as shown in the examples above, so re-running a pipeline doesn’t fail or duplicate containers.
- Use
restart_policy: unless-stoppedoron-failurein automated deployments so containers recover from crashes without manual intervention. - Prefer event-driven automation (subscribing to
/events) over polling loops where real-time reaction matters — it’s both more efficient and lower latency.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
curl: (7) Failed to connect on Unix socket | Wrong socket path or Docker not running | Confirm with systemctl status docker and check socket path |
404 page not found from API | Wrong API version or malformed endpoint path | Check the endpoint against the Engine API reference for your Docker version |
Automation script hangs on /events | Long-lived connection with no read timeout set | Set an explicit client timeout and reconnect logic |
| Container created but never starts | Missing start call after create — these are separate API calls | Always follow POST /containers/create with POST /containers/{id}/start |
| CI pipeline fails only in production | TLS certs not correctly injected as CI secrets | Verify secret variables are populated and paths are correct in the job |
Summary
The Docker Remote API is the same REST interface the CLI itself uses, which means anything Docker can do, your automation can do too — with far more flexibility around conditionals, integration with other systems, and event-driven reactions. Whether you reach for raw curl, the official Python or Node SDKs, or wire it into a CI/CD pipeline, the underlying model is consistent: authenticate, call a versioned REST endpoint, and handle the JSON response or event stream. Treat that API with the same security discipline you’d apply to any other root-equivalent interface.
References
- Docker Engine API reference: https://docs.docker.com/engine/api/
- Docker SDK for Python: https://docker-py.readthedocs.io/
- Dockerode (Node.js SDK): https://github.com/apocas/dockerode
- Docker daemon security guide: https://docs.docker.com/engine/security/protect-access/