Introduction#


When you begin constructing your Ansible tasks - roles - collections, you quickly realise that there are many tasks that you will use repeatedly with little variance. For example, if you’re using Ansible to help set-up and deploy your docker containers, you’ll have common tasks that create directories, configs, DNS and Traefik labels, among many others.

For me, my common tasks consist of two main types:

  1. Multiple (3+) common tasks defined in a file in a common tasks folder (external to the role), which are included in the role using the include_tasks module
  2. Smaller (1-3) common tasks defined and confined entirely within roles.

The rest of this document is dedicated to listing and describing common tasks that I use:


Cloudflare DNS#


One time-saving task when spinning up docker services is to automate the creationa and removal of cloudflare DNS records.


  ## ansible/common/cloudflare.yml

- name: Remove existing A DNS record
  when: cloudflare_remove_existing == 'true'
  community.general.cloudflare_dns:
    api_token: '{{ cloudflare_api }}'
    zone: '{{ cloudflare_domain }}'
    state: absent
    type: '{{ cloudflare_type }}'
    record: '{{ cloudflare_record }}'

- name: Perform Cloudflare DNS tasks
  block:
    - name: Add DNS record
      community.general.cloudflare_dns:
        api_token: '{{ cloudflare_api }}'
        zone: '{{ cloudflare_domain }}'
        state: present
        solo: '{{ cloudflare_solo }}'
        proxied: '{{ cloudflare_proxy }}'
        type: '{{ cloudflare_type }}'
        value: '{{ cloudflare_value }}'
        record: '{{ cloudflare_record }}'
      register: cloudflare_record_creation_status

    - name: Tasks on success
      when: cloudflare_record_creation_status is succeeded
      block:
        - name: Set 'dns_record_print' variable
          ansible.builtin.set_fact:
            cloudflare_record_print: '{{ (cloudflare_record == cloudflare_domain) | ternary(cloudflare_domain, cloudflare_record + "." + cloudflare_domain) }}'

        - name: Display DNS record creation status
          ansible.builtin.debug:
            msg: 'DNS A Record for "{{ cloudflare_record_print }}" set to "{{ cloudflare_value }}" was added. Proxy: {{ cloudflare_proxy }}'

  ## role/tasks/main.yml

- name: Add Cloudflare DNS records
  ansible.builtin.include_tasks: '/ansible/common/cloudflare.yml'
  vars:
    cloudflare_domain: '{{ local_domain }}'
    cloudflare_record: '{{ obsidian_name }}'
    cloudflare_type: 'A'
    cloudflare_value: '{{ ipify_public_ip }}'
    cloudflare_proxy: false
    cloudflare_solo: true
    cloudflare_remove_existing: true

Above, variables from the common task are replaced with relevant values required to create a DNS record for my Obsidian container running on my local server. However, you can create loops to handle as many services as you require:


  ## role/tasks/main.yml

- name: Add 'GitHub Pages' DNS records
  ansible.builtin.include_tasks: '/ansible/common/cloudflare.yml'
  vars:
    cloudflare_domain: '{{ github_pages_domain }}'
    cloudflare_record: '@'
    cloudflare_type: '{{ item.type }}'
    cloudflare_value: '{{ item.value }}'
    cloudflare_proxy: false
    cloudflare_solo: false
    cloudflare_remove_existing: false
  loop:
    - { type: 'A', value: '185.199.108.153' }
    - { type: 'A', value: '185.199.109.153' }
    - { type: 'A', value: '185.199.110.153' }
    - { type: 'A', value: '185.199.111.153' }
    - { type: 'AAAA', value: '2606:50c0:8000::153' }
    - { type: 'AAAA', value: '2606:50c0:8001::153' }
    - { type: 'AAAA', value: '2606:50c0:8002::153' }
    - { type: 'AAAA', value: '2606:50c0:8003::153' }

Directories and Files#


I create directories required by roles with the ansible.builtin.file module:


  ## role/tasks/main.yml

- name: Create directories
  ansible.builtin.file:
    path: '{{ item }}'
    state: directory
    force: false
    owner: '{{ puid }}'
    group: '{{ pgid }}'
    mode: '0755'
  loop:
    - '{{ bazarr_location }}'
    - '{{ bazarr_location }}/config'
    - '{{ lidarr_location }}'
    - '{{ prowlarr_location }}'
    - '{{ radarr_location }}'
    - '{{ radarr_4k_location }}'
    - '{{ sonarr_location }}'
    - '{{ sonarr_4k_location }}'
    - '{{ whisparr_location }}'

