Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

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.

1. Introduction

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.

2. Creating a Docker Compose Configuration File

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.

3. Creating an Ansible Playbook

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 (.).

4. Running Our Ansible Playbook

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.

5. Stopping and Removing the Containers

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.

6. Using the command Module

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.

7. Conclusion

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.