Original context: Docker for Mac. The networking behaviour described here is most pronounced on macOS, but the underlying Docker networking concepts apply generally.
The Problem
A common goal when learning or testing Ansible is to spin up a small cluster of Docker containers: one container acting as the Ansible control node and one or more containers acting as managed targets. The setup seems straightforward, but many people hit the same wall immediately: Ansible reports that every target is UNREACHABLE.
A typical error looks like this:
fatal: [127.0.0.1]: UNREACHABLE! => {"changed": false, "msg": "[Errno None] Unable to connect to port 9800 on 127.0.0.1", "unreachable": true}Code language: PHP (php)
The Ansible inventory (hosts file) in this scenario looks like:
[docker-hosts]
127.0.0.1:9800
127.0.0.1:9801Code language: CSS (css)
And a minimal playbook.yml targeting those hosts:
---
- hosts: docker-hosts
remote_user: root
become: yes
become_method: sudo
tasks:
- name: pulling docker image
docker_image:
name: centos
Run with:
ansible-playbook playbook.yml -f 10Code language: CSS (css)
Both 127.0.0.1 and 0.0.0.0 fail as target addresses. Here is why, and how to fix it.
Why 127.0.0.1 Does Not Work Between Containers
Inside any Docker container, 127.0.0.1 (loopback) refers to that container itself, not to the Docker host and not to any sibling container. When the Ansible control container tries to SSH to 127.0.0.1:9800, it is attempting to connect to a port on its own loopback interface — which has nothing listening on it.
Even if you published the target containers’ SSH ports to the Docker host (e.g., -p 9800:22), the host’s loopback is a separate network namespace from the container’s loopback. On Linux you might be able to reach the host via the default bridge gateway IP, but on Docker for Mac there is an additional networking layer (a Linux VM) that makes 127.0.0.1 from inside a container even less reliable for this purpose.
Solution 1: Use a User-Defined Docker Bridge Network
The cleanest approach is to put all containers on the same user-defined bridge network. Docker then provides automatic DNS resolution by container name, so you can address targets by name rather than by IP address.
Step 1 — Create a dedicated network
docker network create ansible-test
Step 2 — Start the target containers on that network
docker run -d --name target1 --network ansible-test --rm centos:latest /usr/sbin/sshd -D
docker run -d --name target2 --network ansible-test --rm centos:latest /usr/sbin/sshd -D
Note: A base CentOS image does not run SSH by default. You will need a custom image that installs and configures openssh-server and sets up SSH keys. See Solution 3 below for an alternative that avoids SSH entirely.
Step 3 — Start the Ansible control container on the same network
docker run -it --name ansible-control --network ansible-test --rm your-ansible-image bash
Step 4 — Update the Ansible inventory to use container names
[docker-hosts]
target1
target2Code language: CSS (css)
Docker’s embedded DNS resolves target1 and target2 to the correct container IP addresses automatically. No port mapping to the host is required.
Solution 2: Use Docker Compose to Manage the Network Automatically
Docker Compose creates a dedicated bridge network for each project by default, so all services within one Compose file can reach each other by service name.
version: "3"
services:
ansible-control:
image: your-ansible-image
depends_on:
- target1
- target2
volumes:
- ./playbooks:/playbooks
target1:
image: your-ssh-enabled-centos-image
target2:
image: your-ssh-enabled-centos-imageCode language: PHP (php)
Your Ansible inventory then simply references target1 and target2 by service name.
Solution 3: Use the Docker Connection Plugin Instead of SSH
If the goal is purely to test Ansible playbooks against containers (rather than to test SSH connectivity itself), you can skip SSH altogether. Ansible ships with a docker connection plugin that uses docker exec internally.
Set the connection type in your inventory:
[docker-hosts]
target1 ansible_connection=docker
target2 ansible_connection=docker
With this approach:
- No SSH daemon is needed in the target containers.
- No port publishing or network configuration is required.
- The Ansible control node must be able to run
docker exec— either by running on the Docker host itself, or by mounting the Docker socket into the control container (-v /var/run/docker.sock:/var/run/docker.sock).
Why Port-Mapping to the Host Is Not Enough on Its Own
Some guides suggest publishing SSH ports to the host (e.g., -p 9800:22 -p 9801:22) and then pointing Ansible at the host’s IP. This can work when Ansible runs directly on the host, because the host’s loopback or LAN IP is reachable. However, when Ansible itself runs inside a container, the situation changes:
- On Linux, the default Docker bridge gateway (typically
172.17.0.1) is the host’s bridge IP and is reachable from containers. You could use that IP in the inventory. - On Docker for Mac, the Docker engine runs inside a Linux VM. The host’s
localhostis not directly reachable from inside a container using127.0.0.1. Docker for Mac provides the special hostnamehost.docker.internalto reach the host machine, but this adds unnecessary complexity.
Using a user-defined network (Solution 1) or the Docker connection plugin (Solution 3) avoids all of this complexity.
Summary
| Approach | Needs SSH in target? | Works on Docker for Mac? | Complexity |
|---|---|---|---|
| User-defined bridge network + SSH | Yes | Yes | Low |
| Docker Compose network + SSH | Yes | Yes | Low |
Docker connection plugin (docker exec) | No | Yes | Very low |
| Port-map to host + host IP in inventory | Yes | Fragile | Medium |
For most local Ansible testing workflows, a user-defined Docker network with container-name-based inventory, or the docker connection plugin, is the recommended approach. Avoid addressing sibling containers via 127.0.0.1 — it refers to the container’s own loopback, not to any other container or to the host.