How to Use Ansible to Install and Set Up Docker on Ubuntu

Server automation has become an important part of systems administration because modern application environments are often designed to be replaceable. Configuration management tools such as Ansible help automate server configuration by defining repeatable procedures for new systems while reducing the risk of errors that can occur during manual setup.

Ansible uses a straightforward architecture that does not require dedicated software to be installed on managed nodes. It also includes a broad collection of built-in modules and features that make it easier to create automation workflows.

This guide demonstrates how to use Ansible to automate the process of installing and configuring Docker on Ubuntu. Docker simplifies the management of containers, which are isolated processes that function similarly to virtual machines while generally being more portable, using fewer resources, and relying more directly on the host operating system.

Note: This guide has been tested with Ubuntu 24.04 LTS. It may also work with other recent Ubuntu versions, although certain steps, including repository configuration, can require minor changes.

Key Takeaways

  • Idempotency makes it possible to run the same playbook repeatedly without creating unintended changes because every task evaluates the current system state before making modifications.
  • Modern GPG key handling stores repository keys in /etc/apt/keyrings and uses signed-by parameters instead of the deprecated apt-key system command and the older Ansible apt_key module approach.
  • Docker Compose v2 is provided as the docker-compose-plugin package and uses the docker compose subcommand rather than the older docker-compose executable.
  • The {{ ansible_distribution_release }} variable automatically determines Ubuntu release codenames such as focal, jammy, and noble, allowing the playbook to work across versions without hardcoded release values.
  • The community.docker collection must be installed separately on the Ansible control node with ansible-galaxy collection install community.docker before containers can be managed.
  • A complete Docker Engine installation uses five main packages: docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin, and docker-compose-plugin.
  • After modifying Docker group membership with usermod -aG docker, you must log out and sign in again because group permissions are established when the session begins.
  • The order of tasks matters because Ansible normally runs tasks synchronously. Dependencies such as gnupg therefore need to be installed before operations involving GPG are executed.
  • Verbose mode with -vvv exposes the commands that are executed and complete module responses, making it useful for diagnosing network, permission, and execution problems.
  • Ansible playbooks provide greater repeatability than conventional shell scripts by combining built-in idempotency, readable configuration, and straightforward version-control integration.

Prerequisites

To run the automated configuration provided by the playbook in this guide, you will need:

  • One Ansible control node: an Ubuntu 24.04 system with Ansible installed and configured to connect to managed Ansible hosts through SSH keys. The control node should have a regular user with sudo privileges and an enabled firewall. Ansible must also be installed and configured before continuing.
  • One or more Ansible hosts: one or more remote Ubuntu 24.04 servers that have already received the required initial server configuration.

Note: Before continuing, confirm that the Ansible control node can connect to the managed host or hosts and execute commands successfully. Perform an Ansible connection test before running the playbook.

What Does This Ansible Playbook Do?

This Ansible playbook provides an automated alternative to manually completing the Docker installation and configuration procedure on Ubuntu. After the playbook has been prepared once, it can be reused for later installations.

Running the playbook performs the following actions on the managed Ansible hosts:

  1. Install aptitude, which Ansible can use as an alternative interface to the apt package manager.
  2. Install the necessary system packages.
  3. Download and configure the Docker GPG signing key using the modern keyring method.
  4. Add the official Docker repository to the apt package sources.
  5. Install Docker.
  6. Install Docker Compose as the v2 plugin.
  7. Install the Python Docker module through pip.
  8. Pull the default image defined by default_container_image from Docker Hub.
  9. Create the number of containers configured with container_count, use the image stored in default_container_image, and execute the command specified by default_container_command inside every newly created container.

After the playbook completes, the configured number of containers will have been created according to the values defined in the playbook variables.

To start, sign in to the Ansible control node using an account with sudo privileges.

Step 1 — Prepare the Ansible Playbook

The playbook.yml file contains all of the tasks used by the automation. A task represents the smallest individual action that can be automated with an Ansible playbook. Begin by creating the playbook file with your preferred text editor:

This opens an empty YAML file. Before adding the individual tasks, insert the following configuration:

playbook.yml

---
- hosts: all
  become: true
  vars:
    container_count: 4
    default_container_name: docker
    default_container_image: ubuntu
    default_container_command: sleep 1d

Most Ansible playbooks begin with declarations similar to these. The hosts setting defines which servers the Ansible control node will target when the playbook runs. The become setting determines whether commands are executed with elevated root privileges.

