Introduction

community.docker.docker_compose_v2 is the Ansible module for deploying and managing Docker Compose stacks — it wraps the docker compose CLI to bring up, update, and remove multi-container applications idempotently. This article covers the complete workflow from basic deployment to production patterns with health checks and rolling updates.

Prerequisites

# Install the collection
ansible-galaxy collection install community.docker

# Target hosts need:
# - Docker Engine 24+
# - Docker Compose V2 (docker compose, not docker-compose)

Basic Stack Deployment

---
- name: Deploy application stack
  hosts: appservers
  become: true
  tasks:
    - name: Create project directory
      ansible.builtin.file:
        path: /opt/myapp
        state: directory
        mode: '0755'

    - name: Deploy docker-compose.yml
      ansible.builtin.copy:
        dest: /opt/myapp/docker-compose.yml
        content: |
          services:
            web:
              image: nginx:1.27
              ports:
                - "80:80"
              volumes:
                - ./html:/usr/share/nginx/html:ro
              depends_on:
                - api
              restart: unless-stopped

            api:
              image: myregistry.example.com/myapi:latest
              environment:
                DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/myapp
                REDIS_URL: redis://cache:6379
              depends_on:
                - db
                - cache
              restart: unless-stopped

            db:
              image: postgres:16
              volumes:
                - pgdata:/var/lib/postgresql/data
              environment:
                POSTGRES_DB: myapp
                POSTGRES_USER: app
                POSTGRES_PASSWORD: ${DB_PASSWORD}
              restart: unless-stopped

            cache:
              image: redis:7-alpine
              restart: unless-stopped

          volumes:
            pgdata:

    - name: Deploy .env file
      ansible.builtin.copy:
        dest: /opt/myapp/.env
        content: |
          DB_PASSWORD={{ vault_db_password }}
        mode: '0600'
      no_log: true

    - name: Start stack
      community.docker.docker_compose_v2:
        project_src: /opt/myapp
        state: present
      register: output

    - name: Show running services
      ansible.builtin.debug:
        msg: "{{ output.containers | map(attribute='Name') | list }}"

Module Reference

community.docker.docker_compose_v2

ParameterDefaultDescription
project_src—Path to directory containing docker-compose.yml
project_namedirectory nameOverride project name
files—List of compose files (override default)
statepresentpresent, absent, stopped, restarted
pullpolicyalways, missing, never
buildpolicyalways, never
remove_orphansfalseRemove containers for undefined services
recreateautoalways, never, auto
services—Specific services to manage
profiles—Compose profiles to activate
env_files—Additional .env files
timeout—Shutdown timeout in seconds

Common Operations

Pull and Recreate (Update)

- name: Update application
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    state: present
    pull: always
    recreate: always
    services:
      - api
      - web

Stop Stack

- name: Stop all services
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    state: stopped

Remove Stack

- name: Remove stack and volumes
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    state: absent
    remove_orphans: true

Restart Specific Service

- name: Restart API service
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    state: restarted
    services:
      - api

Template-Based Compose Files

---
- name: Deploy with templated compose
  hosts: appservers
  become: true
  vars:
    app_version: "1.5.0"
    app_replicas: 3
    db_version: "16"
    domain: app.example.com
  tasks:
    - name: Deploy compose file from template
      ansible.builtin.template:
        src: docker-compose.yml.j2
        dest: /opt/myapp/docker-compose.yml
        mode: '0644'
      register: compose_file

    - name: Apply changes
      community.docker.docker_compose_v2:
        project_src: /opt/myapp
        state: present
        pull: always
        recreate: "{{ 'always' if compose_file.changed else 'auto' }}"
# templates/docker-compose.yml.j2
services:
  web:
    image: myregistry.example.com/web:{{ app_version }}
    ports:
      - "443:443"
    environment:
      DOMAIN: {{ domain }}
    deploy:
      replicas: {{ app_replicas }}
    restart: unless-stopped

  db:
    image: postgres:{{ db_version }}
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: {{ vault_db_password }}
    restart: unless-stopped

