Initial commit

This commit is contained in:
hiperman
2026-01-26 23:24:37 -05:00
commit 26c32763cc
11 changed files with 936 additions and 0 deletions

415
README.md Normal file
View File

@@ -0,0 +1,415 @@
# Docker Compose App Role
A flexible Ansible role for deploying and managing Docker Compose applications with features including backup/restore and healthchecks.
## Overview
This role provides a standardized way to deploy containerized applications using Docker Compose. It handles directory creation, template rendering, and container lifecycle management.
## Features
- **Template-based deployment** with Jinja2 templating
- **Dual backup system** (controller + remote host)
- **Health checks** and deployment verification
- **Multiple deployment modes** (template, file, or inline content)
- **Flexible directory management**
- **Configurable backup retention policies**
## Quick Start
### 1. Create a New Role
Use the provided script to create a new application skeleton role:
```bash
./create_app_role.sh my-app
```
This creates:
```
roles/my-app/
├── defaults/main.yml # Default variables
├── meta/main.yml # Role metadata and dependencies
└── templates/
└── compose.yml.j2 # Docker Compose template
```
### 2. Configure Your Application
Edit `roles/my-app/defaults/main.yml`:
```yaml
---
app_role_name: my-app
my_app_container_name: "{{ app_name | default('my-app') }}"
my_app_container_version: latest
my_app_restart_policy: "{{ app_restart_policy }}"
my_app_http_port: 8080
my_app_data_path: "{{ app_dir }}/data"
my_app_config_path: "{{ app_dir }}/config"
# Optional: directories to create
app_subdirectories:
- "{{ my_app_data_path }}"
- "{{ my_app_config_path }}"
# Optional: backup configuration
app_backup_subdirectories:
- "{{ my_app_data_path }}"
- "{{ my_app_config_path }}"
```
### 3. Create Docker Compose Template
Edit `roles/my-app/templates/compose.yml.j2`:
```yaml
---
services:
my-app:
image: "my-app:{{ my_app_container_version }}"
container_name: "{{ my_app_container_name }}"
restart: "{{ my_app_restart_policy }}"
ports:
- "{{ my_app_http_port }}:8080"
volumes:
- "{{ my_app_data_path }}:/data"
- "{{ my_app_config_path }}:/config"
environment:
- TZ={{ app_timezone | default('UTC') }}
```
### 4. Deploy Your Application
Add to your playbook:
```yaml
---
- name: Deploy my application
hosts: localhost
roles:
- role: my-app
```
## Advanced Usage
### Backup and Restore
#### Create a New Backup
Create an on-demand backup using tags:
```bash
# Backup a specific application
ansible-playbook --tags backup playbook.yml
```
Configure backup retention and options in your role's meta vars:
```yaml
# In roles/my-app/meta/main.yml
dependencies:
- role: docker_compose_app
vars:
app_backup_subdirectories:
- "{{ my_app_data_path }}"
- "{{ my_app_config_path }}"
app_backup_retention_days_controller: 90 # Keep backups for 90 days
app_backup_retention_days_remote: 7 # Keep remote backups for 7 days
app_backup_stop_services: true # Stop containers during backup
```
#### Automated Backups
For scheduled backups:
```bash
# Run daily backup at 2 AM - only runs backup tasks
0 2 * * * ansible-playbook -i inventory --tags backup my-playbook.yml
```
### Tag-Based Execution
The role supports selective execution using tags:
```bash
# Setup only - doesn't start the containers
ansible-playbook --tags setup playbook.yml
# Full deployment - setup + start containers
ansible-playbook --tags deploy playbook.yml
# Restore and deploy
ansible-playbook --tags restore,deploy playbook.yml
# Backup only
ansible-playbook --tags backup playbook.yml
# Update only
ansible-playbook --tags update playbook.yml
# Backup then update
ansible-playbook --tags backup,update playbook.yml
```
#### Restore from Backup
Restore from the latest backup:
```bash
# Setup directories, restore from backup, then deploy
ansible-playbook --tags setup,restore,deploy playbook.yml
```
Configure restore options in your role's meta vars:
```yaml
# In roles/my-app/meta/main.yml
dependencies:
- role: docker_compose_app
vars:
app_restore_source: controller # or 'remote'
app_restore_max_age_days: 30 # Only restore backups newer than 30 days
```
Restore from a specific archive by setting the variable at runtime:
```bash
ansible-playbook --tags restore playbook.yml \
-e app_restore_archive=/path/to/specific/backup-20240315-120000.tar.gz
```
### Health Checks
Configure health monitoring:
```yaml
- role: my-app
vars:
app_name: my-app
app_health_check: true
app_health_check_method: http
app_health_check_url: "http://localhost:8080/health"
app_health_check_retries: 30
app_health_check_delay: 10
```
### Updates
Update application containers using tags:
```bash
# Update a specific application
ansible-playbook --tags update playbook.yml
# Update all applications in a playbook
ansible-playbook --tags update site.yml
```
**Update process:**
1. Stops the application
2. Pulls latest images for all services
3. Recreates containers with new images
4. Runs health checks (if enabled)
### Rollbacks
Rollback using the restore functionality:
```bash
# First, create a backup then update
ansible-playbook --tags backup,update playbook.yml
# If update fails, rollback from backup
ansible-playbook --tags restore playbook.yml
```
**Important Notes:**
- Updates do NOT create automatic backups - create backups manually beforehand
- Rollbacks must be triggered manually using the restore functionality
- Using `latest` tags prevents effective rollbacks since the old image is overwritten
- For production, use specific version tags instead of `latest`
### Multiple Instances
Deploy multiple instances of the same application:
```yaml
- role: jellyfin
vars:
app_name: jellyfin-movies
jellyfin_http_port: 8096
- role: jellyfin
vars:
app_name: jellyfin-tv
jellyfin_http_port: 8097
```
### Custom Templates and Files
#### Override template location:
```yaml
- role: my-app
vars:
app_compose_template: /path/to/custom/compose.yml.j2
```
#### Use a static compose file:
If you have an existing `docker-compose.yml` file that doesn't need templating:
```yaml
- role: my-app
vars:
app_name: my-app
app_compose_file: /path/to/existing/docker-compose.yml
```
The file will be copied as-is to the application directory without Jinja2 processing.
#### Use inline content:
```yaml
- role: docker_compose_app
vars:
app_name: simple-app
app_role_name: simple-app
app_compose_content: |
services:
app:
image: nginx:alpine
ports:
- "80:80"
```
## Configuration Reference
### Core Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `app_name` | Required | Unique application instance name |
| `role_name` | Required | Role name for template paths |
| `app_dir` | `{{ host_root_path }}/{{ app_name }}` | Application directory |
| `app_uid` | `{{ ansible_facts.user_uid }}` | File ownership UID |
| `app_gid` | `{{ ansible_facts.user_gid }}` | File ownership GID |
| `app_permission_mode` | `"0640"` | File permission mode |
### Deployment Options
| Variable | Default | Description |
|----------|---------|-------------|
| `app_compose_template` | `{{ app_templates_path }}/compose.yml.j2` | Template file path |
| `app_compose_file` | - | Static compose file path |
| `app_compose_content` | - | Inline compose content |
| `app_compose_validate` | `true` | Validate compose syntax |
| `app_compose_pull` | `policy` | Image pull strategy |
| `app_compose_recreate` | `auto` | Container recreate strategy |
### Backup Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `app_backup_subdirectories` | `[]` | Specific directories to backup |
| `app_backup_stop_services` | `true` | Stop containers during backup |
| `app_backup_retention_days_controller` | `90` | Controller backup retention |
| `app_backup_retention_days_remote` | `7` | Remote backup retention |
### Restore Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `app_restore_source` | `controller` | Restore source (`controller`/`remote`) |
| `app_restore_archive` | - | Specific archive file to restore (overrides latest) |
| `app_restore_max_age_days` | `30` | Maximum backup age for restore (when finding latest) |
### Health Check Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `app_health_check` | `true` | Enable health checks |
| `app_health_check_method` | `docker` | Check method (`docker`/`http`) |
| `app_health_check_url` | - | HTTP endpoint for health check |
| `app_health_check_retries` | `30` | Number of check retries |
| `app_health_check_delay` | `10` | Delay between checks (seconds) |
| `app_health_check_status_codes` | `[200, 201, 202]` | Valid HTTP status codes |
### Directory Management
| Variable | Default | Description |
|----------|---------|-------------|
| `app_subdirectories` | `[]` | Directories to create in app_dir |
| `app_extra_templates` | `[]` | Additional templates to render |
Example `app_extra_templates`:
```yaml
app_extra_templates:
- src: config.json.j2
dest: "{{ app_dir }}/config/app.json"
- src: env.j2
dest: "{{ app_dir }}/.env"
```
## Best Practices
1. **Use semantic versioning** for container tags instead of `latest`
2. **Test restore procedures** regularly to ensure backups are valid
3. **Use specific directories** for backups instead of entire app directory
4. **Monitor disk usage** on backup storage locations
## Architecture
The role is organized into logical task files for selective execution:
```
├── setup.yaml # Validation, directories, and templates
├── restore.yaml # Backup restoration logic
├── deploy.yaml # Docker Compose deployment and startup
├── health_check.yaml # Health monitoring and verification
├── backup.yaml # Backup creation and management
├── update.yaml # Container update and image pulling
└── manage_compose.yaml # Docker Compose lifecycle management
```
### Task Execution Flow
1. **Setup** (`setup.yaml`) - [setup, deploy]
- Docker/Compose validation
- Variable validation and security checks
- Directory creation
- Additional template deployment
2. **Restore** (`restore.yaml`) - [restore, never]
- Backup file selection and validation
- Service stopping
- Archive extraction
- Service restart
3. **Deploy** (`deploy.yaml`) - [deploy]
- Docker Compose file creation
- Container startup and management
4. **Health Check** (`health_check.yaml`) - [deploy, healthcheck]
- Service health verification
- Endpoint monitoring
5. **Backup** (`backup.yaml`) - [backup, never]
- Service stopping (optional)
- Archive creation
- Backup copying and cleanup
- Service restart
6. **Update** (`update.yaml`) - [update, never]
- Service stopping
- Image pulling
- Container recreation
- Health verification
Each task file is designed to be idempotent and can be run multiple times safely.