The vars section stores reusable values as variables. When these values need to change later, you only need to modify the corresponding definitions in one location. The variables have the following purposes:

  • container_count: Specifies how many containers should be created.
  • default_container_name: Defines the default name used for containers.
  • default_container_image: Defines the Docker image used when containers are created.
  • default_container_command: Specifies the default command executed in newly created containers.

Note: If you want to review the final version of the complete playbook, continue to Step 5. YAML depends heavily on correct indentation, so check the structure carefully after adding all tasks.

Step 2 — Add Package Installation Tasks to the Playbook

Ansible executes tasks synchronously from the top of a playbook toward the bottom by default. The order of tasks is therefore important, and one task can generally be expected to finish before the following task starts.

Each task used in this playbook can also function independently and can be reused in other Ansible playbooks.

A fundamental Ansible concept is idempotency. This means repeatedly running the same playbook produces the same intended system state without introducing unnecessary or unintended modifications.

Every task in this playbook is designed to behave idempotently. Package installation tasks, for example, use state: latest or state: present, so packages are installed or updated only when required. Repository and signing-key tasks likewise modify the system only when the expected configuration is not already present.

As a result, the playbook can safely be run again when provisioning additional servers or applying updates to existing systems.

Begin by adding tasks that install aptitude, which provides an interface to the Linux package manager, together with the required system packages. Ansible will make sure these packages remain installed:

playbook.yml

  tasks:
    - name: Install aptitude
      apt:
        name: aptitude
        state: latest
        update_cache: true

    - name: Install required system packages
      apt:
        pkg:
          - ca-certificates
          - curl
          - gnupg
          - software-properties-common
          - python3-pip
          - python3-venv
          - python3-setuptools
        state: latest
        update_cache: true

These tasks use Ansible’s built-in apt module to install the required packages. Ansible modules provide predefined ways to perform operations that would otherwise require manually written shell commands. If aptitude is unavailable, Ansible can fall back to apt for package installation, although aptitude has historically been preferred by Ansible.

Packages can be added or removed according to your requirements. This configuration ensures that the specified packages are installed at their latest available versions after the apt package cache has been updated.

Step 3 — Add Docker Installation Tasks to the Playbook

The following tasks install the latest Docker version from the official repository. The Docker GPG key verifies downloaded packages, the official repository is configured as a package source, and Docker is then installed. The Docker Compose plugin and Docker’s Python module are installed as part of the same configuration.

playbook.yml

    - name: Create keyrings directory
      file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'

    - name: Download Docker GPG key
      get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: '0644'
        force: true

    - name: De-armor Docker GPG key for APT
      command: gpg --dearmor --yes --output /etc/apt/keyrings/docker.gpg /etc/apt/keyrings/docker.asc
      args:
        creates: /etc/apt/keyrings/docker.gpg

    - name: Add Docker repository
      apt_repository:
        repo: "deb [signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
        state: present
        filename: docker

    - name: Update apt and install Docker Engine
      apt:
        pkg:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
        state: latest
        update_cache: true

    - name: Install Docker Module for Python
      pip:
        name: docker

This configuration uses an APT keyring together with the repository’s signed-by option. On modern Ubuntu releases, this approach replaces the deprecated apt_key method.

Docker Compose is installed through the docker-compose-plugin package. This package provides the current docker compose command syntax and replaces the older standalone docker-compose executable.

Note: Docker Compose v2 uses docker compose, with a space between the commands, instead of the older docker-compose syntax.

Step 4 — Add Docker Image and Container Tasks to the Playbook

The creation of Docker containers begins by pulling the Docker image that should be used. By default, these images are available through the official Docker Hub. Containers are then created from that image according to the variables defined near the beginning of the playbook.

Before adding these tasks, make sure the community.docker Ansible collection is installed on the control node. Install the collection with:

ansible-galaxy collection install community.docker

Next, add these tasks to the playbook:

playbook.yml

    - name: Pull default Docker image
      community.docker.docker_image:
        name: "{{ default_container_image }}"
        source: pull

    - name: Create default containers
      community.docker.docker_container:
        name: "{{ default_container_name }}{{ item }}"
        image: "{{ default_container_image }}"
        command: "{{ default_container_command }}"
        state: present
      with_sequence: count={{ container_count }}

The community.docker.docker_image module pulls the Docker image that will serve as the base for the containers. The community.docker.docker_container module defines the container configuration and the command that should be passed to each container.

