Install docker and docker-compose using Ansible

Srijan Choudhary Srijan Choudhary
- 4 min read

Updated for 2023: I've updated this post with the following changes:

1. Added a top-level sample playbook
2. Used ansible apt_module's cache_time parameter to prevent repeated apt-get updates
3. Install docker-compose-plugin using apt (provides docker compose v2)
4. Make installing docker compose v1 optional
5. Various fixes as suggested in comments
6. Tested against Debian 10,11,12 and Ubuntu 18.04 (bionic), 20.04 (focal), 22.04 (jammy) using Vagrant.

I've published a new post on how I've done this testing.


I wanted a simple, but optimal (and fast) way to install docker and docker-compose using Ansible. I found a few ways online, but I was not satisfied.

My requirements were:

  • Support Debian and Ubuntu
  • Install docker and docker compose v2 using apt repositories
  • Prevent unnecessary apt-get update if it has been run recently (to make it fast)
  • Optionally install docker compose v1 by downloading from github releases
    • But, don’t download if current version >= the minimum version required

I feel trying to achieve these requirements gave me a very good idea of how powerful ansible can be.

The final role and vars files can be seen in this gist. But, I’ll go through each section below to explain what makes this better / faster.

File structure

playbook.yml
roles/
├── docker/
│    ├── defaults/
│    │   ├── main.yml
│    ├── tasks/
│    │   ├── main.yml
│    │   ├── docker_setup.yml
File structure

Playbook

This is the top-level playbook. Any default vars mentioned below can be overridden here.

---
- hosts: all
  vars:
    - docker_compose_install_v1: true
    - docker_compose_version_v1: "1.29.2"
  tasks:
    - name: Docker setup
      block:
        - import_role: name=docker
playbook.yml

Variables

First, we’ve defined some variables in defaults/main.yml. These will control which release channel of docker will be used and whether to install docker compose v1.

---
docker_apt_release_channel: stable
docker_apt_arch: amd64
docker_apt_repository: "deb [arch={{ docker_apt_arch }}] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} {{ docker_apt_release_channel }}"
docker_apt_gpg_key: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
docker_compose_install_v1: false
docker_compose_version_v1: "1.29.2"
roles/docker/defaults/main.yml

Role main.yml

The tasks/main.yml file imports tasks from tasks/docker_setup.yml and turns on become for the whole task.

---
- import_tasks: docker_setup.yml
  become: true
roles/docker/tasks/main.yml

Docker Setup

This task is divided into the following sections:

Install dependencies

- name: Install packages using apt
  apt:
    name: 
        - apt-transport-https
        - ca-certificates
        - curl
        - gnupg2
        - software-properties-common
    state: present
    cache_valid_time: 86400

Here the state: present makes sure that these packages are only installed if not already installed. I've set cache_valid_time to 1 day so that apt-get update is not run if it has already run recently.

Add docker repository

- name: Add Docker GPG apt Key
  apt_key:
    url: "{{ docker_apt_gpg_key }}"
    state: present

- name: Add Docker Repository
  apt_repository:
    repo: "{{ docker_apt_repository }}"
    state: present
    update_cache: true

Here, the state: present and update_cache: true make sure that the cache is only updated if this state was changed. So, apt-get update is not run if the docker repo is already present.

Install and enable docker and docker compose v2

- name: Install docker-ce
  apt:
    name: docker-ce
    state: present
    cache_valid_time: 86400

- name: Run and enable docker
  service:
    name: docker
    state: started
    enabled: true

- name: Install docker compose
  apt:
    name: docker-compose-plugin
    state: present
    cache_valid_time: 86400

Again, due to state: present and cache_valid_time: 86400, there are no extra cache fetches if docker and docker-compose-plugin are already installed.

Docker Compose V1 Setup

WARNING: docker-compose v1 is end-of-life, please keep that in mind and only install/use it if absolutely required.

This task is wrapped in an ansible block that checks if docker_compose_install_v1 is true.

- name: Install docker-compose v1
  when:
    - docker_compose_install_v1 is defined
    - docker_compose_install_v1
  block:

Inside the block, there are two sections:

Check if docker-compose is installed and it’s version

- name: Check current docker-compose version
  command: docker-compose --version
  register: docker_compose_vsn
  changed_when: false
  failed_when: false
  check_mode: no

