Files
gh-poindexter12-waypoint-te…/skills/ansible/references/docker/compose-patterns.md
2025-11-30 08:47:38 +08:00

6.2 KiB

Ansible Docker Compose Patterns

Common patterns for managing Docker Compose stacks with Ansible.

Project Structure

roles/
└── docker_app/
    ├── tasks/
    │   └── main.yml
    ├── templates/
    │   ├── docker-compose.yml.j2
    │   └── .env.j2
    ├── defaults/
    │   └── main.yml
    └── handlers/
        └── main.yml

Role Template

defaults/main.yml

app_name: myapp
app_version: latest
app_port: 8080
app_data_dir: "/opt/{{ app_name }}"

# Compose settings
compose_pull: always
compose_recreate: auto  # auto, always, never

# Resource limits
app_memory_limit: 512M
app_cpu_limit: 1.0

templates/docker-compose.yml.j2

name: {{ app_name }}

services:
  app:
    image: {{ app_image }}:{{ app_version }}
    container_name: {{ app_name }}
    restart: unless-stopped
    ports:
      - "{{ app_port }}:{{ app_internal_port | default(app_port) }}"
    volumes:
      - {{ app_data_dir }}/data:/app/data
{% if app_config_file is defined %}
      - {{ app_data_dir }}/config:/app/config:ro
{% endif %}
    environment:
      TZ: {{ timezone | default('UTC') }}
{% for key, value in app_env.items() %}
      {{ key }}: "{{ value }}"
{% endfor %}
{% if app_memory_limit is defined or app_cpu_limit is defined %}
    deploy:
      resources:
        limits:
{% if app_memory_limit is defined %}
          memory: {{ app_memory_limit }}
{% endif %}
{% if app_cpu_limit is defined %}
          cpus: '{{ app_cpu_limit }}'
{% endif %}
{% endif %}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:{{ app_internal_port | default(app_port) }}/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - {{ app_network | default('default') }}

{% if app_network is defined %}
networks:
  {{ app_network }}:
    external: true
{% endif %}

tasks/main.yml

---
- name: Create application directory
  ansible.builtin.file:
    path: "{{ app_data_dir }}"
    state: directory
    owner: "{{ ansible_user }}"
    group: "{{ ansible_user }}"
    mode: '0755'

- name: Create data directories
  ansible.builtin.file:
    path: "{{ app_data_dir }}/{{ item }}"
    state: directory
    owner: "{{ ansible_user }}"
    mode: '0755'
  loop:
    - data
    - config

- name: Deploy compose file
  ansible.builtin.template:
    src: docker-compose.yml.j2
    dest: "{{ app_data_dir }}/docker-compose.yml"
    owner: "{{ ansible_user }}"
    mode: '0644'
  notify: Redeploy stack

- name: Deploy environment file
  ansible.builtin.template:
    src: .env.j2
    dest: "{{ app_data_dir }}/.env"
    owner: "{{ ansible_user }}"
    mode: '0600'
  notify: Redeploy stack
  when: app_secrets is defined

- name: Ensure stack is running
  community.docker.docker_compose_v2:
    project_src: "{{ app_data_dir }}"
    state: present
    pull: "{{ compose_pull }}"
    recreate: "{{ compose_recreate }}"
  register: compose_result

- name: Show deployment result
  ansible.builtin.debug:
    msg: "Deployed {{ compose_result.containers | length }} containers"
  when: compose_result is changed

handlers/main.yml

---
- name: Redeploy stack
  community.docker.docker_compose_v2:
    project_src: "{{ app_data_dir }}"
    state: present
    pull: always
    recreate: always

Multi-Service Stack

templates/docker-compose.yml.j2 (full stack)

name: {{ stack_name }}

services:
  app:
    image: {{ app_image }}:{{ app_version }}
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      DATABASE_URL: "postgres://{{ db_user }}:{{ db_password }}@db:5432/{{ db_name }}"
      REDIS_URL: "redis://redis:6379"
    networks:
      - internal
      - web

  db:
    image: postgres:15
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: {{ db_user }}
      POSTGRES_PASSWORD: {{ db_password }}
      POSTGRES_DB: {{ db_name }}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U {{ db_user }}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - internal

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    networks:
      - internal

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "{{ http_port | default(80) }}:80"
      - "{{ https_port | default(443) }}:443"
    volumes:
      - {{ app_data_dir }}/nginx/conf.d:/etc/nginx/conf.d:ro
      - {{ app_data_dir }}/nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    networks:
      - web

networks:
  internal:
    driver: bridge
  web:
    driver: bridge

volumes:
  db_data:
  redis_data:

Zero-Downtime Update

- name: Zero-downtime update
  hosts: docker_hosts
  serial: 1  # One host at a time
  tasks:
    - name: Pull new image
      community.docker.docker_image:
        name: "{{ app_image }}"
        tag: "{{ app_version }}"
        source: pull

    - name: Drain connections (if load balanced)
      # ... remove from load balancer ...

    - name: Update stack
      community.docker.docker_compose_v2:
        project_src: "{{ app_data_dir }}"
        state: present
        recreate: always

    - name: Wait for health
      ansible.builtin.uri:
        url: "http://localhost:{{ app_port }}/health"
        status_code: 200
      register: health
      until: health.status == 200
      retries: 30
      delay: 2

    - name: Restore to load balancer
      # ... add back to load balancer ...

Secrets Management

With ansible-vault

# group_vars/secrets.yml (encrypted)
app_secrets:
  DB_PASSWORD: supersecret
  API_KEY: abc123
  JWT_SECRET: longsecret
# templates/.env.j2
{% for key, value in app_secrets.items() %}
{{ key }}={{ value }}
{% endfor %}

With external secrets

- name: Fetch secret from 1Password
  ansible.builtin.set_fact:
    db_password: "{{ lookup('community.general.onepassword', 'database', field='password') }}"

- name: Deploy with secret
  community.docker.docker_compose_v2:
    project_src: "{{ app_data_dir }}"
    env_files:
      - "{{ app_data_dir }}/.env"
    state: present