Ansible CI/CD Docker Images for Reproducible Role and Playbook Testing
Ansible automation often fails in CI for boring reasons: different Python versions, missing system packages, missing sshpass, a Molecule image that does not support systemd, or a pipeline runner that is not close to the Linux distribution used in production. The bsmeding/ansible_cicd_* images are built to remove that drift.
The images include Ansible, common Ansible Python libraries, linting tools, Molecule-friendly system dependencies, and distro-specific builds for Ubuntu, Debian, Rocky Linux, and Alpine.
When To Use These Images
Use ansible_cicd images when you need to test Ansible roles, playbooks, collections, inventories, or Molecule scenarios in a predictable environment.
Good fits:
- Role testing with Molecule.
- Playbook syntax checks and dry runs.
ansible-lint,yamllint, and inventory validation.- Testing against several Linux distributions.
- CI pipelines that need Ansible preinstalled instead of installing it on every run.
- Docker-based role tests where the image is used as the Molecule instance.
Use another image when:
- You only need Python NetDevOps libraries and no Ansible runtime. Use
netdevops_cicd. - You need LLM, prompt, agent, or RAG evaluation tooling. Use
aiops_cicd. - You are building a minimal production runtime image. These are CI images, not application runtimes.
Image Tags
The general pattern is:
Common tags:
ubuntu,ubuntu2404,ubuntu2604debian,debian12,debian13rockylinux,rockylinux8,rockylinux9alpine3,alpine3.22,alpine3.23
Older tags such as ubuntu2004, ubuntu2204, debian11, alpine3.20, and alpine3.21 may still exist on Docker Hub for older pipelines, but the newest two versions per distro family are the actively maintained targets.
GitHub Actions: Lint And Syntax Check
name: Ansible CI
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
container:
image: bsmeding/ansible_cicd_ubuntu:latest
steps:
- uses: actions/checkout@v5
- name: Show tool versions
run: |
ansible --version
ansible-lint --version
yamllint --version
- name: Lint YAML
run: yamllint .
- name: Lint Ansible
run: ansible-lint
- name: Syntax check site playbook
run: ansible-playbook -i inventories/dev/hosts.yml site.yml --syntax-check
GitHub Actions: Molecule Test Matrix
This example runs the same role against multiple distro images.
name: Molecule
on:
pull_request:
push:
branches:
- main
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
distro:
- ubuntu2404
- debian12
- rockylinux9
- alpine3.23
steps:
- uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Run Molecule
run: molecule test
env:
PY_COLORS: "1"
ANSIBLE_FORCE_COLOR: "1"
MOLECULE_DISTRO: ${{ matrix.distro }}
Example molecule/default/molecule.yml:
---
driver:
name: docker
platforms:
- name: instance
image: "bsmeding/ansible_cicd_${MOLECULE_DISTRO:-ubuntu2404}:latest"
command: ${MOLECULE_DOCKER_COMMAND:-"/lib/systemd/systemd"}
privileged: true
pre_build_image: true
cgroupns_mode: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
provisioner:
name: ansible
config_options:
defaults:
remote_tmp: /var/tmp/.ansible
For Alpine, systemd is not available, so override the command:
GitHub Actions: Publish A Role After Tests
name: Test And Publish Role
on:
push:
tags:
- "v*"
jobs:
test:
runs-on: ubuntu-latest
container:
image: bsmeding/ansible_cicd_debian:latest
steps:
- uses: actions/checkout@v5
- run: ansible-lint
- run: molecule test
publish:
needs: test
runs-on: ubuntu-latest
container:
image: bsmeding/ansible_cicd_debian:latest
steps:
- uses: actions/checkout@v5
- name: Import role into Ansible Galaxy
env:
ANSIBLE_GALAXY_API_KEY: ${{ secrets.ANSIBLE_GALAXY_API_KEY }}
run: |
ansible-galaxy role import \
--api-key "$ANSIBLE_GALAXY_API_KEY" \
bsmeding "${GITHUB_REPOSITORY#*/}"
GitLab CI: Lint, Syntax, And Molecule
stages:
- lint
- test
variables:
ANSIBLE_FORCE_COLOR: "true"
PY_COLORS: "1"
lint:
stage: lint
image: bsmeding/ansible_cicd_ubuntu:latest
script:
- yamllint .
- ansible-lint
- ansible-playbook -i inventories/dev/hosts.yml site.yml --syntax-check
molecule:
stage: test
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
MOLECULE_DISTRO: ubuntu2404
before_script:
- apk add --no-cache python3 py3-pip
- pip install --break-system-packages molecule molecule-plugins[docker] ansible
script:
- molecule test
Gitea Or Forgejo Actions
name: ansible-role-ci
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
container:
image: bsmeding/ansible_cicd_ubuntu2404:latest
steps:
- uses: actions/checkout@v4
- run: yamllint .
- run: ansible-lint
- run: ansible-playbook --syntax-check -i inventory.yml playbook.yml
Jenkins Declarative Pipeline
pipeline {
agent {
docker {
image 'bsmeding/ansible_cicd_ubuntu:latest'
args '-u root:root'
}
}
stages {
stage('Versions') {
steps {
sh 'ansible --version'
sh 'python --version'
}
}
stage('Lint') {
steps {
sh 'yamllint .'
sh 'ansible-lint'
}
}
stage('Syntax Check') {
steps {
sh 'ansible-playbook -i inventories/dev/hosts.yml site.yml --syntax-check'
}
}
stage('Molecule') {
steps {
sh 'molecule test'
}
}
}
}
Local CI Reproduction
When CI fails, reproduce the same environment locally:
Then run the same checks:
yamllint .
ansible-lint
ansible-playbook -i inventories/dev/hosts.yml site.yml --syntax-check
molecule test
Practical Tips
- Use
ubuntuordebianaliases for normal pipelines. - Use version-specific tags when your test must match a target OS.
- Use
rockylinux8only when you still support older Enterprise Linux hosts. - Use Alpine for lightweight syntax and Python checks, but not for systemd-based Molecule tests.
- Pin your role dependencies in
requirements.ymland install them in CI before running Molecule. - Keep secrets in CI secret stores, never in
group_varsor committed inventories.
Summary
The ansible_cicd images are a practical base for repeatable Ansible CI. They keep Ansible, Python modules, lint tooling, and Molecule-friendly OS dependencies together so every pull request is tested in a known environment.