How to Orchestrate Containers with Ansible Docker Module: Automation and Management Guide

How to Orchestrating Containers with Ansible Docker Module

Somewhere between “a few docker run commands in a README” and “a full Kubernetes cluster,” there’s a huge amount of real-world container infrastructure that’s best managed with plain configuration management — and Ansible’s Docker modules are, in my experience, the most underrated tool for that middle ground. This guide covers everything from installing Docker itself via Ansible to building multi-container application stacks, all declaratively, all idempotent, all re-runnable without side effects.

Why Ansible for Docker?

  • Idempotency — running the same playbook twice doesn’t recreate containers unnecessarily; Ansible checks current state first.
  • Agentless — no daemon to install on managed hosts beyond SSH access and Python.
  • Fits existing infrastructure-as-code workflows — the same tool that configures your VMs can also manage the containers on them.
  • Good middle ground — lighter-weight than Kubernetes for small-to-medium fleets of hosts running a handful of services each.

Prerequisites

pip install ansible docker

Install the community Docker collection (the modules used to ship in ansible-core, they’ve since moved to the community.docker collection):

ansible-galaxy collection install community.docker

Verify:

ansible-galaxy collection list | grep docker

Expected output:

community.docker    4.1.0

Inventory Setup

# inventory.ini
[docker_hosts]

docker-host-01 ansible_host=203.0.113.10 ansible_user=ubuntu docker-host-02 ansible_host=203.0.113.11 ansible_user=ubuntu

Step 1: Installing Docker Itself with Ansible

Before orchestrating containers, you need Docker installed on the target hosts. Here’s a full playbook for that.

# install-docker.yml
---
- name: Install Docker Engine
  hosts: docker_hosts
  become: true
  tasks:
    - name: Install prerequisite packages
      apt:
        name:
          - ca-certificates
          - curl
          - gnupg
        state: present
        update_cache: true

    - name: Add Docker's official GPG key
      apt_key:
        url: https://download.docker.com/linux/ubuntu/gpg
        state: present

    - name: Add Docker repository
      apt_repository:
        repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
        state: present

    - name: Install Docker Engine
      apt:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-compose-plugin
        state: present
        update_cache: true

    - name: Ensure Docker service is running
      systemd:
        name: docker
        state: started
        enabled: true

    - name: Add ansible_user to the docker group
      user:
        name: "{{ ansible_user }}"
        groups: docker
        append: true

    - name: Install Python Docker SDK (required by community.docker modules)
      pip:
        name: docker
        state: present

Run it:

ansible-playbook -i inventory.ini install-docker.yml

Expected output (abridged):

PLAY [Install Docker Engine] **************************************

TASK [Ensure Docker service is running] ***************************
changed: [docker-host-01]
changed: [docker-host-02]

PLAY RECAP **********************************************************
docker-host-01 : ok=6 changed=5 unreachable=0 failed=0
docker-host-02 : ok=6 changed=5 unreachable=0 failed=0

Step 2: Running a Single Container with community.docker.docker_container

# run-nginx.yml
---
- name: Run Nginx container
  hosts: docker_hosts
  become: true
  tasks:
    - name: Start Nginx container
      community.docker.docker_container:
        name: web-nginx
        image: nginx:latest
        state: started
        restart_policy: unless-stopped
        ports:
          - "80:80"
        env:
          NGINX_HOST: "{{ ansible_host }}"
ansible-playbook -i inventory.ini run-nginx.yml

Expected output:

TASK [Start Nginx container] ****************************************
changed: [docker-host-01]
changed: [docker-host-02]

Run it again immediately — because Ansible checks current state before acting, nothing changes the second time:

TASK [Start Nginx container] ****************************************
ok: [docker-host-01]
ok: [docker-host-02]

That ok instead of changed on the second run is the idempotency guarantee in action.

Step 3: Pulling and Managing Images with docker_image

- name: Pull a specific image tag
  community.docker.docker_image:
    name: redis
    tag: "7.4-alpine"
    source: pull

Step 4: Managing Networks with docker_network

- name: Create an application network
  community.docker.docker_network:
    name: app-net
    driver: bridge

- name: Attach container to the network
  community.docker.docker_container:
    name: web-nginx
    image: nginx:latest
    networks:
      - name: app-net
    state: started

Step 5: Managing Volumes with docker_volume

- name: Create a named volume for persistent data
  community.docker.docker_volume:
    name: app-data
    state: present

- name: Mount volume in container
  community.docker.docker_container:
    name: app-db
    image: postgres:16
    volumes:
      - app-data:/var/lib/postgresql/data
    env:
      POSTGRES_PASSWORD: "{{ vault_db_password }}"
    networks:
      - name: app-net
    state: started

Note the vault_db_password reference — sensitive values like database passwords should always come from Ansible Vault rather than being hardcoded in the playbook:

ansible-vault create group_vars/docker_hosts/vault.yml
vault_db_password: "SuperSecretPassword123!"

Run the playbook with:

ansible-playbook -i inventory.ini deploy-app.yml --ask-vault-pass

Step 6: A Complete Multi-Container Application Stack

Here’s a realistic example bringing together a web app, a database, and a reverse proxy — the kind of stack you’d otherwise reach for Compose to define, but expressed as an Ansible playbook so it can be rolled out across a fleet of hosts consistently.