Sometimes I’ll need different permissions for directories in the loop:


  ## role/tasks/main.yml

- name: Create directories
  ansible.builtin.file:
    path: '{{ item.path }}'
    state: directory
    force: false
    owner: '{{ item.owner }}'
    group: '{{ item.group }}'
    mode: '0755'
  loop:
    - { path: '{{ authelia_location }}', owner: '{{ puid }}', group: '{{ pgid }}' }
    - { path: '{{ authelia_logs_location }}', owner: '{{ puid }}', group: '{{ pgid }}' }
    - { path: '{{ authelia_redis_location }}', owner: '1001', group: '1001' }
    - { path: '{{ traefik_location }}', owner: '{{ puid }}', group: '{{ pgid }}' }
    - { path: '{{ traefik_logs_location }}', owner: '{{ puid }}', group: '{{ pgid }}' }

Above, I use the Bitnami Redis container, which requires 1001/1001 permissions.

The builtin.file module is also useful when you need to ’touch’ a file:


  ## role/tasks/main.yml

- name: Touch acme.json
  ansible.builtin.file:
    path: '{{ traefik_location }}/acme.json'
    state: touch
    force: false
    owner: '{{ puid }}'
    group: '{{ pgid }}'
    mode: '0600'

Removing directories and files is as simple as providing the builtin.file module a path and state: absent:


- name: Remove sqlite files
  ansible.builtin.file:
    path: '{{ item }}'
    state: absent
  loop:
    - '{{ sqlite_location }}/logs.db'
    - '{{ sqlite_location }}/logs.db-shm'
    - '{{ sqlite_location }}/logs.db-wal'
    - '{{ sqlite_location }}/{{ sqlite_name }}.db'
    - '{{ sqlite_location }}/{{ sqlite_name }}.db-shm'
    - '{{ sqlite_location }}/{{ sqlite_name }}.db-wal'

- name: Remove sqlite backups folders
  ansible.builtin.file:
    path: '{{ backups_location }}'
    state: absent

File Copy#


A simple task to copy files from one directory to another. I typically use this to copy files that docker services require from their role folder to the services appdata directory (though, I prefer to template files where and when possible).


  ## ansible/common/copy.yml

- name: Check if file exists
  ansible.builtin.stat:
    path: '{{ copy_destination }}'
  register: file_copy_stat

- name: Copy file
  when: not file_copy_stat.stat.exists
  ansible.builtin.copy:
    src: '{{ copy_source }}'
    dest: '{{ copy_destination }}'
    force: '{{ copy_force }}'
    owner: '{{ copy_owner }}'
    group: '{{ copy_pgid }}'
    mode: '{{ copy_mode }}'

- name: Wait for file to be created
  ansible.builtin.wait_for:
    path: '{{ copy_destination }}'
    state: present

  ## role/tasks/main.yml

- name: Copy Hugo Terminal Themes files
  ansible.builtin.include_tasks: '/ansible/common/copy.yml'
  vars:
    copy_source: '{{ item.source }}'
    copy_destination: '{{ item.destination }}'
    copy_force: true
    copy_owner: '{{ puid }}'
    copy_group: '{{ pgid }}'
    copy_mode: '0644'
  loop:
    - { source: '{{ role_path }}/files/favicon.png', destination: '/{{ hugo_site_name }}/static/favicon.png' }
    - { source: '{{ role_path }}/files/og-image.png', destination: '/{{ hugo_site_name }}/static/og-image.png' }
    - { source: '{{ role_path }}/files/terminal.css', destination: '/{{ hugo_site_name }}/static/terminal.css' }

In the above example, I copy the files required for this blogs theme.


Templates#


A core reason I’m using Ansible is to set up configs for my services. For this, I typically template a config file from the role into the service folder.


  ## role/tasks/main.yml

- name: Conduct template tasks
  ansible.builtin.template:
    src: '{{ item.template }}'
    dest: '{{ item.file }}'
    force: false
    owner: '{{ puid }}'
    group: '{{ pgid }}'
    mode: '0664'
  loop:
    - { template: '{{ role_path }}/templates/configs/bazarr_config.yaml.j2', file: '{{ bazarr_location }}/config/config.yaml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ lidarr_location }}/config.xml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ prowlarr_location }}/config.xml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ radarr_location }}/config.xml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ radarr_4k_location }}/config.xml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ sonarr_location }}/config.xml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ sonarr_4k_location }}/config.xml' }
    - { template: '{{ role_path }}/templates/configs/arrs_config.xml.j2', file: '{{ whisparr_location }}/config.xml' }

