Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:47:38 +08:00
commit 18faa0569e
47 changed files with 7969 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
# 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
```yaml
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
```yaml
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
```yaml
---
- 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
```yaml
---
- 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)
```yaml
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
```yaml
- 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
```yaml
# group_vars/secrets.yml (encrypted)
app_secrets:
DB_PASSWORD: supersecret
API_KEY: abc123
JWT_SECRET: longsecret
```
```yaml
# templates/.env.j2
{% for key, value in app_secrets.items() %}
{{ key }}={{ value }}
{% endfor %}
```
### With external secrets
```yaml
- 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
```

View File

@@ -0,0 +1,307 @@
# Docker Deployment with Ansible
Managing Docker containers and compose stacks via Ansible.
## Collection Setup
```bash
ansible-galaxy collection install community.docker
```
## Compose Deployment (Recommended)
### Deploy from local compose file
```yaml
- name: Deploy application stack
hosts: docker_hosts
become: true
tasks:
- name: Create project directory
ansible.builtin.file:
path: /opt/myapp
state: directory
owner: "{{ ansible_user }}"
mode: '0755'
- name: Copy compose file
ansible.builtin.template:
src: docker-compose.yml.j2
dest: /opt/myapp/docker-compose.yml
owner: "{{ ansible_user }}"
mode: '0644'
- name: Copy environment file
ansible.builtin.template:
src: .env.j2
dest: /opt/myapp/.env
owner: "{{ ansible_user }}"
mode: '0600'
- name: Deploy with compose
community.docker.docker_compose_v2:
project_src: /opt/myapp
state: present
pull: always
register: deploy_result
- name: Show deployed services
ansible.builtin.debug:
var: deploy_result.containers
```
### Compose operations
```yaml
# Pull latest images and recreate
- name: Update stack
community.docker.docker_compose_v2:
project_src: /opt/myapp
state: present
pull: always
recreate: always
# Stop stack (keep volumes)
- name: Stop stack
community.docker.docker_compose_v2:
project_src: /opt/myapp
state: stopped
# Remove stack
- name: Remove stack
community.docker.docker_compose_v2:
project_src: /opt/myapp
state: absent
remove_volumes: false # Keep data volumes
```
## Container Deployment (Individual)
### Run container
```yaml
- name: Run nginx container
community.docker.docker_container:
name: nginx
image: nginx:1.25
state: started
restart_policy: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /opt/nginx/html:/usr/share/nginx/html:ro
- /opt/nginx/conf.d:/etc/nginx/conf.d:ro
env:
TZ: "America/Los_Angeles"
labels:
app: web
env: production
- name: Run database
community.docker.docker_container:
name: postgres
image: postgres:15
state: started
restart_policy: unless-stopped
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
env:
POSTGRES_USER: "{{ db_user }}"
POSTGRES_PASSWORD: "{{ db_password }}"
POSTGRES_DB: "{{ db_name }}"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ db_user }}"]
interval: 10s
timeout: 5s
retries: 5
```
### Container lifecycle
```yaml
# Stop container
- name: Stop container
community.docker.docker_container:
name: myapp
state: stopped
# Restart container
- name: Restart container
community.docker.docker_container:
name: myapp
state: started
restart: true
# Remove container
- name: Remove container
community.docker.docker_container:
name: myapp
state: absent
# Force recreate
- name: Recreate container
community.docker.docker_container:
name: myapp
image: myapp:latest
state: started
recreate: true
```
## Image Management
```yaml
# Pull image
- name: Pull latest image
community.docker.docker_image:
name: myapp
tag: latest
source: pull
force_source: true # Always check for updates
# Build from Dockerfile
- name: Build image
community.docker.docker_image:
name: myapp
tag: "{{ version }}"
source: build
build:
path: /opt/myapp
dockerfile: Dockerfile
pull: true # Pull base image updates
# Remove image
- name: Remove old images
community.docker.docker_image:
name: myapp
tag: old
state: absent
```
## Network Management
```yaml
# Create network
- name: Create app network
community.docker.docker_network:
name: app_network
driver: bridge
ipam_config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
# Create macvlan network
- name: Create macvlan network
community.docker.docker_network:
name: lan
driver: macvlan
driver_options:
parent: eth0
ipam_config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1
# Attach container to network
- name: Run container on network
community.docker.docker_container:
name: myapp
image: myapp:latest
networks:
- name: app_network
ipv4_address: 172.20.0.10
```
## Volume Management
```yaml
# Create named volume
- name: Create data volume
community.docker.docker_volume:
name: app_data
driver: local
# Create volume with options
- name: Create NFS volume
community.docker.docker_volume:
name: shared_data
driver: local
driver_options:
type: nfs
device: ":/exports/data"
o: "addr=192.168.1.10,rw"
# Backup volume
- name: Backup volume
community.docker.docker_container:
name: backup
image: alpine
command: tar czf /backup/data.tar.gz /data
volumes:
- app_data:/data:ro
- /opt/backups:/backup
auto_remove: true
```
## Common Patterns
### Wait for service health
```yaml
- name: Deploy database
community.docker.docker_container:
name: postgres
image: postgres:15
# ... config ...
- name: Wait for database
community.docker.docker_container_info:
name: postgres
register: db_info
until: db_info.container.State.Health.Status == "healthy"
retries: 30
delay: 2
```
### Rolling update
```yaml
- name: Pull new image
community.docker.docker_image:
name: myapp
tag: "{{ new_version }}"
source: pull
- name: Update container
community.docker.docker_container:
name: myapp
image: "myapp:{{ new_version }}"
state: started
recreate: true
restart_policy: unless-stopped
```
### Cleanup
```yaml
- name: Remove stopped containers
community.docker.docker_prune:
containers: true
containers_filters:
status: exited
- name: Remove unused images
community.docker.docker_prune:
images: true
images_filters:
dangling: true
- name: Full cleanup (careful!)
community.docker.docker_prune:
containers: true
images: true
networks: true
volumes: false # Don't remove data!
builder_cache: true
```