# deploy-app.yml
---
- name: Deploy full application stack
  hosts: docker_hosts
  become: true
  vars_files:
    - group_vars/docker_hosts/vault.yml

  tasks:
    - name: Create application network
      community.docker.docker_network:
        name: app-net
        state: present

    - name: Create database volume
      community.docker.docker_volume:
        name: app-db-data
        state: present

    - name: Run PostgreSQL database
      community.docker.docker_container:
        name: app-db
        image: postgres:16
        state: started
        restart_policy: unless-stopped
        networks:
          - name: app-net
        volumes:
          - app-db-data:/var/lib/postgresql/data
        env:
          POSTGRES_DB: appdb
          POSTGRES_USER: appuser
          POSTGRES_PASSWORD: "{{ vault_db_password }}"

    - name: Run application backend
      community.docker.docker_container:
        name: app-backend
        image: myregistry.example.com/app-backend:1.4.2
        state: started
        restart_policy: unless-stopped
        networks:
          - name: app-net
        env:
          DATABASE_URL: "postgresql://appuser:{{ vault_db_password }}@app-db:5432/appdb"

    - name: Run Nginx reverse proxy
      community.docker.docker_container:
        name: app-proxy
        image: nginx:latest
        state: started
        restart_policy: unless-stopped
        ports:
          - "80:80"
        networks:
          - name: app-net
        volumes:
          - /etc/nginx/conf.d/app.conf:/etc/nginx/conf.d/default.conf:ro

Run it:

ansible-playbook -i inventory.ini deploy-app.yml --ask-vault-pass

Rolling Updates and Image Changes

To roll out a new backend image version, just bump the tag and re-run:

    - name: Run application backend
      community.docker.docker_container:
        name: app-backend
        image: myregistry.example.com/app-backend:1.5.0
        state: started
        pull: true

Setting pull: true forces Ansible to check the registry for a newer image before recreating the container, giving you a one-line rolling update across every host in your inventory.

ansible-playbook -i inventory.ini deploy-app.yml --limit docker-host-01
ansible-playbook -i inventory.ini deploy-app.yml --limit docker-host-02

Running with --limit one host at a time gives you a manual rolling deployment — update host 1, verify health, then move to host 2.

Gathering Container Facts

- name: Gather info about a running container
  community.docker.docker_container_info:
    name: app-backend
  register: backend_info

- name: Show container status
  debug:
    msg: "{{ backend_info.container.State.Status }}"

Expected output:

TASK [Show container status] ****************************************
ok: [docker-host-01] => {
    "msg": "running"
}

This mirrors exactly the kind of data you’d get from docker inspect, but consumable directly inside a playbook for conditional logic (e.g., only restart a downstream service if the upstream one is healthy).

Networking and Security Considerations

  • Ansible’s Docker modules talk to the Docker daemon exactly the way the CLI does — either over the local Unix socket (when running the playbook with become: true on the target host) or over a TLS-secured TCP connection if you set docker_host and TLS parameters in the module arguments.
  • Always store database passwords, API keys, and registry credentials in Ansible Vault, never plaintext in the playbook.
  • Scope become: true carefully — Ansible needs root (or docker-group membership) to talk to the Docker socket, which is itself equivalent to root on the host, so treat playbook repositories with the same access control rigor as the hosts themselves.

Best Practices

  1. Use tags in your images, never latest, in any playbook meant for repeatable production deployments — pull: true combined with a floating tag can silently deploy different code to different hosts if pulls happen at different times.
  2. Split playbooks by concern — one for Docker installation, one for networks/volumes, one for application deployment — so you can re-run just the piece that changed.
  3. Use docker_container_info for health-gated rollouts rather than blindly restarting every host at once.
  4. Keep secrets in Ansible Vault, and rotate the vault password periodically.
  5. Test playbooks against a staging inventory group before running against production hosts.

Troubleshooting

  • “Failed to import docker or docker-py” — the Python docker SDK is missing on the target host; install it via pip as shown in the install playbook above (it’s a dependency of the module itself, separate from the Docker Engine).
  • Container recreated on every run when it shouldn’t be — check for parameters that change between runs, like a timestamp in an environment variable; the module compares configuration and will recreate if anything differs.
  • Permission denied connecting to Docker socket — confirm the ansible_user was actually added to the docker group and that the SSH session used for the playbook run is a fresh one (group membership changes don’t apply to already-established sessions).
  • Vault password prompt breaks CI automation — use --vault-password-file pointing at a securely stored file (or a secrets manager integration) instead of --ask-vault-pass in non-interactive pipelines.

Summary

Ansible’s community.docker collection gives you a declarative, idempotent way to manage everything from Docker installation itself to multi-container application stacks with networks, volumes, and rolling image updates — all without needing a full Kubernetes cluster. It’s a genuinely good fit for teams running a modest fleet of Docker hosts who want infrastructure-as-code discipline without taking on Kubernetes’ operational complexity. And because the underlying primitives are the same Docker Engine API concepts covered elsewhere in this series — containers, networks, volumes, images — everything you already know about Docker itself carries over directly into how you write these playbooks.

References

  • Ansible community.docker collection documentation: https://docs.ansible.com/ansible/latest/collections/community/docker/
  • Ansible Vault documentation: https://docs.ansible.com/ansible/latest/vault_guide/index.html
  • Docker Engine API reference: https://docs.docker.com/reference/api/engine/
  • Kubernetes documentation (CNCF), for comparison at greater scale: https://kubernetes.io/docs/home/
Total
0
Shares

Leave a Reply

Previous Post
How to Start Containers on a Cluster with Docker Swarm

How to Start Containers on a Cluster with Docker Swarm: Orchestration and Scaling Guide

Next Post
How to Access Public Clouds to Run Docker

How to Access Public Clouds to Run Docker: AWS, Azure, and GCP Deployment Options Explained

Related Posts