- name: Wait for files to be created
  ansible.builtin.wait_for:
    path: '{{ item }}'
    state: present
  loop:
    - '{{ bazarr_location }}/config/config.yaml'
    - '{{ lidarr_location }}/config.xml'
    - '{{ prowlarr_location }}/config.xml'
    - '{{ radarr_location }}/config.xml'
    - '{{ radarr_4k_location }}/config.xml'
    - '{{ sonarr_location }}/config.xml'
    - '{{ sonarr_4k_location }}/config.xml'
    - '{{ whisparr_location }}/config.xml'

Above, I template configs from the arrs role folder into respective arrs appdata directories. The wait_for task ensures templated files are in place before subsequent tasks continue.

Example config (autobrr_config.toml.j2):

# config.toml

host = '0.0.0.0'
port = '{{ autobrr_ports_cont }}'
logPath = '{{ autobrr_logs_path }}'
logLevel = '{{ autobrr_logs_level }}'
logMaxSize = '{{ autobrr_logs_max_size }}'
logMaxBackups = '{{ autobrr_logs_max_backups }}'
checkForUpdates = true
sessionSecret = '{{ autobrr_session_secret.stdout }}'

After templating:

# config.toml

host = '0.0.0.0'
port = '7474'
logPath = '/log/autobrr.log'
logLevel = 'DEBUG'
logMaxSize = '50'
logMaxBackups = '3'
checkForUpdates = true
sessionSecret = 'SomeSecret'

Traefik Labels#


Traefik is my reverse-proxy of choice, with much of the config taking place in docker labels. Previously, I would define the labels for each service individually in role defaults. Depending on the service, I have http, https, api and theme-park labels. It can get extensive, but most of the labels are common across services with few differing variables.

To reduce the bloat, I make use of:

  1. A labels template contained in my group_vars
  2. A common task file to determine required labels
  3. Include_tasks task with relevant variables.

  ## ansible/group_vars/all/traefik.yml

traefik_middlewares: 'globalHeaders@file,autodetect@swarm,gzip@swarm,robotHeaders@file'
traefik_http_middlewares: '{{ traefik_middlewares + ",redirect-to-https@swarm,cloudflarewarp@swarm" }}'
traefik_https_middlewares: '{{ traefik_middlewares + ",secureHeaders@file,hsts@file,cloudflarewarp@swarm" }}'

traefik_labels_core:
  - traefik.enable: "true"
  - traefik.docker.network: "{{ router_network }}"

traefik_labels_http:
  - '{ "traefik.http.routers.{{ router_name }}.entrypoints": "{{ router_entrypoint }}" }'
  - '{ "traefik.http.routers.{{ router_name }}.rule": "{{ router_rule }}" }'
  - '{ "traefik.http.routers.{{ router_name }}.middlewares": "{{ router_http_middlewares }}" }'
  - '{ "traefik.http.routers.{{ router_name }}.priority": "20" }'
  - '{ "traefik.http.routers.{{ router_name }}.service": "{{ router_name }}-svc" }'
  - '{ "traefik.http.services.{{ router_name }}-svc.loadbalancer.server.port": "{{ router_port }}" }'

traefik_labels_https:
  - '{ "traefik.http.routers.{{ router_name }}-secure.entrypoints": "{{ router_secure_entrypoint }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-secure.rule": "{{ router_rule }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-secure.middlewares": "{{ router_https_middlewares }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-secure.priority": "20" }'
  - '{ "traefik.http.routers.{{ router_name }}-secure.service": "{{ router_name }}-secure-svc" }'
  - '{ "traefik.http.routers.{{ router_name }}-secure.tls.certresolver": "{{ router_tls_certresolver }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-secure.tls.options": "{{ router_tls_options }}" }'
  - '{ "traefik.http.services.{{ router_name }}-secure-svc.loadbalancer.server.port": "{{ router_port }}" }'

traefik_labels_http_api:
  - '{ "traefik.http.routers.{{ router_name }}-api.entrypoints": "{{ router_entrypoint }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api.rule": "{{ router_api_rule }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api.middlewares": "{{ traefik_http_middlewares }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api.priority": "30" }'
  - '{ "traefik.http.routers.{{ router_name }}-api.service": "{{ router_name }}-svc" }'