The with_sequence directive creates an Ansible loop. In this example, it repeats the container-creation task according to the value configured in container_count. The item variable contains the number for the current loop iteration, and that number becomes part of each container’s name.

Step 5 — Review the Complete Ansible Playbook

The finished playbook should resemble the following example, with small differences depending on any custom changes you make:

playbook.yml

---
- hosts: all
  become: true
  vars:
    container_count: 4
    default_container_name: docker
    default_container_image: ubuntu
    default_container_command: sleep 1d

  tasks:
    - name: Install aptitude
      apt:
        name: aptitude
        state: latest
        update_cache: true

    - name: Install required system packages
      apt:
        pkg:
          - ca-certificates
          - curl
          - gnupg
          - software-properties-common
          - python3-pip
          - python3-venv
          - python3-setuptools
        state: latest
        update_cache: true

    - name: Create keyrings directory
      file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'

    - name: Download Docker GPG key
      get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: '0644'
        force: true

    - name: De-armor Docker GPG key for APT
      command: gpg --dearmor --yes --output /etc/apt/keyrings/docker.gpg /etc/apt/keyrings/docker.asc
      args:
        creates: /etc/apt/keyrings/docker.gpg

    - name: Add Docker repository
      apt_repository:
        repo: "deb [signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
        state: present
        filename: docker

    - name: Update apt and install Docker Engine
      apt:
        pkg:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
        state: latest
        update_cache: true

    - name: Install Docker Module for Python
      pip:
        name: docker

    - name: Pull default Docker image
      community.docker.docker_image:
        name: "{{ default_container_image }}"
        source: pull

    - name: Create default containers
      community.docker.docker_container:
        name: "{{ default_container_name }}{{ item }}"
        image: "{{ default_container_image }}"
        command: "{{ default_container_command }}"
        state: present
      with_sequence: count={{ container_count }}

You can modify the playbook to match the requirements of your own workflow. For example, the docker_image module can push images to Docker Hub, while the docker_container module can be used to configure container networks.

Note: Pay close attention to indentation. Incorrect indentation is a common reason for YAML errors. The example uses two spaces for indentation.

After you are satisfied with the playbook, save the file and close the text editor.

Step 6 — Run the Ansible Playbook

The playbook is now ready to run on one or more servers. Playbooks are commonly configured to target all applicable servers from the inventory, but an individual server can also be selected explicitly.

To run the playbook only against host1 while connecting with the user user, use:

ansible-playbook playbook.yml -l host1 -u user

The -l option identifies the target server, while -u specifies the account Ansible should use to connect to the remote system. The resulting output should be similar to this:

Output. . .
changed: [host1]

TASK [Create default containers] *****************************************************************************************************************
changed: [host1] => (item=1)
changed: [host1] => (item=2)
changed: [host1] => (item=3)
changed: [host1] => (item=4)

PLAY RECAP ***************************************************************************************************************************************
host1                    : ok=10    changed=8    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Note: An Ansible command reference or cheat sheet can provide additional information about the available options for running playbooks.

This output indicates that the server configuration has completed. Your exact results can differ, but the important point is that the play recap reports zero failures.

After the playbook finishes, connect to the server through SSH and verify that the Docker containers were created successfully.

Connect to the remote server with:

ssh user@your_remote_server_ip

Then list the Docker containers on the remote server:

The output should look similar to the following:

Output
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a3fe9bfb89cf        ubuntu              "sleep 1d"          5 minutes ago       Created                                 docker4
8799c16cde1e        ubuntu              "sleep 1d"          5 minutes ago       Created                                 docker3
ad0c2123b183        ubuntu              "sleep 1d"          5 minutes ago       Created                                 docker2
b9350916ffd8        ubuntu              "sleep 1d"          5 minutes ago       Created                                 docker1

This confirms that the containers defined by the playbook were created. Because container creation is the final task, successful containers also indicate that the complete playbook ran successfully on the server.

Troubleshooting Common Ansible and Docker Errors

Although this playbook is intended to run reliably on a correctly configured system, problems can occasionally occur because of network settings, existing package conditions, or permissions. The following sections describe common issues and the steps that can be used to investigate them.

Docker GPG Key Errors

Docker GPG key errors generally appear when the playbook is unable to download or process the repository signing key. Possible causes include network connectivity problems, DNS failures, or file permission restrictions involving the /etc/apt/keyrings directory.

If the Ansible output reports a GPG key problem, first confirm that the Docker GPG key URL can be reached from the managed node. Test the download manually with curl:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg

If the key downloads successfully, manually refresh the APT cache to determine whether a remaining repository configuration problem exists:

If the problem continues, verify that the server has working network connectivity and DNS resolution. DNS can be tested with nslookup download.docker.com, while general connectivity can be checked with ping 8.8.8.8.

Docker Repository Issues

Repository configuration problems commonly appear as “package not found” errors during Docker installation. One common reason is a mismatch between the configured repository URL and the Ubuntu release codename. This playbook reduces that risk by using ansible_distribution_release to determine the codename automatically.

To investigate repository problems, examine the configured APT sources and confirm that the Docker repository is present:

grep -r docker /etc/apt/sources.list*

The repository entry should contain [signed-by=/etc/apt/keyrings/docker.gpg] and the appropriate Ubuntu codename, such as focal for 20.04, jammy for 22.04, or noble for 24.04. If the repository appears correct but packages are still unavailable, run sudo apt update to refresh the package index and then execute the playbook again.

Package Installation Failures

Package installation may fail because of existing package conflicts, interrupted installations, or a locked package database. When Ansible reports a package installation failure, the underlying cause is often related to the current APT state rather than the playbook itself.

Start troubleshooting by confirming that the APT cache is current. An outdated package cache can reference package versions that are no longer available. Next, check whether broken dependencies are preventing additional packages from being installed:

sudo apt --fix-broken install

This command attempts to repair incomplete or damaged package installations. Also verify that another package-management process is not running. APT processes such as automatic updates can lock the package database and prevent Ansible from changing it. Active package processes can be checked with ps aux | grep apt.

Permission Denied When Running Docker Commands

Docker normally requires root privileges for communication with the Docker daemon. Running Docker commands from a regular account without sudo can therefore result in “permission denied” errors. The playbook installs Docker but does not automatically add the current user to the docker group.

To execute Docker commands without sudo, add the current account to the docker group:

sudo usermod -aG docker $USER

This command adds the current account to the docker group and grants permission to communicate with the Docker daemon. Group membership changes are not applied to the existing shell session, so you must log out and sign in again before the new permissions are recognized. Another option is to start a new shell with the updated group membership by running newgrp docker.

Remember that membership in the docker group effectively provides root-level system access because containers can be started with complete filesystem access. Only trusted users should be added to this group.

Docker Service Does Not Start

If Docker does not start automatically after installation, possible causes include systemd configuration problems, conflicting services, or insufficient system resources. Docker runs as a systemd service, so standard systemd diagnostic commands can be used to investigate the issue.

First, inspect the current Docker service status for obvious error messages:

sudo systemctl status docker

The result shows whether Docker is active, failed, or stopped and includes recent log messages. For more detailed diagnostics, inspect the Docker service logs with journalctl:

Add --no-pager to review the complete log without a pager, or use -n 50 to display only the latest 50 lines. Typical causes include port conflicts when another service is already using a required port or problems with the storage driver.

After correcting a configuration problem identified in the logs, restart Docker:

sudo systemctl restart docker

Docker can also be configured to start automatically during system boot with sudo systemctl enable docker.

Python Module or Dependency Issues

The Docker Python module, installed through the docker package, is required by Ansible’s community.docker collection to communicate with the Docker API. Installation problems may be caused by pip configuration errors, incompatible Python versions, or missing build dependencies.

If the playbook fails at the “Install Docker Module for Python” task, first confirm that pip is available and functioning. Check the installed Python 3 version:

The Docker Python module requires Python 3.6 or newer. If the Python version is suitable but installation still fails, install the module manually to obtain more detailed error information:

If compilation errors occur, development headers or build tools may be missing. Installing python3-dev commonly resolves this type of problem. When using a Python virtual environment, also confirm that the Docker module is being installed into the correct environment.

Ansible Connection Failures

Ansible connection failures occur when the control node cannot establish an SSH connection with one or more managed hosts. SSH connectivity is a basic requirement for Ansible, so these problems need to be resolved before any playbook tasks can run.

Begin by confirming that you can connect manually to the target host through SSH using the same credentials Ansible is expected to use:

If manual SSH access succeeds while Ansible still cannot connect, inspect the Ansible inventory configuration and SSH key authentication. Verify that all hostnames or IP addresses are correct and confirm that custom SSH ports or other connection parameters have been configured correctly.

Use ssh-add -l to verify that your SSH key is loaded into the SSH agent. If the key is missing, add it with ssh-add ~/.ssh/id_rsa, or substitute the path to the appropriate private key. Also verify that the corresponding public key exists in ~/.ssh/authorized_keys on the managed host.