volumes:
  pgdata:

Health Check Verification

- name: Deploy and verify health
  hosts: appservers
  become: true
  tasks:
    - name: Start stack
      community.docker.docker_compose_v2:
        project_src: /opt/myapp
        state: present
        pull: always

    - name: Wait for API health check
      ansible.builtin.uri:
        url: "http://localhost:8080/health"
        status_code: 200
      register: health
      until: health.status == 200
      retries: 30
      delay: 5

    - name: Verify all containers healthy
      community.docker.docker_container_info:
        name: "myapp-{{ item }}-1"
      register: container_info
      loop:
        - web
        - api
        - db
      failed_when: container_info.container.State.Status != 'running'

Rolling Updates Across Hosts

---
- name: Rolling update of Docker Compose stack
  hosts: appservers
  become: true
  serial: 1                        # One host at a time
  max_fail_percentage: 0           # Stop on any failure
  tasks:
    - name: Pull new images
      community.docker.docker_compose_v2:
        project_src: /opt/myapp
        state: present
        pull: always
        recreate: always
        services: [api, web]

    - name: Wait for health check
      ansible.builtin.uri:
        url: "http://localhost:8080/health"
        status_code: 200
      register: health
      until: health.status == 200
      retries: 30
      delay: 5

    - name: Verify service responding
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}/api/version"
        return_content: true
      register: version

    - name: Display deployed version
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }}: {{ version.json.version }}"

Manage Docker Networks

- name: Create shared network
  community.docker.docker_network:
    name: app-network
    driver: bridge
    ipam_config:
      - subnet: 172.28.0.0/16
        gateway: 172.28.0.1

# Reference in compose
# networks:
#   default:
#     external: true
#     name: app-network

Backup Database Before Update

- name: Safe update with backup
  hosts: appservers
  become: true
  tasks:
    - name: Backup database
      community.docker.docker_container_exec:
        container: myapp-db-1
        command: pg_dump -U app myapp
      register: db_dump

    - name: Save backup
      ansible.builtin.copy:
        content: "{{ db_dump.stdout }}"
        dest: "/backups/myapp-{{ ansible_date_time.iso8601_basic_short }}.sql"

    - name: Update stack
      community.docker.docker_compose_v2:
        project_src: /opt/myapp
        state: present
        pull: always
        recreate: always

Troubleshooting

"docker compose" Not Found

# Ensure Docker Compose V2 plugin is installed
- name: Install Docker Compose V2
  ansible.builtin.apt:
    name: docker-compose-plugin
    state: present

Permission Denied

# Add ansible user to docker group
- name: Add user to docker group
  ansible.builtin.user:
    name: ansible
    groups: docker
    append: true

Container Won't Start

# Check logs
- name: Get container logs
  community.docker.docker_container_info:
    name: myapp-api-1
  register: info

- name: Show logs
  ansible.builtin.command: docker compose -f /opt/myapp/docker-compose.yml logs api --tail=50
  register: logs

- ansible.builtin.debug:
    var: logs.stdout_lines

Best Practices

  1. Template compose files — use Jinja2 for environment-specific config
  2. Pin image tags — never use :latest in production
  3. Use .env files for secrets — deploy with mode: 0600 and no_log: true
  4. Health checks in compose — let Docker monitor container health
  5. Rolling updates with serial: 1 — update one host at a time
  6. Backup before update — especially databases
  7. remove_orphans: true — clean up old services after refactoring
  8. Separate data volumes — never store data in containers

Conclusion

The community.docker.docker_compose_v2 module manages Docker Compose stacks declaratively through Ansible — deploy, update, restart, and remove multi-container applications. Use templated compose files for environment-specific configuration, rolling updates with serial: 1 for zero-downtime deployments, and health check verification to confirm each host is serving before moving to the next.