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: trueon the target host) or over a TLS-secured TCP connection if you setdocker_hostand 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: truecarefully — 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
- Use tags in your images, never
latest, in any playbook meant for repeatable production deployments —pull: truecombined with a floating tag can silently deploy different code to different hosts if pulls happen at different times. - 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.
- Use
docker_container_infofor health-gated rollouts rather than blindly restarting every host at once. - Keep secrets in Ansible Vault, and rotate the vault password periodically.
- Test playbooks against a staging inventory group before running against production hosts.
Troubleshooting
- “Failed to import docker or docker-py” — the Python
dockerSDK is missing on the target host; install it viapipas 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_userwas actually added to thedockergroup 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-filepointing at a securely stored file (or a secrets manager integration) instead of--ask-vault-passin 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/