Ansible Playbook Execution Failures

When an Ansible task fails, playbook execution stops and Ansible reports which task failed and what caused the failure. These messages are the primary source of information for diagnosing execution problems. They generally identify the task, the affected module, and additional details explaining the error.

Start by reviewing the Ansible error output and identifying the failed task. Error messages frequently provide clues such as insufficient permissions, unreachable resources, or playbook syntax errors.

If the normal output does not contain enough information, execute the playbook in verbose mode to inspect Ansible’s actions in greater detail:

ansible-playbook playbook.yml -vvv

The -vvv option enables extensive verbosity and displays the commands Ansible executes, complete task output, and detailed diagnostic information. This is particularly helpful when investigating networking problems, permission errors, or unexpected behavior from an Ansible module.

Before running the playbook again, confirm that every required resource is accessible. The Docker GPG key URL must be reachable, managed hosts need internet connectivity for downloading packages, and sufficient disk space must be available for installation.

Because the playbook is idempotent, it can safely be executed again after a problem has been corrected. Tasks that already completed successfully will normally appear as ok rather than changed, while Ansible continues with tasks that still need work.

Frequently Asked Questions About Ansible and Docker

1. What Version of Ansible Is Required for This Playbook?

Any recent Ansible release should generally work, although Ansible 2.10 or newer provides a useful baseline. Ansible 2.10 and later use the collections structure by default, which is relevant because this playbook relies on the community.docker collection for the docker_image and docker_container tasks. The installed version can be checked on the control node with ansible --version.

2. Does the Playbook Install Docker Compose, and Which Version Is Used?

Yes. The playbook installs the docker-compose-plugin package, which supplies Docker Compose v2. Compose v2 is executed as a Docker subcommand with docker compose, using a space, rather than through the older standalone docker-compose executable.

3. Why Use Ansible to Install Docker Instead of a Shell Script or Cloud-Init?

Ansible makes server configuration repeatable and easier to audit. Because the tasks are idempotent, the same playbook can be run repeatedly to move systems toward the expected configuration, such as ensuring that the Docker repository exists and the necessary packages are installed, without having to create custom checks for resources that are already configured. Playbooks are also easier to review, store in version control, and extend with additional provisioning tasks.

4. What Does Idempotent Mean for This Ansible Playbook?

Idempotency means that the same playbook can be executed repeatedly while producing predictable results. Each task evaluates the current system state and makes a change only when necessary. If Docker is already installed, the repository already exists, and all packages are current, Ansible reports the relevant tasks as ok rather than installing everything again.

5. Can This Playbook Provision Multiple Ubuntu Servers at the Same Time?

Yes. Add all of the target hosts to the Ansible inventory, for example within the same host group, and execute the playbook against that group. Ansible can run tasks across multiple hosts in parallel, while the forks option in the Ansible configuration controls the amount of parallel execution.

6. Should I Use This Custom Playbook or the geerlingguy.docker Ansible Galaxy Role?

The appropriate choice depends on your requirements. A custom playbook keeps the complete configuration in one location and makes every change being applied easy to inspect, which can be useful for internal reviews or compliance. A commonly used Galaxy role such as geerlingguy.docker can reduce setup work, but it introduces an external dependency that should be reviewed and kept current.

7. How Can I Verify That Docker Was Installed Correctly?

Connect to the managed node with SSH and verify that Docker responds correctly:

  • Check the Docker Engine version with docker --version.
  • Confirm that the Docker daemon is active with sudo systemctl status docker.
  • Run a test container with sudo docker run hello-world.

If the account has been added to the docker group, Docker commands can be run without sudo after logging out and signing in again. Docker Compose v2 can be verified with docker compose version.

Conclusion

Automating infrastructure configuration saves time while helping ensure that servers follow a consistent configuration that can still be adapted to individual requirements. Because modern applications are distributed across different systems and need consistent development, staging, and production environments, infrastructure automation has become an important part of many development workflows.

This guide demonstrated how Ansible can automate the installation and configuration of Docker on a remote Ubuntu 24.04 server, including support for the current Docker Compose plugin. Container requirements differ between environments, so the official Ansible documentation provides additional information and examples for the docker_container module.

Additional tasks can also be included in the playbook when more customization is required during the initial server configuration. General introductory material about writing Ansible playbooks can provide further guidance for expanding the automation.

Source: digitalocean.com

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in: