Ansible vs Docker Compose #

When we start designing infrastructure automation for container-based applications, one architecture question that very often comes up is: “Why should we use Ansible if we can already define our entire container stack in a docker-compose.yml file?” This question is reasonable because both tools have the ability to run and manage container lifecycles.

However, pitting Ansible and Docker Compose against each other as two mutually exclusive alternatives is a less-than-accurate point of view. Both tools are built with very different philosophies, scopes, and architectural focuses. Docker Compose is specifically designed to define and run multi-container applications on one target host. Meanwhile, Ansible is a large-scale configuration management and infrastructure automation tool capable of operating on many target hosts in parallel. Understanding each tool’s strengths, when to choose one over the other, and how to integrate both in a hybrid fashion is a key to building production-grade deployment flows.

flowchart TD
    Start["Deployment Requirements Analysis"] --> Q1{"How many target hosts?"}
    Q1 -- "One Host" --> Q2{"Is it only running containers?"}
    Q1 -- "Many Hosts" --> UseAnsible["Use Ansible Absolutely"]
    Q2 -- "Yes" --> UseCompose["Use Docker Compose Directly"]
    Q2 -- "No (Need OS/Firewall/SSL Setup)" --> UseHybrid["Use the Hybrid Pattern: Ansible + Compose"]
    UseAnsible --> Q3{"Need complex microservices orchestration?"}
    Q3 -- "Yes" --> UseK8s["Consider Kubernetes (K8s)"]
    Q3 -- "No" --> UseAnsibleOnly["Use Ansible Playbooks + Docker Modules"]

Philosophy and Architecture Comparison #

Docker Compose works at the container runtime level on a single machine. It reads a local YAML description file (docker-compose.yml) then translates it into API calls to the local Docker daemon. Its main focus is simplifying dependency relationships between containers (like internal networks and startup order) so developers don’t have to type dozens of long docker run parameters.

Ansible, on the other hand, works on the agentless principle through SSH or WinRM connections. It doesn’t care whether the target is a bare-metal server, virtual machine (VM), cloud instance, or just a local container. Ansible sees Docker containers as just one of many infrastructure components that must be managed — alongside OS kernel configuration, security packages, SSL certificates, firewall rules, user creation, and physical storage mounting.

Here’s a comprehensive comparison table to see the characteristic differences between the two tools:

Analysis CriteriaDocker ComposeAnsible (Docker Modules)
Operational ScopeLimited to one Docker host (single host).Capable of managing hundreds of servers in parallel (multi-host).
Architecture ModelRequires Docker CLI and the Compose plugin installed locally on the target host.Agentless. Only requires Python on the target host and SSH access.
Host OS ManagementNo ability to configure the operating system, firewall, or host OS packages.Very strong at configuring every aspect of the host OS before running Docker.
Conditional LogicLimited to container health status dependencies (depends_on).Very flexible with conditional blocks (when), loops, handlers, and asserts.
Secret ManagementDepends on local .env files or secret files whose secrets are often exposed if not careful.Built-in integration with Ansible Vault for variable-level secret encryption.
Dynamic TemplatingLimited to basic environment variable substitution.Has a full Jinja2 template engine for composing highly complex configuration files.

When to Choose Docker Compose #

Docker Compose is a very expressive and user-friendly tool when we’re in a local development environment or deploying a standalone multi-container application on a single staging server.

Compose excels because of its concise syntax for defining private networks between containers, volume isolation, and container startup order dependencies (depends_on with the service_healthy condition).

Example Production-Grade docker-compose.yml File #

Below is an example Compose file defining a 3-tier web stack (Node.js App, Redis Cache, and PostgreSQL Database) complete with system health verification:

# docker-compose.yml
version: "3.8"

services:
  web-app:
    image: company/node-app:v2.4.0
    container_name: production-web
    restart: unless-stopped
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: "postgresql://app_user:***@db-service:5432/production_db"
      REDIS_URL: "redis://cache-service:6379"
      NODE_ENV: "production"
    depends_on:
      db-service:
        condition: service_healthy
      cache-service:
        condition: service_started
    networks:
      - app-network

  db-service:
    image: postgres:15-alpine
    container_name: production-db
    restart: unless-stopped
    volumes:
      - pg-data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: "production_db"
      POSTGRES_USER: "app_user"
      POSTGRES_PASSWORD: "app_secure_pass"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d production_db"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  cache-service:
    image: redis:7-alpine
    container_name: production-redis
    restart: unless-stopped
    networks:
      - app-network

volumes:
  pg-data:
    driver: local

networks:
  app-network:
    driver: bridge

In local development scenarios, developers just run the single command docker compose up -d to bring up this entire application ecosystem along with the relationships between its components.


When to Choose Ansible for Container Management #

We must switch to Ansible when our deployment scope has exceeded the boundaries of a standalone application. Some examples of real needs that Docker Compose can’t solve include:

  1. Multi-Host Targets: We want to deploy frontend containers on the webservers group servers and backend/database containers on the dbservers group servers. Docker Compose can’t do cross-host coordination.
  2. Non-Docker Infrastructure Dependencies: Before containers run, we must expand LVM disk partitions, install commercial SSL certificates into the /etc/ssl folder, configure iptables or ufw firewall rules, and register backup cron jobs to external storage servers.
  3. Complex Configuration Templating: We need to dynamically generate application configuration files (like nginx.conf or settings.py) based on internal server IPs automatically detected by Ansible at runtime.

Here’s an example Ansible playbook handling all the non-docker infrastructure preparation before finally running Docker containers:

# playbooks/deploy_infrastructure_and_docker.yml
---
- name: Prepare Host Infrastructure and Run Containers
  hosts: app_servers
  become: true
  vars:
    app_port: 8080

  tasks:
    - name: 1. Ensure UFW (Firewall) is installed
      apt:
        name: ufw
        state: present

    - name: 2. Open the application port access in the host firewall
      ufw:
        rule: allow
        port: "{{ app_port }}"
        proto: tcp

    - name: 3. Create the persistent data storage directory
      file:
        path: /var/data/webapp
        state: directory
        owner: www-data
        group: www-data
        mode: '0750'

    - name: 4. Copy the host SSL certificate dynamically
      copy:
        src: "files/ssl/{{ inventory_hostname }}.crt"
        dest: /etc/ssl/certs/app.crt
        owner: root
        group: root
        mode: '0640'

    - name: 5. Run the container using the Ansible module
      community.docker.docker_container:
        name: my-web-app
        image: nginx:alpine
        state: started
        ports:
          - "{{ app_port }}:80"
        volumes:
          - "/var/data/webapp:/usr/share/nginx/html:ro"
          - "/etc/ssl/certs/app.crt:/etc/nginx/ssl/app.crt:ro"

The Best Integration: Ansible Deploying Docker Compose #

In the real production world, we don’t need to force ourselves to choose one tool extremely. The hybrid integration pattern is the best approach adopted by many modern DevOps teams: Use Ansible as the orchestrator and host infrastructure preparer, then use Docker Compose as the application stack definer.

In this pattern, the roles are clearly divided:

  • Ansible: Manages VMs, installs Docker Engine, configures the daemon, prepares directories, copies SSL certificates, generates encrypted .env files from Ansible Vault, copies the docker-compose.yml file to target hosts, and executes the Compose stack.
  • Docker Compose: Determines dependencies between containers, private micro-network configurations, and internal application mapping parameters.

Ansible provides the community.docker.docker_compose_v2 module to execute Compose stacks in a very clean and idempotent way.

Hybrid Playbook Implementation Example #

Here’s a complete hybrid task arrangement deploying a Compose file to target servers:

# playbooks/deploy_hybrid_stack.yml
---
- name: Deploy the Hybrid Application via Docker Compose
  hosts: production_servers
  become: true
  vars:
    project_dir: "/opt/production_app"
    app_db_pass: "{{ vault_db_password }}" # Sensitive variable from Vault

  tasks:
    - name: 1. Ensure the application project directory exists on the host
      file:
        path: "{{ project_dir }}"
        state: directory
        owner: deployer
        group: docker
        mode: '0775'

    - name: 2. Deploy the docker-compose.yml file from a Jinja2 template
      template:
        src: templates/docker-compose.yml.j2
        dest: "{{ project_dir }}/docker-compose.yml"
        owner: deployer
        group: docker
        mode: '0644'

    - name: 3. Deploy the .env environment file securely (using no_log)
      template:
        src: templates/env.j2
        dest: "{{ project_dir }}/.env"
        owner: deployer
        group: docker
        mode: '0600' # Only readable by the owner (deployer)
      no_log: true

    - name: 4. Pull the latest images defined in the Compose file
      community.docker.docker_compose_v2:
        project_src: "{{ project_dir }}"
        state: present
        pull: always # Guarantees we pull the latest images before start
      register: compose_pull_result

    - name: 5. Run the entire Docker Compose stack
      community.docker.docker_compose_v2:
        project_src: "{{ project_dir }}"
        state: present
        recreate: auto # Automatically recreates containers if configuration changes
        build: false # Don't build on the target server, use pre-built images

With this method, developer teams can keep updating their docker-compose.yml file in the application git repository, while operations teams (Ops) just use Ansible to pull those updates and deploy them consistently across the entire production server cluster.


Anti-Patterns and Best Practices for Integration #

For the hybrid integration to run smoothly, there are several bad habits (anti-patterns) we must avoid, along with the best practices (best practices) as implementation guidance:

1. Duplicating Container Parameter Definitions #

  • Anti-pattern: Defining the same ports, volumes, and environment variables inside the docker-compose.yml file and also redundantly rewriting the parameters in Ansible docker_container tasks.
  • Best Practice: If using Compose, let the docker-compose.yml file be the single source of truth regarding container structure. Use Ansible only to deliver that file and trigger its execution.

2. Storing .env Files Containing Secrets in Git #

  • Anti-pattern: Storing a .env file containing plain database passwords directly in the Git repository alongside the docker-compose.yml file.
  • Best Practice: Store passwords in Ansible Vault. Create an env.j2 template in Ansible, then let Ansible generate the .env file on the target server during the deployment process with very strict file access permissions (0600).

Hybrid Integration Project Directory Structure #

When combining Ansible and Docker Compose, arranging a neat and standardized folder layout is very important for maintaining code clarity. Without a clear structure, playbook files, Jinja2 templates, and docker-compose descriptions get mixed together, making collaboration difficult for developer and operations teams.

Here’s the recommended directory structure pattern for production-grade hybrid integration projects:

ansible-compose-project/
  ├── group_vars/
  │   ├── all.yml            # Common variables for all environments
  │   ├── staging.yml        # Staging-specific configuration
  │   └── production.yml     # Production-specific configuration (e.g. domain, IP)
  ├── host_vars/
  │   └── app-node-01.yml    # Per-server-host specific parameters
  ├── roles/
  │   ├── common/            # Role for basic OS, firewall, & user setup
  │   ├── docker/            # Role for Docker Engine installation & configuration
  │   └── app_deploy/        # Dedicated role for deploying the Compose stack
  │       ├── tasks/
  │       │   └── main.yml   # Compose deployment task flow
  │       └── templates/
  │           ├── docker-compose.yml.j2  # Dynamic Compose template
  │           └── env.j2                 # Environment file template (.env)
  ├── playbooks/
  │   └── deploy.yml         # Main playbook calling all roles
  ├── inventory/
  │   ├── staging            # Staging server list
  │   └── production         # Production server list
  └── ansible.cfg            # Global Ansible configuration

In this structure, the app_deploy role only focuses on managing the application lifecycle. The docker-compose.yml.j2 template inside it can read variables from files in group_vars/production.yml dynamically when the playbook runs, allowing the same Compose file to be used in staging or production without manual changes.


Troubleshooting Strategy and Cross-Host Log Monitoring #

One of Docker Compose’s biggest weaknesses is its limited visibility when applications run on many servers. If we deploy an application to 5 different web servers using Docker Compose manually, we must SSH into each of the five servers and type docker compose logs separately to find the error source.

Ansible solves this challenge by enabling itself to act as a simple centralized log aggregation console through ad-hoc command orchestration or troubleshooting playbooks.

Here’s an example Ansible playbook specifically for diagnosing and collecting error logs from all target hosts centrally:

# playbooks/troubleshoot.yml
---
- name: Cross-Host Container Error Log Aggregation
  hosts: production_servers
  become: true
  vars:
    project_dir: "/opt/production_app"
    log_output_dir: "./debug_logs"

  tasks:
    - name: 1. Check the health status of all containers on the target server
      community.docker.docker_compose_v2:
        project_src: "{{ project_dir }}"
        state: present
      register: compose_status

    - name: 2. Collect logs from problematic containers (last 50 lines)
      command: "docker compose -p {{ compose_status.actions[0].project }} logs --tail=50"
      args:
        chdir: "{{ project_dir }}"
      register: container_logs
      changed_when: false

    - name: 3. Create the local debug storage directory on the control node
      file:
        path: "{{ log_output_dir }}"
        state: directory
      delegate_to: localhost
      run_once: true

    - name: 4. Save logs from each server to a local file
      copy:
        content: "{{ container_logs.stdout }}"
        dest: "{{ log_output_dir }}/logs-{{ inventory_hostname }}.txt"
      delegate_to: localhost

When this debugging playbook is executed, Ansible runs in parallel across all hosts, extracts the latest logs from containers, and saves them on our local control machine in the ./debug_logs/ folder separated per server (for example: logs-web-01.txt, logs-web-02.txt). This orchestrated troubleshooting pattern significantly saves incident detection time.


Summary #

  • Docker Compose Focus — Perfect for defining multi-container relationships on a single target server, especially in local environments and isolated staging.
  • Ansible Focus — Excels at multi-host target management, base operating system configuration automation, host security management, and cross-server workflow coordination.
  • Use the Hybrid Approach — Apply the best combination: use Ansible to prepare the host OS and deliver files, then use Docker Compose to orchestrate container lifecycles.
  • Use the docker_compose_v2 Module — Leverage Ansible’s official community.docker.docker_compose_v2 module to execute stack deployments idempotently and structurally on target servers.
  • Secure with Vault — Never let .env environment configuration files containing secrets stay in plaintext on servers. Encrypt secret variables with Ansible Vault and install the no_log: true option on related tasks.

← Previous: Deploy Container Next: Image Build →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact