Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: December 25, 2024
Using Docker Compose in Ansible streamlines the automation of the lifecycle of multi-container workloads. However, the syntax isn’t as straightforward as running docker-compose from the command line.
In this tutorial, we’ll discuss how to use Docker Compose in Ansible, providing a practical illustration.
When using Docker Compose in Ansible, we need a YAML file just as we would if we were running docker-compose from the command line.
Our Docker Compose file will define a multi-container application, featuring a PostgreSQL database and Adminer:
$ cat docker-compose.yml
services:
database:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD=test
adminer:
image: adminer:latest
restart: always
ports:
- "5000:8080"
depends_on:
- database
We omitted the version attribute from our file because it’s now obsolete.
Our docker-compose.yml file will create a PostgreSQL service named database with the password test. Of course, we only hardcoded the password because this is an illustration; we’d use options like environment variables for real-world projects.
In the configuration, the adminer service depends on the database service, so it’ll be created after and removed before the database service. Also, containers created from the adminer service will have their port 8080 mapped to the host’s port 5000. In other words, adminer containers will be accessible from port 5000 on the host.
Now that we have our docker-compose.yml file, we’ll create our Ansible playbook, playbook.yml:
$ cat playbook.yml
- name: Docker Compose in Ansible
hosts: localhost
tasks:
- name: create and start docker compose services
community.docker.docker_compose_v2:
project_src: .
The playbook above will create a play named Docker Compose in Ansible and run it on localhost. Then, using the community.docker.docker_compose_v2 module, it’ll create and start the services defined in our docker-compose.yml.
The project_src parameter specifies the directory containing our docker-compose.yml. So, since playbook.yml and docker-compose.yml are located at the same level within the same directory, we used the dot character (.) to represent the current directory for project_src.
If our Docker Compose file was located at a different level, we would have specified an absolute path or relative path as needed. Also, if we named it anything besides compose.yml, compose.yaml, docker-compose.yml, and docker-compose.yaml, we would have specified the filename using the files parameter.
For instance, if we named our Docker Compose file d_compose.yml and placed it in a subdirectory named sub, our task would look like this:
$ cat playbook.yml
...truncated...
tasks:
- name: create and deploy docker compose services
community.docker.docker_compose_v2:
project_src: ./sub
files: d_compose.yml
If the file in the example above wasn’t in a subdirectory, our project_src would have remained as the current directory (.).
With our playbook and docker-compose.yml all in place, we’ll run our Ansible playbook to create and deploy the services:
$ ansible-playbook playbook.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match
'all'
PLAY [Docker Compose in Ansible] ***************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]
TASK [create and start docker compose services] ************************************************************************
changed: [localhost]
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Let’s test our adminer service using w3m:
$ w3m http://localhost:5000
Login
System [MySQL ]
Server [db ]
Username [ ]
Password [ ]
Database [ ]
[Login] [ ]Permanent login
Language: [English ] [Use]
Adminer 4.8.1
As seen above, when we try to access adminer from our host’s port 5000, we get the Adminer login page, so the playbook run was successful.
Our playbook only includes a task to create and start the containers, but what if we want to stop and remove them? Well, we can add a task that stops and removes the containers to our playbook:
$ cat playbook.yml
- name: Docker Compose in Ansible
hosts: localhost
tasks:
- name: create and start docker compose services
community.docker.docker_compose_v2:
project_src: .
- name: stop and remove docker compose services
community.docker.docker_compose_v2:
project_src: .
state: absent
In our edited playbook, we now have a task to stop and remove the services. However, if we run this playbook as it is, it’ll create and remove the services in one flow. To prevent this, we can create a separate playbook for each task or configure our playbook to run only specific tasks.
Let’s add tags to each task to make their execution exclusive:
$ cat playbook.yml
- name: Docker Compose in Ansible
hosts: localhost
tasks:
- name: create and start docker compose services
community.docker.docker_compose_v2:
project_src: .
tags: create
- name: stop and remove docker compose services
community.docker.docker_compose_v2:
project_src: .
state: absent
tags: destroy
With the addition of tags, we can now create and start the services by passing create to the ansible-playbook –tags option:
$ ansible-playbook playbook.yml --tags create
...truncated...
TASK [create and start docker compose services] ************************************************************************
ok: [localhost]
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We can also remove the services by passing destroy to the –tags option:
$ ansible-playbook playbook.yml --tags destroy
...truncated...
TASK [stop and remove docker compose services] *************************************************************************
changed: [localhost]
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
With the services destroyed, the adminer service will no longer respond:
$ w3m http://localhost:5000
w3m: Can't load http://localhost:5000.
However, we can readily spin up the same services with the same configuration files using one command, which we’ve done previously.
In place of the community.docker.docker_compose_v2 module, we can run docker compose using Ansible’s built-in command module:
$ cat playbook.yml
- name: Docker Compose in Ansible
hosts: localhost
tasks:
- name: create and start docker compose services
command: docker compose up -d
tags: create
- name: stop and remove docker compose services
command: docker compose down
tags: destroy
Since we intend to run the playbook above in the same directory that holds our docker-compose.yml, we didn’t have to specify a file using the docker compose -f option.
Now, let’s confirm it works by running the create task using the create tag:
$ ansible-playbook playbook.yml --tags create
...truncated...
TASK [create and start docker compose services] ************************************************************************
changed: [localhost]
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Next, we’ll destroy the services:
$ ansible-playbook playbook.yml --tags destroy
...truncated...
TASK [stop and remove docker compose services] *************************************************************************
changed: [localhost]
PLAY RECAP *************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
While we’ll get a similar outcome using the command module, it’s not as idempotent as the community.docker.docker_compose_v2 module. It could also get complex as our playbook grows.
In this article, we discussed how to use Docker Compose in Ansible with the community.docker.docker_compose_v2 module and the built-in command module. Then, we highlighted the fact that using community.docker.docker_compose_v2 is better because it’s more idempotent and offers better readability as our playbook gets complex.