40
defaults/main.yaml Normal file
View File

@@ -0,0 +1,40 @@
---
app_dir: "{{ host_root_path }}/{{ app_name }}"
app_uid: "{{ ansible_facts.user_uid }}"
app_gid: "{{ ansible_facts.user_gid }}"
app_permission_mode: "0640"
app_restart_policy: unless-stopped
app_backup_path: "{{ backup_path }}/{{ app_name }}"
app_backup_path_remote: "{{ app_dir }}/backups"
app_backup: false
app_backup_subdirectories: []
app_backup_stop_services: true
app_backup_retention_days_controller: 90
app_backup_retention_days_remote: 7
app_restore: false
app_restore_source: controller
app_restore_max_age_days: 30
app_compose_dest: "{{ app_dir }}/docker-compose.yml"
app_compose_validate: true
app_compose_pull: policy
app_compose_recreate: auto
app_compose_start: true
app_health_check: true
app_health_check_method: docker
app_health_check_retries: 30
app_health_check_delay: 10
app_health_check_status_codes: [200, 201, 202]
app_templates_path: "{{ app_roles_path + '/' + app_role_name }}/templates"
app_compose_template: "{{ app_templates_path }}/compose.yml.j2"
app_subdirectories: []
# - config
# - data
app_extra_templates: []
# - src: path/to/template/config.json.j2
# dest: "{{ app_dir }}/config.json"

