If you’ve ever tried to run Molecule with Podman instead of Docker, you already know the truth: it never “just works.” Between dynamic inventories, missing Python interpreters, inconsistent container images, and Molecule’s habit of generating YAML that looks like it was written by a committee, you spend more time debugging the framework than testing your Ansible roles.
This post walks through a real, working Molecule + Podman setup that supports multiple OS containers (Ubuntu, Rocky, CentOS Stream), uses Podman natively, and avoids the usual landmines:
- Python missing inside containers
- Fact‑gathering failures
- Molecule’s dynamic inventory breaking
- Destroy steps leaving orphaned containers
- Podman connection plugin quirks
If you’ve been fighting Molecule + Podman, this example will save you hours.
The Problem: Molecule + Podman Is Not a Drop‑In Replacement for Docker
Molecule’s Docker driver is mature. Podman’s driver… well… it isn’t.
If you’re testing roles across multiple OSes, these problems multiply and you eventually just give up.
The Working Example: Multi‑OS Molecule Scenario Using Podman
Here’s the structure:
molecule/default/
├── create.yml
├── prepare.yml
├── converge.yml
├── verify.yml
├── destroy.yml
├── molecule.yml
└── requirements.yml
Key idea:
We start our own containers with podman and generate our own dynamic inventory inside create.yml and force Molecule to use it.
This avoids Molecule’s broken Podman inventory plugin entirely.
1. create.yml Start Containers + Build Inventory
We define our Podman containers, start them and force feed the inventory to molecule:
- name: Create
hosts: localhost
gather_facts: false
vars:
molecule_inventory:
all:
hosts: {}
molecule: {}
podman:
- name: ubuntu-2404
image: ubuntu:24.04
command: /bin/bash
tty: true
- name: rocky9
image: quay.io/rockylinux/rockylinux:9
command: /bin/bash
tty: true
- name: rocky10
image: quay.io/rockylinux/rockylinux:10
command: /bin/bash
tty: true
- name: cs9
image: quay.io/centos/centos:stream9
command: /bin/bash
tty: true
tasks:
- name: Start Podman container(s)
containers.podman.podman_container:
name: "{{ item.name }}"
image: "{{ item.image }}"
state: started
command: "{{ item.command | default(omit) }}"
tty: "{{ item.tty | default(omit) }}"
register: result
loop: "{{ podman }}"
loop_control:
label: "{{ item.name }}"
- name: Add container to molecule_inventory
vars:
inventory_partial_yaml: |
all:
children:
molecule:
hosts:
"{{ item.name }}":
ansible_connection: containers.podman.podman
ansible.builtin.set_fact:
molecule_inventory: >
{{ molecule_inventory | combine(inventory_partial_yaml | from_yaml, recursive=true) }}
loop: "{{ podman }}"
loop_control:
label: "{{ item.name }}"
- name: Dump molecule_inventory
ansible.builtin.copy:
content: |
{{ molecule_inventory | to_yaml }}
dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml"
mode: "0600"
- name: Force inventory refresh
ansible.builtin.meta: refresh_inventory
- name: Fail if molecule group is missing
ansible.builtin.assert:
that: "'molecule' in groups"
fail_msg: |
molecule group was not found inside inventory groups: {{ groups }}
run_once: true # noqa: run-once[task]
# we want to avoid errors like "Failed to create temporary directory"
- name: Validate that inventory was refreshed
hosts: molecule
gather_facts: false
tasks:
- name: Check uname
ansible.builtin.raw: uname -a
register: result
changed_when: false
- name: Display uname info
ansible.builtin.debug:
msg: "{{ result.stdout }}"
It’s important to note, we added our containers to the inventory with the podman ansible_connection, containers.podman.podman_container.
Out inventory file under the covers looks like this:
all:
children:
molecule:
hosts:
ubuntu-2404:
ansible_connection: containers.podman.podman
rocky9:
ansible_connection: containers.podman.podman
rocky10:
ansible_connection: containers.podman.podman
cs9:
ansible_connection: containers.podman.podman
And note that we force Molecule to refresh:
- meta: refresh_inventory
This is the magic step most examples miss.
2. prepare.yml Only Touch Ubuntu Containers
Instead of conditionals, we use a host pattern and fix the missing python3 in the Ubuntu containers.
- hosts: "ubuntu*"
gather_facts: false
tasks:
- name: Refresh apt cache Ubuntu container
raw: |
apt-get update
- name: Install Python inside Ubuntu container
raw: |
apt-get install -y python3
- name: Set correct Python interpreter for Ubuntu
set_fact:
ansible_python_interpreter: /usr/bin/python3
Why this works:
- Molecule names your Ubuntu containers
ubuntu-* - No need for OS detection
- No need for fact gathering
This is the cleanest possible prepare step.
3. converge.yml Run Tasks on All Containers
This is the thing we’re testing
- hosts: all
tasks:
- shell: "cat /etc/*release*"
register: result
- debug: msg="{{ result }}"
Simple, predictable, works across all OSes or well, it does what you want your code to do.
4. verify.yml OS‑Specific Assertions
- hosts: rocky9
tasks:
- assert:
that:
- "'dnf' in ansible_facts.packages"
- hosts: ubuntu-2404
tasks:
- assert:
that:
- "'apt' in ansible_facts.packages"
This is how you validate OS‑specific behaviour cleanly, again, you do you.
5. destroy.yml Clean Container Removal
- name: Destroy molecule containers
hosts: molecule
gather_facts: false
tasks:
- name: Delete Podman container(s)
containers.podman.podman_container:
name: "{{ item }}"
state: absent
loop: "{{ groups['all'] | difference(['localhost']) }}"
delegate_to: localhost
- name: Remove dynamic molecule inventory
hosts: localhost
gather_facts: false
tasks:
- name: Remove dynamic inventory file
ansible.builtin.file:
path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml"
state: absent
This avoids:
- Molecule leaving orphaned containers
- Podman complaining about missing names
- localhost being treated as a container
Why This Example Works When Others Don’t
1. We bypass Molecule’s broken Podman inventory plugin
We generate our own containers and inventory and force Molecule to use it.
2. We use host patterns instead of OS detection
Fact gathering doesn’t work until Python is installed.
3. We use Podman’s connection plugin correctly
Every host gets:
ansible_connection: containers.podman.podman
4. We refresh inventory explicitly
Without this, Molecule uses stale inventory.
Conclusion: Molecule + Podman Is Painful …. But Fixable
Podman is great. Molecule is great. Together? They fight.
But with:
- explicit container creation
- explicit inventory generation
- host patterns
- correct Podman connection plugin
- correct Ubuntu prepare steps
- correct destroy logic
You can run a multi‑OS Molecule test matrix under Podman reliably.
This example proves it.
If you’re building cloud automation, CI pipelines, or multi‑distro Ansible roles, this pattern will save you hours of frustration.