Docker Compose describes what runs on a server. Ansible describes how the server gets there: Docker installed, project files in place, secrets delivered, and the stack started or updated. Together, they give you repeatable deployments to one or many VPS servers, without logging in by hand.

Basics: Ansible playbooks and Docker Compose in production.

Project layout

deploy/
├── inventory.ini
├── group_vars/
│   └── app/
│       ├── vars.yml
│       └── vault.yml          # encrypted with ansible-vault
├── files/app/compose.yaml
├── templates/app.env.j2
└── deploy.yml
# group_vars/app/vars.yml
app_dir: /opt/app
app_image_tag: "1.8.3"
db_password: "{{ vault_db_password }}"

See Ansible Vault.

Install Docker

ansible-galaxy collection install community.docker
# deploy.yml (part 1)
- hosts: app
  become: true
  tasks:
    - name: Docker prerequisites
      ansible.builtin.apt:
        name: [ca-certificates, curl]
        update_cache: true

    - name: Docker GPG key
      ansible.builtin.get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: "0644"

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

    - name: Docker Engine and Compose plugin
      ansible.builtin.apt:
        name: [docker-ce, docker-ce-cli, containerd.io, docker-compose-plugin]
        update_cache: true

    - name: Docker running
      ansible.builtin.service:
        name: docker
        state: started
        enabled: true

(Use linux/debian in the URLs for Debian. The geerlingguy.docker role does the same, if you prefer a ready-made role. See Ansible roles.)

Deploy the stack

# deploy.yml (part 2)
    - name: App directory
      ansible.builtin.file:
        path: "{{ app_dir }}"
        state: directory
        mode: "0750"

    - name: Compose file
      ansible.builtin.copy:
        src: files/app/compose.yaml
        dest: "{{ app_dir }}/compose.yaml"
        mode: "0640"

    - name: Environment file with secrets
      ansible.builtin.template:
        src: app.env.j2
        dest: "{{ app_dir }}/.env"
        mode: "0600"
      no_log: true

    - name: Start or update the stack
      community.docker.docker_compose_v2:
        project_src: "{{ app_dir }}"
        pull: always          # fetch new images for changed tags
        remove_orphans: true
        state: present
      register: compose

    - name: Prune old images
      community.docker.docker_prune:
        images: true
      when: compose.changed
{# templates/app.env.j2 #}
APP_IMAGE_TAG={{ app_image_tag }}
DB_PASSWORD={{ db_password }}

In compose.yaml, reference the tag: image: registry.example.com/myapp:${APP_IMAGE_TAG}. See Docker image tags best practices.

Deploy a new version

ansible-playbook -i inventory.ini deploy.yml -e app_image_tag=1.8.4 --ask-vault-pass

Ansible updates .env, and the Compose module recreates only the containers whose image or config changed.

Several servers behind a load balancer: add serial: 1 to the play, so servers update one at a time, and a health check task between them avoids taking everything down at once:

- hosts: app
  serial: 1
  tasks:
    # … deploy tasks …
    - name: Wait for health
      ansible.builtin.uri:
        url: "http://127.0.0.1:8080/health"
      register: h
      until: h.status == 200
      retries: 20
      delay: 3

Run it from CI

A CI job (GitHub Actions, GitLab CI, Gitea Actions) can build the image, push it to a registry, then run the playbook with the new tag. Store the SSH key and vault password as CI secrets. See GitHub Actions deploy to VPS and GitHub Actions secrets.

Tips

  • Keep port publishing private (127.0.0.1:port), and run a reverse proxy role. See Docker and UFW.
  • Back up volumes before major upgrades (an Ansible task can trigger the backup script). See Docker volume backups.
  • Use --check --diff to preview config changes, and --limit web1 to deploy to one server first.

Quick reference

Task Module / command
Install Docker apt repository + docker-ce, docker-compose-plugin
Copy files copy, template (mode: 0600 for .env)
Run stack community.docker.docker_compose_v2
New version -e app_image_tag=…
Rolling serial: 1 + health check

Related: Ansible server hardening and Docker VPS hosting.

  • Ansible
  • Docker
  • DevOps