6
meta/main.yml Normal file
View File

@@ -0,0 +1,6 @@
---
galaxy_info:
author: Patrick Jaroszewski
description: Role for deploying Docker Compose apps
license: MIT
min_ansible_version: "2.14"

97
tasks/backup.yaml Normal file
View File

@@ -0,0 +1,97 @@
---
- name: "{{ app_name }} - Backup application data"
block:
- name: Starting backup
debug:
msg: |
Starting backup of {{ app_name }}
Backing up dirs: {{ app_backup_subdirectories }}
- name: Validate required backup variables
ansible.builtin.fail:
msg: "Required variable {{ item }} is not defined"
when: vars[item] is not defined
loop:
- app_name
- app_dir
- backup_path
- name: Set backup source paths
ansible.builtin.set_fact:
_backup_sources: "{{ app_backup_subdirectories | default([app_dir + '/*']) }}"
- name: Create local backup directory on controller
ansible.builtin.file:
path: "{{ app_backup_path }}"
state: directory
mode: "0750"
delegate_to: localhost
- name: Create remote backup directory on host
ansible.builtin.file:
path: "{{ app_backup_path_remote | default(app_dir + '/backups') }}"
state: directory
owner: "{{ app_uid | default(omit) }}"
group: "{{ app_gid | default(omit) }}"
mode: "{{ app_permission_mode | default('0755') }}"
- name: Stop application for backup
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: stopped
when: app_backup_stop_services | default(true)
- name: Create timestamped backup
ansible.builtin.archive:
path: "{{ _backup_sources }}"
dest: "{{ app_backup_path_remote | default(app_dir + '/backups') }}/{{ app_name }}-{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
format: gz
owner: "{{ app_uid | default(omit) }}"
group: "{{ app_gid | default(omit) }}"
mode: "{{ app_permission_mode | default('0644') }}"
register: remote_backup
- name: Copy backup to controller
ansible.builtin.fetch:
src: "{{ remote_backup.dest }}"
dest: "{{ app_backup_path }}/{{ app_name }}-{{ ansible_date_time.iso8601_basic_short }}.tar.gz"
flat: true
when: remote_backup is succeeded
- name: Clean old backups on controller
ansible.builtin.find:
paths: "{{ app_backup_path }}"
patterns: "{{ app_name }}-*.tar.gz"
age: "{{ app_backup_retention_days_controller | default(90) }}d"
register: old_backups_controller
delegate_to: localhost
- name: Remove old backups from controller
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups_controller.files | default([]) }}"
delegate_to: localhost
- name: Clean old backups on remote host
ansible.builtin.find:
paths: "{{ app_backup_path_remote | default(app_dir + '/backups') }}"
patterns: "{{ app_name }}-*.tar.gz"
age: "{{ app_backup_retention_days_remote | default(7) }}d"
register: old_backups_remote
- name: Remove old backups from remote host
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups_remote.files | default([]) }}"
always:
- name: Restart application after backup
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
when: app_backup_stop_services | default(true)
tags: [backup]
when: app_backup | bool