View File

@@ -0,0 +1,292 @@
# Ansible Docker Troubleshooting
Common issues and debugging patterns.
## Module Issues
### "Could not find docker-compose"
```yaml
# docker_compose_v2 requires Docker Compose V2 (plugin)
# NOT standalone docker-compose binary
# Check on target host:
# docker compose version # V2 (plugin)
# docker-compose version # V1 (standalone) - won't work
```
Fix: Install Docker Compose V2:
```yaml
- name: Install Docker Compose plugin
ansible.builtin.apt:
name: docker-compose-plugin
state: present
```
### "Permission denied"
```yaml
# User not in docker group
- name: Add user to docker group
ansible.builtin.user:
name: "{{ ansible_user }}"
groups: docker
append: true
become: true
# Then reconnect or use become
- name: Run with become
community.docker.docker_container:
name: myapp
# ...
become: true
```
### "Cannot connect to Docker daemon"
```yaml
# Docker not running
- name: Ensure Docker is running
ansible.builtin.service:
name: docker
state: started
enabled: true
become: true
# Socket permission issue
# Add become: true to docker tasks
```
## Container Issues
### Get container logs
```yaml
- name: Get logs
community.docker.docker_container_exec:
container: myapp
command: cat /var/log/app.log
register: logs
ignore_errors: true
- name: Alternative - docker logs
ansible.builtin.command: docker logs --tail 100 myapp
register: docker_logs
changed_when: false
- name: Show logs
ansible.builtin.debug:
var: docker_logs.stdout_lines
```
### Container keeps restarting
```yaml
- name: Get container info
community.docker.docker_container_info:
name: myapp
register: container_info
- name: Show restart count
ansible.builtin.debug:
msg: "Restart count: {{ container_info.container.RestartCount }}"
- name: Show last exit code
ansible.builtin.debug:
msg: "Exit code: {{ container_info.container.State.ExitCode }}"
- name: Get logs from dead container
ansible.builtin.command: docker logs myapp
register: crash_logs
changed_when: false
- name: Show crash logs
ansible.builtin.debug:
var: crash_logs.stderr_lines
```
### Health check failing
```yaml
- name: Check health status
community.docker.docker_container_info:
name: myapp
register: info
- name: Show health
ansible.builtin.debug:
msg: |
Status: {{ info.container.State.Health.Status }}
Failing: {{ info.container.State.Health.FailingStreak }}
Log: {{ info.container.State.Health.Log | last }}
# Manual health check
- name: Test health endpoint
ansible.builtin.command: >
docker exec myapp curl -f http://localhost:8080/health
register: health
ignore_errors: true
changed_when: false
```
## Network Issues
### Container can't reach external network
```yaml
- name: Test DNS from container
ansible.builtin.command: docker exec myapp nslookup google.com
register: dns_test
changed_when: false
ignore_errors: true
- name: Test connectivity
ansible.builtin.command: docker exec myapp ping -c 1 8.8.8.8
register: ping_test
changed_when: false
ignore_errors: true
# Check iptables
- name: Check IP forwarding
ansible.builtin.command: sysctl net.ipv4.ip_forward
register: ip_forward
changed_when: false
- name: Enable IP forwarding
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
state: present
become: true
when: "'0' in ip_forward.stdout"
```
### Containers can't communicate
```yaml
- name: List networks
community.docker.docker_network_info:
name: "{{ network_name }}"
register: network_info
- name: Show connected containers
ansible.builtin.debug:
var: network_info.network.Containers
# Verify both containers on same network
- name: Test inter-container connectivity
ansible.builtin.command: >
docker exec app ping -c 1 db
register: ping_result
changed_when: false
```
## Compose Issues
### Services not starting in order
```yaml
# depends_on only waits for container start, not readiness
# Use healthcheck + condition
# In compose template:
services:
app:
depends_on:
db:
condition: service_healthy # Wait for health check
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5
```
### Orphaned containers
```yaml
# Containers from old compose runs
- name: Remove orphans
community.docker.docker_compose_v2:
project_src: /opt/myapp
state: present
remove_orphans: true
```
### Volume data not persisting
```yaml
# Check volume exists
- name: List volumes
ansible.builtin.command: docker volume ls
register: volumes
changed_when: false
# Check volume contents
- name: Inspect volume
ansible.builtin.command: docker volume inspect myapp_data
register: volume_info
changed_when: false
- name: Show volume mountpoint
ansible.builtin.debug:
msg: "{{ (volume_info.stdout | from_json)[0].Mountpoint }}"
```
## Debug Playbook
```yaml
---
- name: Docker debug
hosts: docker_hosts
tasks:
- name: Docker version
ansible.builtin.command: docker version
register: docker_version
changed_when: false
- name: Compose version
ansible.builtin.command: docker compose version
register: compose_version
changed_when: false
- name: List containers
ansible.builtin.command: docker ps -a
register: containers
changed_when: false
- name: List images
ansible.builtin.command: docker images
register: images
changed_when: false
- name: Disk usage
ansible.builtin.command: docker system df
register: disk
changed_when: false
- name: Show all
ansible.builtin.debug:
msg: |
Docker: {{ docker_version.stdout_lines[0] }}
Compose: {{ compose_version.stdout }}
Containers:
{{ containers.stdout }}
Images:
{{ images.stdout }}
Disk:
{{ disk.stdout }}
```
## Common Error Reference
| Error | Cause | Fix |
|-------|-------|-----|
| `docker.errors.DockerException` | Docker not running | Start docker service |
| `docker.errors.APIError: 404` | Container/image not found | Check name/tag |
| `docker.errors.APIError: 409` | Container name conflict | Remove or rename |
| `PermissionError` | Not in docker group | Add user or use become |
| `requests.exceptions.ConnectionError` | Docker socket inaccessible | Check socket permissions |
| `FileNotFoundError: docker-compose` | V1 compose not installed | Use docker_compose_v2 |