Skip to main content
RunBook Academy

AnsibleXLII · Ansible Beyond Linux ServersBeyond Linux servers

Ansible and Docker hosts: drawing the line

Advanced⏱ ~26 minansible-coreansible-galaxy

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

Not yet marked complete on this device.

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:

KindNamesHonest role
Stack modulecommunity.docker.docker_compose_v2The recommended apply verb
Container modulescommunity.docker.docker_container, docker_imagePowerful; usually the wrong altitude
Info modulescommunity.docker.docker_host_info, docker_container_infoRead-only, genuinely useful
Connection pluginscommunity.docker.docker, docker_api, nsenterEscape hatches, discussed below
Inventory plugincommunity.docker.docker_containersContainers 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

  1. 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?

  2. 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?

  3. Q3. Which responsibilities sit on the Ansible side of the boundary this lesson argues for? Select all that apply.

  4. 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.