39
tasks/deploy.yaml Normal file
View File

@@ -0,0 +1,39 @@
---
- name: "{{ app_name }} - Deploy Docker Compose configuration"
block:
- name: Copy Docker Compose template
ansible.builtin.template:
src: "{{ app_compose_template }}"
dest: "{{ app_compose_dest }}"
mode: "{{ app_permission_mode }}"
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
validate: "{{ 'docker compose -f %s config -q' if app_compose_validate else omit }}"
when: _compose_type == 'template'
register: compose_file
- name: Copy Docker Compose file
ansible.builtin.copy:
src: "{{ app_compose_file }}"
dest: "{{ app_compose_dest }}"
mode: "{{ app_permission_mode }}"
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
validate: "{{ 'docker compose -f %s config -q' if app_compose_validate else omit }}"
when: _compose_type == 'file'
register: compose_file
- name: Copy Docker Compose content from YAML string
ansible.builtin.copy:
content: "{{ app_compose_content }}"
dest: "{{ app_compose_dest }}"
mode: "{{ app_permission_mode }}"
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
validate: "{{ 'docker compose -f %s config -q' if app_compose_validate else omit }}"
when: _compose_type == 'content'
register: compose_file
- ansible.builtin.include_tasks: manage_compose.yaml
when: app_compose_start | default(true)
tags: [deploy]

49
tasks/health_check.yaml Normal file
View File

@@ -0,0 +1,49 @@
---
- name: Wait for containers to be healthy
community.docker.docker_container_info:
name: "{{ app_name }}"
register: container_info
until: container_info.container.State.Health.Status | default('healthy') == 'healthy'
retries: "{{ app_health_check_retries | default(30) }}"
delay: "{{ app_health_check_delay | default(10) }}"
when: app_health_check | default(true) and app_health_check_method | default('docker') == 'docker'
ignore_errors: true
- name: Verify application HTTP endpoint
ansible.builtin.uri:
url: "{{ app_health_check_url }}"
method: GET
status_code: "{{ app_health_check_status_codes | default([200, 201, 202]) }}"
register: http_health_check
until: http_health_check.status in (app_health_check_status_codes | default([200, 201, 202]))
retries: "{{ app_health_check_retries | default(30) }}"
delay: "{{ app_health_check_delay | default(10) }}"
when: app_health_check | default(true) and app_health_check_method | default('docker') == 'http' and app_health_check_url is defined
ignore_errors: true
- name: Check if all containers are running
ansible.builtin.command:
cmd: docker compose ps --services --filter status=running
chdir: "{{ app_dir }}"
register: running_services
when: app_health_check | default(true)
- name: Check total services
ansible.builtin.command:
cmd: docker compose ps --services
chdir: "{{ app_dir }}"
register: total_services
when: app_health_check | default(true)
- name: Verify deployment success
ansible.builtin.assert:
that:
- running_services.stdout_lines | length == total_services.stdout_lines | length
fail_msg: "Not all containers are running. Running: {{ running_services.stdout_lines | length }}, Total: {{ total_services.stdout_lines | length }}"
success_msg: "All containers are running successfully."
when: app_health_check | default(true) and total_services.stdout_lines is defined
- name: Log deployment status
ansible.builtin.debug:
msg: "Deployment {{ 'successful' if (running_services.stdout_lines | length == total_services.stdout_lines | length) else 'failed' }}"
when: app_health_check | default(true) and total_services.stdout_lines is defined

14
tasks/main.yaml Normal file
View File

@@ -0,0 +1,14 @@
---
- ansible.builtin.include_tasks: setup.yaml
tags: [setup, deploy]
- ansible.builtin.include_tasks: restore.yaml
tags: [restore, never]
- ansible.builtin.include_tasks: deploy.yaml
tags: [deploy]
- ansible.builtin.include_tasks: health_check.yaml
tags: [deploy, healthcheck]
- ansible.builtin.include_tasks: backup.yaml
tags: [backup, never]
- ansible.builtin.include_tasks: update.yaml
tags: [update, never]

14
tasks/manage_compose.yaml Normal file
View File

@@ -0,0 +1,14 @@
---
- name: "{{ app_name }} - Stop Docker Compose stack"
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: stopped
remove_orphans: true
ignore_errors: true
- name: "{{ app_name }} - Start Docker Compose stack"
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
pull: "{{ app_compose_pull | default('policy') }}"
recreate: "{{ app_compose_recreate | default('auto') }}"

120
tasks/restore.yaml Normal file
View File