traefik_labels_https_api:
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.entrypoints": "{{ router_secure_entrypoint }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.rule": "{{ router_api_rule }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.middlewares": "{{ traefik_https_middlewares }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.priority": "30" }'
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.service": "{{ router_name }}-secure-svc" }'
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.tls.certresolver": "{{ router_tls_certresolver }}" }'
  - '{ "traefik.http.routers.{{ router_name }}-api-secure.tls.options": "{{ router_tls_options }}" }'

traefik_labels_themepark:
  - '{ "traefik.http.middlewares.themepark-{{ router_name }}.plugin.themepark.app": "{{ router_themepark_app }}" }'
  - '{ "traefik.http.middlewares.themepark-{{ router_name }}.plugin.themepark.theme": "{{ router_themepark_theme }}" }'

  ## ansible/common/labels.yml

################################
# CHECKS
################################

- name: Set api fact
  ansible.builtin.set_fact:
    router_api_is_enabled: '{{ (router_api is defined) and
                               (router_api is not none) and
                               (router_api | trim | length > 0) }}'

- name: Set themepark fact
  ansible.builtin.set_fact:
    router_themepark_is_enabled: '{{ (router_themepark_app is defined) and
                                     (router_themepark_app is not none) and
                                     (router_themepark_app | trim | length > 0) }}'

################################
# RULES
################################

- name: Set router rule
  ansible.builtin.set_fact:
    router_rule: '{{ "Host(`" + router_name + "." + router_domain + "`)" }}'

- name: Set router api rule
  when: router_api_is_enabled
  ansible.builtin.set_fact:
    router_api_rule: '{{ "Host(`" + router_name + "." + router_domain + "`) && (" + router_api + ")" }}'

################################
# LABELS
################################

- name: Set basic traefik labels
  when:
    - not router_themepark_is_enabled
    - not router_api_is_enabled
  ansible.builtin.set_fact:
    '{{ router_variable }}': '{{ traefik_labels_core
                                 | combine(traefik_labels_http)
                                 | combine(traefik_labels_https) }}'

- name: Set api traefik labels
  when:
    - not router_themepark_is_enabled
    - router_api_is_enabled
  ansible.builtin.set_fact:
    '{{ router_variable }}': '{{ traefik_labels_core
                                 | combine(traefik_labels_http)
                                 | combine(traefik_labels_https)
                                 | combine(traefik_labels_http_api) 
                                 | combine(traefik_labels_https_api) }}'

- name: Set api-only traefik labels
  when:
    - not router_themepark_is_enabled
    - router_api_is_enabled
    - router_variable is search('_api_labels')
  ansible.builtin.set_fact:
    '{{ router_variable }}': '{{ traefik_labels_core
                                 | combine(traefik_labels_http_api) 
                                 | combine(traefik_labels_https_api) }}'

- name: Set themepark traefik labels
  when:
    - router_themepark_is_enabled
    - not router_api_is_enabled
  ansible.builtin.set_fact:
    '{{ router_variable }}': '{{ traefik_labels_core
                                 | combine(traefik_labels_http)
                                 | combine(traefik_labels_https)
                                 | combine(traefik_labels_themepark) }}'

- name: Set full traefik labels
  when:
    - router_themepark_is_enabled
    - router_api_is_enabled
  ansible.builtin.set_fact:
    '{{ router_variable }}': '{{ traefik_labels_core
                                 | combine(traefik_labels_http)
                                 | combine(traefik_labels_https)
                                 | combine(traefik_labels_http_api) 
                                 | combine(traefik_labels_https_api)
                                 | combine(traefik_labels_themepark) }}'

The above task file combines the relevant labels into a single variable that is provided to the docker compose file.


  ## role/tasks/main.yml

