AnsibleXLII · Ansible Beyond Linux ServersBeyond Linux servers
Ansible and Docker hosts: drawing the line
What you'll learn
- State the division of ownership between Ansible and a container runtime and justify it
- Identify what community.docker offers and which parts belong in a production design
- Recognise the two-owners failure where Ansible and Compose both reconcile the same state
- Explain why declaring containers as Ansible tasks costs you the container platform
- Decide when the docker connection plugin is legitimate and when it is an escape hatch
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
A container host is a Linux server, so all of this course applies to it. That is the easy half.
The hard half is that a container host also runs a second configuration management system. Docker keeps a container in the state its definition asks for, restarts it when it exits, and decides whether it is healthy. Those are convergence behaviours. You now have two things capable of reconciling the same state, and the interesting question in this lesson is not what Ansible can do to a container host — the collection is capable and will let you do nearly anything — but where Ansible should stop.
The recommendation this lesson argues for is short:
Ansible converges the host and delivers the stack definition. Docker owns the runtime.
Everything below is the reasoning, and the failure modes that appear when the line is drawn somewhere else.
What Ansible is unambiguously responsible for
Everything that exists before a container can run, and everything that outlives one:
- The packages, including the container engine itself and its repository and pinning.
- Kernel parameters, cgroup and storage-driver configuration, the filesystem layout for the data directory.
- Users, groups, and who is in the group that grants access to the daemon socket — a privilege boundary the Docker socket security lesson treats as equivalent to root, correctly.
- Firewall rules, host networking, DNS, NTP, certificates.
- The systemd units that start a stack at boot, which the Docker course covers in systemd units for Compose stacks.
- Log shipping, backup agents, monitoring agents, patching.
- The compose file, rendered from a template and placed on disk.
That last one is the seam. It is a file, on a filesystem, with an owner and a mode and a validated content — which is precisely the kind of object Ansible is excellent at owning.
- name: Converge a container host
hosts: container_hosts
become: true
tasks:
- name: Render the stack definition
ansible.builtin.template:
src: compose.yaml.j2
dest: /srv/app/compose.yaml
owner: root
group: root
mode: '0640'
validate: 'docker compose -f %s config --quiet'
notify: Apply the stack
handlers:
- name: Apply the stack
community.docker.docker_compose_v2:
project_src: /srv/app
state: present
The validate: argument is doing real work there. It renders the template
to a temporary file and asks Compose whether it parses before the file is
ever moved into place, which is the
render, check, activate
pattern applied to a compose file. A template with a typo never reaches the
host.
Note also what the handler does not do: it does not enumerate containers, ports, volumes or restart policies. It hands Compose a file and asks it to reconcile. Ansible’s statement is “this definition should be applied”; the definition itself is the artefact under review.
What the collection can do, and what that means
community.docker is a large collection and it is worth knowing its shape
before deciding what to use:
| Kind | Names | Honest role |
|---|---|---|
| Stack module | community.docker.docker_compose_v2 | The recommended apply verb |
| Container modules | community.docker.docker_container, docker_image | Powerful; usually the wrong altitude |
| Info modules | community.docker.docker_host_info, docker_container_info | Read-only, genuinely useful |
| Connection plugins | community.docker.docker, docker_api, nsenter | Escape hatches, discussed below |
| Inventory plugin | community.docker.docker_containers | Containers as inventory hosts |
The existence of docker_container invites a design where every container
is an Ansible task, with its image, ports, volumes, environment and restart
policy expressed as module arguments. It works. People run it in
production. And it costs three things that are not obvious on day one.
You lose the artefact. A compose file is a reviewable, diffable,
portable description of a stack that a developer can run locally and a
reviewer can read in one screen. Twelve docker_container tasks spread
across a role are none of those things.
You lose the tooling. Compose knows about dependency ordering, profiles,
project-scoped networks and volumes, and config validation. Reimplementing
the useful parts in task ordering and when: conditions is work you did not
need to do.
You inherit recreation semantics. docker_container reconciles a
container against the parameters you gave it, and a container is not
mutable — changing an environment variable means destroying and recreating
it. A one-character change to a variable in group_vars becomes a
container restart across the fleet, in parallel, at the speed of forks.
That is a SERVICE-IMPACT change wearing the clothes of a variable edit,
and the recap will call it changed=1.
The connection plugins are escape hatches
community.docker.docker and community.docker.docker_api let Ansible
treat a running container as a managed node, executing tasks inside it
rather than on the host. nsenter does something similar by entering the
host namespaces from a privileged container.
These are legitimate tools with narrow uses:
- Testing. Molecule drives containers this way, and Part XXVI leans on it. A disposable container is a fine place to converge a role.
- Bootstrapping a host that has no SSH yet, from a container that does.
- Forensics. Reading state out of a container during an incident.
They are not a production configuration path, for the reason the callout above gives: anything Ansible writes inside a container is state with no durable home. The container is a cache of the image plus its mounts. Writing into it produces exactly the class of undocumented, un-rebuildable machine that the drift part of this course calls a snowflake — with the added insult that it disappears on restart rather than accumulating.
The docker_containers inventory plugin has the same character: excellent
for a testing scenario or for a read-only survey of what is running, a
warning sign in a production play that intends to change something.
Health is not Ansible’s to assert
Ansible can start a stack. It is poorly placed to tell you the stack is working, and this is where the boundary earns its keep.
A docker_compose_v2 task that reports changed tells you Compose applied
the definition. It does not tell you the application is serving requests. A
container can be running and broken; that is the entire reason container
health checks exist, and the Docker course covers their design in
Compose health checks.
The division that follows:
- Docker owns liveness. The health check is defined in the compose file, evaluated continuously by the runtime, and acted on by the restart policy. It runs at three in the morning when no playbook is running.
- Ansible owns the point-in-time gate. After applying a stack, a play may reasonably poll the service until it answers, and fail the run if it does not — the verify the outcome, not the task result discipline.
- name: Wait for the stack to answer before declaring the change good
ansible.builtin.uri:
url: "http://{{ ansible_host }}:8080/healthz"
status_code: 200
delegate_to: localhost
register: health
until: health.status == 200
retries: 12
delay: 5
That task is a gate, not a monitor. It answers “did this change work”, which is the question the run is entitled to ask. It does not replace the health check, and a play that has no such gate is the subject of the whole of Part XLIII: a successful run is not a healthy service.
Knowledge check
Knowledge check · 4 questions
Q1. A role manages a container with docker_container and also writes a config file into that container using the docker connection plugin. Both tasks are idempotent and both report ok. What is wrong?
Q2. Someone changes one environment variable in group_vars for a stack managed entirely by docker_container tasks. What is the operational classification of that change?
Q3. Which responsibilities sit on the Ansible side of the boundary this lesson argues for? Select all that apply.
Q4. A docker_compose_v2 task that reports changed is evidence that the application is now serving requests.
Passing score: 75%. Answers are checked in this browser.