@@ -0,0 +1,120 @@
---
- name: "{{ app_name }} - Restore application data"
block:
- name: Set specific archive if provided
ansible.builtin.set_fact:
_restore_backup: "{{ app_restore_archive }}"
when: app_restore_archive is defined
- name: Verify specific archive exists (controller)
ansible.builtin.stat:
path: "{{ _restore_backup }}"
delegate_to: localhost
register: specific_archive_stat
when: app_restore_archive is defined and app_restore_source | default('controller') == 'controller'
- name: Verify specific archive exists (remote)
ansible.builtin.stat:
path: "{{ _restore_backup }}"
register: specific_archive_stat
when: app_restore_archive is defined and app_restore_source | default('controller') == 'remote'
- name: Fail if specific archive doesn't exist
ansible.builtin.fail:
msg: "Specified restore archive does not exist: {{ _restore_backup }}"
when: app_restore_archive is defined and not specific_archive_stat.stat.exists
- name: Find latest backup on controller
ansible.builtin.find:
paths: "{{ app_backup_path }}"
patterns: "{{ app_name }}-*.tar.gz"
age: "-{{ app_restore_max_age_days | default(30) }}d"
delegate_to: localhost
register: controller_backups
when: app_restore_source | default('controller') == 'controller' and app_restore_archive is not defined
- name: Find latest backup on remote host
ansible.builtin.find:
paths: "{{ app_backup_path_remote | default(app_dir + '/backups') }}"
patterns: "{{ app_name }}-*.tar.gz"
age: "-{{ app_restore_max_age_days | default(30) }}d"
register: remote_backups
when: app_restore_source | default('controller') == 'remote' and app_restore_archive is not defined
- name: Set latest backup file (controller)
ansible.builtin.set_fact:
_restore_backup: "{{ (controller_backups.files | sort(attribute='mtime') | last).path }}"
when: app_restore_source | default('controller') == 'controller' and controller_backups.files | length > 0 and app_restore_archive is not defined
- name: Set latest backup file (remote)
ansible.builtin.set_fact:
_restore_backup: "{{ (remote_backups.files | sort(attribute='mtime') | last).path }}"
when: app_restore_source | default('controller') == 'remote' and remote_backups.files | length > 0 and app_restore_archive is not defined
- name: Copy backup from controller to remote if needed
ansible.builtin.copy:
src: "{{ _restore_backup }}"
dest: "{{ app_dir }}/restore_backup.tar.gz"
owner: "{{ app_uid | default(omit) }}"
group: "{{ app_gid | default(omit) }}"
mode: "{{ app_permission_mode | default('0644') }}"
when: app_restore_source | default('controller') == 'controller' and _restore_backup is defined
register: copied_backup
- name: Set restore path for remote backup
ansible.builtin.set_fact:
_restore_path: "{{ _restore_backup if app_restore_source | default('controller') == 'remote' else copied_backup.dest }}"
when: _restore_backup is defined
- name: Stop application for restore
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: stopped
when: _restore_path is defined
- name: Debug directory permissions before deletion
ansible.builtin.shell: |
ls -la "{{ item }}"
ls -la "{{ item | dirname }}"
whoami
id
loop: "{{ app_backup_subdirectories | default([]) }}"
when: _restore_path is defined
become: true
register: debug_perms
- name: Show debug output
ansible.builtin.debug:
var: debug_perms
- name: Remove existing backup subdirectories for clean restore
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ app_backup_subdirectories | default([]) }}"
when: _restore_path is defined
become: true
- name: Extract backup to application directory
ansible.builtin.unarchive:
src: "{{ _restore_path }}"
dest: "{{ app_dir }}"
owner: "{{ app_uid | default(omit) }}"
group: "{{ app_gid | default(omit) }}"
remote_src: true
when: _restore_path is defined
- name: Clean up copied backup file
ansible.builtin.file:
path: "{{ app_dir }}/restore_backup.tar.gz"
state: absent
when: app_restore_source | default('controller') == 'controller' and copied_backup is defined
always:
- name: Start application after restore
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
when: _restore_path is defined
tags: [restore]