- name: Set traefik Labels
  ansible.builtin.include_tasks: /ansible/common/labels.yml
  vars:
    router_variable: '{{ item.var }}'
    router_network: '{{ network_overlay }}'
    router_name: '{{ item.name }}'
    router_port: '{{ item.port }}'
    router_api: '{{ item.api }}'
    router_domain: '{{ local_domain }}'
    router_entrypoint: 'http'
    router_secure_entrypoint: 'https'
    router_tls_certresolver: 'dns-cloudflare'
    router_tls_options: 'securetls@file'
    router_themepark_app: '{{ item.tp_app }}'
    router_themepark_theme: 'hotpink'
    router_http_middlewares: '{{ traefik_http_middlewares + item.sso + item.tp }}'
    router_https_middlewares: '{{ traefik_https_middlewares + item.sso + item.tp }}'
  loop:
    ## bazarr
    - { var: 'bazarr_labels', 
        name: '{{ bazarr_name }}', 
        port: '{{ bazarr_ports_cont }}', 
        api: 'PathPrefix(`/api`)', 
        sso: ',authelia@swarm', 
        tp: ',themepark-bazarr', 
        tp_app: 'bazarr' }
    ## lidarr
    - { var: 'lidarr_labels', 
        name: '{{ lidarr_name }}', 
        port: '{{ lidarr_ports_cont }}', 
        api: 'PathPrefix(`/api`) || PathPrefix(`/feed`) || PathPrefix(`/ping`)', 
        sso: ',authelia@swarm', 
        tp: ',themepark-lidarr', 
        tp_app: 'lidarr' }

In the above example, both services receive full Traefik labels, including being protected by my SSO provider (Authelia), API labels and Theme-Park. Simply leaving the API and/or Themepark vars empty will remove these labels.

All that remains after this is to point to the labels in your docker service:


    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.labels.ansible_host == localhost]
      labels: {{ lidarr_labels }}
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s

Databases#


I let Ansible handle the creation of various databases required by my services:


Postgres#


I use the postgresql ping and db modules to ping for databases and to create them if none exists:

Note: Requires an existing running postgres instance.


  ## ansible/common/postgres.yml

- name: Ping for existing database
  community.postgresql.postgresql_ping:
    login_host: '{{ local_ip }}'
    login_user: '{{ postgres_username }}'
    login_password: '{{ postgres_password }}'
    port: '{{ postgres_ports_host }}'
    login_db: '{{ postgres_database }}'
  register: postgres_db_exists

- name: Create postgres database
  when: not postgres_db_exists == true
  community.postgresql.postgresql_db:
    login_host: '{{ local_ip }}'
    login_user: '{{ postgres_username }}'
    login_password: '{{ postgres_password }}'
    port: '{{ postgres_ports_host }}'
    name: '{{ postgres_database }}'
    state: present

  ## role/tasks/main.yml

- name: Conduct Postgres DB tasks
  ansible.builtin.include_tasks: /ansible/common/postgres.yml
  vars:
    postgres_database: '{{ item }}'
  loop:
    - 'bazarr'
    - 'lidarr-main'
    - 'lidarr-log'
    - 'prowlarr-main'
    - 'prowlarr-log'
    - 'radarr-main'
    - 'radarr-log'
    - 'radarr-4k-main'
    - 'radarr-4k-log'
    - 'sonarr-main'
    - 'sonarr-log'
    - 'sonarr-4k-main'
    - 'sonarr-4k-log'
    - 'whisparr-main'
    - 'whisparr-log'

Above, the only the database name changes, since I’m working with one postgres instance.


MariaDB#


I use the mysql_db module to ping and create databases in a single task:


  ## role/tasks/main.yml

- name: Create mariadb database
  community.mysql.mysql_db:
    login_host: '{{ local_machine }}'
    login_user: 'root'
    login_password: '{{ mariadb_password }}'
    login_port: '{{ mariadb_ports_host }}'
    name: wordpress
    state: present

Note: Requires an existing MariaDB instance to be present and running.


Docker#


I use Ansible to automate important Docker tasks:


Containers#


Removing individual containers using the docker_container module:


  ## role/tasks/main.yml

- name: remove existing container
  community.docker.docker_container:
    container_default_behavior: compatibility
    name: '{{ radarr_name }}'
    state: absent
    stop_timeout: 10
  register: remove_radarr_docker
  retries: 5
  delay: 10
  until: remove_radarr_docker is succeeded

Creating containers using the same module:


  ## role/tasks/main.yml

