How to Automate Docker Tasks with Docker Remote API: Complete Automation and Integration Guide

How to Automate Docker Tasks with Docker Remote API

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.sock into 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 raw curl for anything beyond quick diagnostics — they handle API versioning, error parsing, and streaming responses correctly.
  • Pin the Engine API version your automation targets (client.api_version in 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-stopped or on-failure in 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

SymptomCauseFix
curl: (7) Failed to connect on Unix socketWrong socket path or Docker not runningConfirm with systemctl status docker and check socket path
404 page not found from APIWrong API version or malformed endpoint pathCheck the endpoint against the Engine API reference for your Docker version
Automation script hangs on /eventsLong-lived connection with no read timeout setSet an explicit client timeout and reconnect logic
Container created but never startsMissing start call after create — these are separate API callsAlways follow POST /containers/create with POST /containers/{id}/start
CI pipeline fails only in productionTLS certs not correctly injected as CI secretsVerify 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/

Total
0
Shares

Leave a Reply

Previous Post
How to Access the Docker Daemon Remotely

How to Access the Docker Daemon Remotely: Secure Remote Connection Setup and Configuration

Next Post
How to Secure the Docker Daemon for Remote Access

How to Secure the Docker Daemon for Remote Access: TLS Encryption and Authentication Setup

Related Posts