111
tasks/setup.yaml Normal file
View File

@@ -0,0 +1,111 @@
---
- name: "{{ app_name }} - Validate deployment requirements"
block:
- name: Verify Docker is available
ansible.builtin.command: docker --version
register: docker_check
failed_when: docker_check.rc != 0
changed_when: false
- name: Verify Docker Compose is available
ansible.builtin.command: docker compose version
register: compose_check
failed_when: compose_check.rc != 0
changed_when: false
- name: Validate required variables
ansible.builtin.assert:
that:
- app_name is defined
- app_name | length > 0
- app_name is match('^[a-zA-Z0-9_-]+$')
- host_root_path is defined
- host_root_path | length > 0
- host_root_path is match('^/[a-zA-Z0-9/_-]*$')
fail_msg: "Validation failed for {{ app_name | default('undefined') }}: Required variables missing or contain invalid characters. app_name and host_root_path must be defined and contain only alphanumeric, underscore, hyphen, and slash characters."
- name: Set compose source type
ansible.builtin.set_fact:
_compose_type: >-
{{
'template' if app_compose_template is not none else
'file' if app_compose_file is not none else
'content' if app_compose_content is not none else
'none'
}}
- name: Validate compose source
ansible.builtin.assert:
that:
- _compose_type != 'none'
fail_msg: "No Docker Compose source specified for {{ app_name }}: Must define one of app_compose_template, app_compose_file, or app_compose_content"
- name: Validate directory paths
ansible.builtin.assert:
that:
- app_dir is match('^/[a-zA-Z0-9/_-]*$')
- not (app_dir is match('.*\.\..*'))
fail_msg: "Security validation failed for {{ app_name }}: app_dir '{{ app_dir }}' contains invalid characters or path traversal sequences"
- name: Check if compose template exists
ansible.builtin.stat:
path: "{{ app_compose_template }}"
register: _compose_template_stat
delegate_to: localhost
become: false
when: app_compose_template is defined and app_compose_template | length > 0
- name: Assert compose template exists
ansible.builtin.assert:
that:
- _compose_template_stat.stat.exists
fail_msg: "Template file not found for {{ app_name }}: '{{ app_compose_template }}' does not exist"
when: app_compose_template is defined and app_compose_template | length > 0
- name: Check if compose file exists
ansible.builtin.stat:
path: "{{ app_compose_file }}"
register: _compose_file_stat
delegate_to: localhost
become: false
when: app_compose_file is defined and app_compose_file
- name: Assert compose file exists
ansible.builtin.assert:
that:
- _compose_file_stat.stat.exists
fail_msg: "Docker Compose file not found for {{ app_name }}: '{{ app_compose_file }}' does not exist"
when: app_compose_file is defined and app_compose_file
tags: [setup, deploy]
- name: "{{ app_name }} - Create application directories"
block:
- name: Create application directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
mode: "{{ app_permission_mode }}"
- name: Create application subdirectories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
mode: "{{ app_permission_mode }}"
loop: "{{ app_subdirectories }}"
tags: [setup, deploy]
- name: "{{ app_name }} - Deploy additional templates"
block:
- name: Copy over additional templates
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: "{{ app_uid }}"
group: "{{ app_gid }}"
mode: "{{ app_permission_mode }}"
loop: "{{ app_extra_templates }}"
tags: [setup, deploy]

31
tasks/update.yaml Normal file
View File

@@ -0,0 +1,31 @@
---
- name: "{{ app_name }} - Update application"
block:
- name: Stop application for update
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: stopped
- name: Pull latest container images
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
pull: always
- name: Start application with new images
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
recreate: always
register: update_restart
- name: Verify application health after update
ansible.builtin.include_tasks: health_check.yaml
when: app_health_check | default(true)
rescue:
- name: Update failed, attempt to restart application
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
tags: [update]