- name: Create radarr container
  community.docker.docker_container:
    name: '{{ radarr_name }}'
    image: '{{ radarr_image_repo }}:{{ radarr_image_tag }}'
    networks: '{{ network_overlay }}'
    env:
      PUID: '{{ puid }}'
      PGID: '{{ pgid }}'
      TZ: '{{ timezone }}'
      TP_SCHEME: 'http'
      TP_DOMAIN: '{{ themepark_domain }}'
      TP_HOTIO: 'true'
      TP_THEME: '{{ themepark_theme }}'
    labels: '{{ radarr_labels }}'
    ports:
      - '{{ radarr_ports_host }}:{{ radarr_ports_cont }}'
    volumes:
      - '{{ radarr_location }}:/config'
      - '{{ themes_location }}/98-themepark-radarr:/etc/cont-init.d/98-themepark'
      - '/mnt:/mnt'
      - '{{ torrents_volume }}:{{ torrents_volume_mount_path }}'
    restart_policy: '{{ radarr_restart_policy }}'

Compose#


Compose down using the docker_compose_v2 module:


  ## role/tasks/main.yml

- name: Check if VPN compose exists
  ansible.builtin.stat:
    path: /opt/vpn-compose.yml
  register: vpn_compose_yaml

- name: VPN compose down
  when: vpn_compose_yaml.stat.exists
  community.docker.docker_compose_v2:
    project_src: /opt
    files: vpn-compose.yml
    state: absent

- name: Remove vpn-compose file
  when: vpn_compose_yaml.stat.exists
  ansible.builtin.file:
    path: /opt/vpn-compose.yml
    state: absent

Compose up using the same module:


  ## role/tasks/main.yml

- name: Import VPN compose file
  ansible.builtin.template:
    src: '{{ role_path }}/templates/vpn-compose.yml.j2'
    dest: /opt/vpn-compose.yml
    force: true
    owner: '{{ puid }}'
    group: '{{ pgid }}'
    mode: '0664'

- name: VPN compose up
  community.docker.docker_compose_v2:
    project_src: /opt
    files: vpn-compose.yml
    state: present

Networks#


I use the docker_network module to create bridge and overlay networks:


  ## ansible/playbook.yml

- name: Conduct overlay network tasks
  when: inventory_hostname == 'localhost'
  block:
    - name: Register overlay network
      community.docker.docker_network_info:
        name: '{{ network_overlay }}'
      register: network_overlay_result

    - name: Create overlay network
      when: not network_overlay_result.exists
      community.docker.docker_network:
        name: '{{ network_overlay }}'
        driver: '{{ network_overlay_driver }}'
        attachable: true 
        ipam_config:
          - subnet: '{{ network_overlay_subnet }}'

Stacks#


Docker stack down using the docker_stack module:


  ## role/tasks/main.yml

- name: Remove arrs stack
  community.docker.docker_stack:
    name: arrs
    state: absent

- name: Remove arrs-stack file
  ansible.builtin.file:
    path: /opt/arrs-stack.yml
    state: absent

Stack deploy using the same module:


  ## role/tasks/main.yml

- name: Import arrs-stack file
  ansible.builtin.template:
    src: '{{ role_path }}/templates/arrs-stack.yml.j2'
    dest: /opt/arrs-stack.yml
    force: true
    owner: '{{ puid }}'
    group: '{{ pgid }}'
    mode: '0664'

- name: Deploy arrs stack
  community.docker.docker_stack:
    state: present
    name: arrs
    compose:
      - /opt/arrs-stack.yml

Secrets#


Creating Docker Secrets using the docker_secret module:


  ## role/tasks/main.yml

- name: Prepare docker secrets
  community.docker.docker_secret:
    name: '{{ item.name }}'
    data: '{{ item.secret }}'
    state: present
    force: false
  loop:
    - { name: 'authelia_redis_secret', secret: '{{ authelia_redis_key }}' }
    - { name: 'postgres_pass_secret', secret: '{{ postgres_password }}' }

Then referring to these in the compose file:


services:
  {{ authelia_name }}:
    environment:
      AUTHELIA_SESSION_REDIS_PASSWORD_FILE: /run/secrets/authelia_redis_secret
      AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE: /run/secrets/postgres_pass_secret
    secrets:
      - authelia_redis_secret
      - postgres_pass_secret

secrets:
  authelia_redis_secret:
    external: true

  postgres_pass_secret:
    external: true

Volumes#


I use the docker_volume module to create docker volumes:


  ## role/tasks/main.yml

- name: Create media volume
  community.docker.docker_volume:
    volume_name: '{{ media_volume }}'
    state: present
    recreate: options-changed
    driver: local
    driver_options:
      type: nfs
      o: 'addr={{ media_volume_address }},rw,nfsvers=4.2'
      device: '{{ media_volume_device }}'

Above, I create an NFS volume to attach to my Plex container providing access to media shares.