- set_fact:
    docker_compose_current_version: "{{ docker_compose_vsn.stdout | regex_search('(\\d+(\\.\\d+)+)') }}"
  when:
    - docker_compose_vsn.stdout is defined

The first block saves the output of docker-compose --version into a variable docker_compose_vsn. The failed_when: false ensures that this does not call a failure even if the command fails to execute. (See error handling in ansible).

Sample output when docker-compose is installed: docker-compose version 1.26.0, build d4451659

The second block parses this output and extracts the version number using a regex (see ansible filters). There is a when condition which causes the second block to skip execution if the first block failed (See playbook conditionals).

Install or upgrade docker-compose if required

- name: Install or upgrade docker-compose
  get_url: 
    url : "https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64"
    dest: /usr/local/bin/docker-compose
    mode: 'a+x'
    force: yes
  when: >
    docker_compose_current_version == ""
    or docker_compose_current_version is version(docker_compose_version, '<')

This just downloads the required docker-compose binary and saves it to /usr/local/bin/docker-compose, but it has a conditional that this will only be done if either docker-compose is not already installed, or if the installed version is less than the required version. To do version comparison, it uses ansible’s built-in version comparison function.

So, we used a few ansible features to achieve what we wanted. I’m sure there are a lot of other things we can do to make this even better and more fool-proof. Maybe a post for another day.

Interactions

  • schaerli
    schaerli

    Hey many thank you for this. I have two little changes. Name for set_fact and docker_compose_current_version == "".

    - name: set docker compose current version
    set_fact:
    docker_compose_current_version: "{{ docker_compose_vsn.stdout | regex_search('(\\d+(\\.\\d+)+)') }}"
    when:
    - docker_compose_vsn.stdout is defined
    - name: Install or upgrade docker-compose
    get_url:
    url : "https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-Linux-x86_64"
    dest: /usr/local/bin/docker-compose
    mode: 'a+x'
    force: yes
    when:
    - docker_compose_current_version == "" or docker_compose_current_version is version(docker_compose_version, '<')
    Reply
    • jaydrogers
      jaydrogers

      docker_compose_current_version == ""

      This helped me out a ton (especially for new server provisions that do not have docker-compose installed yet). Thanks @schaerli!

      Reply
  • NurdinDev
    NurdinDev

    Thank you @srijan for your article, I faced an issue during the Install or upgrade docker-compose step
    I got this error Destination /usr/local/bin/docker-compose is not writable after some researches about the issue I tried to add become: true to this step and it's worked for me.
    so is it working with you without become?

    Reply
  • Vladimir
    Vladimir

    Really smart and useful approach to update docker compose!

    Reply
  • scottix
    scottix

    Nice script, I needed to change the get_url section and added the checksum.

    - name: Install or upgrade docker-compose
    become: true
    get_url:
    url: "https://github.com/docker/compose/releases/download/v{{ docker_compose_version }}/docker-compose-linux-x86_64"
    checksum: "sha256:https://github.com/docker/compose/releases/download/v{{ docker_compose_version }}/docker-compose-linux-x86_64.sha256"
    dest: /usr/local/bin/docker-compose
    mode: 'a+x'
    force: yes
    when: >
    docker_compose_current_version is not defined
    or docker_compose_current_version is version(docker_compose_version, '<')
    Reply
  • chamalis
    chamalis

    There are several separate issues with this gist. Failing in clean debian 10.4. Issues found:

    1. -name: Install packages using apt
      fails because of update_cache: no

    2. -name: setup-servers : Install or upgrade docker-compose

    fails because:

    docker_compose_current_version is not defined

    should be:

    - docker_compose_current_version == ""
    1. - name: Run and enable docker

    fails with:

    No module named 'requests'
    fatal: [vm01]: FAILED! => {"changed": false, "msg": "Failed to import the required Python library (Docker SDK for Python: docker>=5.0.0 (Python >= 3.6) or docker<5.0.0 (Python 2.7))

    because python3-requests also needs to be installed

    1. When using docker-compose:
    ModuleNotFoundError: No module named 'docker'
    Reply
    • Srijan Choudhary
      Srijan Choudhary

      @chamalis - thanks. I plan to update the gist and linked blog post with a more complete solution. These days docker compose v2 is available via apt as well, so no need to download from github releases anymore if you don't need v1.

      Reply
    • Srijan Choudhary
      Srijan Choudhary

      @chamalis - Updated, thanks.

      Reply