Docker a révolutionné la manière dont nous déployons et gérons les applications. Mais lorsqu'on multiplie les serveurs et les environnements, le déploiement manuel de conteneurs devient rapidement ingérable. C'est là qu'Ansible entre en jeu : en combinant la puissance de Docker avec l'automatisation d'Ansible, vous obtenez une infrastructure conteneurisée reproductible, versionnable et déployable en un clic.
Dans cet article, nous allons explorer en profondeur comment utiliser Ansible pour installer Docker, gérer les images, conteneurs, réseaux et volumes, puis déployer une stack complète de production comprenant Traefik, une application web, PostgreSQL et Redis. Nous aborderons également la gestion des registries privés, la maintenance automatisée et les avantages par rapport à un docker-compose classique.
Architecture en un coup d'œil

Prérequis et installation de la collection community.docker
Avant de pouvoir gérer Docker avec Ansible, il faut installer la collection community.docker. Cette collection fournit tous les modules nécessaires pour interagir avec le daemon Docker : gestion des conteneurs, images, réseaux, volumes et Docker Compose.
Installation de la collection
# Installer la collection community.docker
ansible-galaxy collection install community.docker
# Vérifier l'installation
ansible-galaxy collection list | grep docker
# Pour un projet, créez un fichier requirements.yml
cat > requirements.yml <<EOF
---
collections:
- name: community.docker
version: ">=3.8.0"
- name: community.general
version: ">=8.0.0"
EOF
# Installer depuis le fichier requirements
ansible-galaxy collection install -r requirements.ymlPrérequis Python sur les hôtes cibles
Les modules community.docker nécessitent le SDK Python Docker sur les machines cibles. Ce prérequis sera géré automatiquement dans notre playbook d'installation, mais il est important de le connaître :
- docker (Python package) : version 7.0.0 ou supérieure
- docker-compose (Python package) : pour le module
docker_compose_v2, la CLI Docker Compose v2 doit être installée - Python 3.8+ sur les machines cibles
Structure du projet
docker-ansible/
├── inventory/
│ ├── hosts.yml
│ └── group_vars/
│ └── docker_hosts.yml
├── roles/
│ ├── docker-install/
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ └── handlers/
│ │ └── main.yml
│ ├── docker-stack/
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ ├── templates/
│ │ │ ├── docker-compose.yml.j2
│ │ │ ├── traefik.yml.j2
│ │ │ └── .env.j2
│ │ └── handlers/
│ │ └── main.yml
│ └── docker-maintenance/
│ └── tasks/
│ └── main.yml
├── requirements.yml
└── site.ymlInstaller Docker via Ansible : playbook complet
L'installation de Docker via les paquets officiels nécessite plusieurs étapes : ajout du dépôt GPG, configuration des sources APT, installation du moteur et de ses plugins. Notre rôle Ansible automatise tout cela de manière idempotente.
Variables de configuration
# inventory/group_vars/docker_hosts.yml
---
# Configuration Docker
docker_edition: "ce"
docker_version: "" # Vide = dernière version stable
docker_compose_version: "v2.24.5"
docker_users:
- deployer
# Configuration réseau Docker
docker_daemon_config:
log-driver: "json-file"
log-opts:
max-size: "50m"
max-file: "3"
default-address-pools:
- base: "172.20.0.0/16"
size: 24
storage-driver: "overlay2"
live-restore: true
userland-proxy: false
experimental: false
metrics-addr: "127.0.0.1:9323"
# Configuration de la stack applicative
app_name: "monapp"
app_domain: "app.codeclan.fr"
app_image: "registry.codeclan.fr/monapp:latest"
app_port: 8000
app_replicas: 2
# Traefik
traefik_dashboard_domain: "traefik.codeclan.fr"
traefik_acme_email: "admin@codeclan.fr"
traefik_dashboard_auth: "admin:$apr1$xyz123$hashedpassword"
# PostgreSQL
postgres_version: "16"
postgres_db: "monapp_db"
postgres_user: "monapp_user"
postgres_password: "{{ vault_postgres_password }}"
postgres_max_connections: 100
# Redis
redis_version: "7-alpine"
redis_password: "{{ vault_redis_password }}"
redis_maxmemory: "256mb"
# Registry privé
docker_registry_url: "registry.codeclan.fr"
docker_registry_username: "{{ vault_registry_username }}"
docker_registry_password: "{{ vault_registry_password }}"
# Maintenance
docker_prune_schedule: "0 3 * * 0" # Dimanche à 3h
docker_image_update_schedule: "0 4 * * 1" # Lundi à 4hRôle docker-install
# roles/docker-install/tasks/main.yml
---
- name: Supprimer les anciennes versions de Docker
ansible.builtin.apt:
name:
- docker
- docker-engine
- docker.io
- containerd
- runc
state: absent
- name: Installer les prérequis système
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- python3-pip
- python3-setuptools
- python3-venv
state: present
update_cache: yes
- name: Créer le répertoire pour la clé GPG Docker
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
mode: "0755"
- name: Ajouter la clé GPG officielle Docker
ansible.builtin.get_url:
url: "https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg"
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
- name: Ajouter le dépôt Docker APT
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ docker_arch }} signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/{{ ansible_distribution | lower }}
{{ ansible_distribution_release }} stable
state: present
filename: docker
vars:
docker_arch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"
- name: Installer Docker Engine et ses composants
ansible.builtin.apt:
name:
- "docker-{{ docker_edition }}"
- "docker-{{ docker_edition }}-cli"
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: yes
notify: Redémarrer Docker
- name: Installer le SDK Python Docker
ansible.builtin.pip:
name:
- docker>=7.0.0
- docker-compose
state: present
- name: Ajouter les utilisateurs au groupe docker
ansible.builtin.user:
name: "{{ item }}"
groups: docker
append: yes
loop: "{{ docker_users }}"
- name: Créer le répertoire de configuration Docker
ansible.builtin.file:
path: /etc/docker
state: directory
owner: root
group: root
mode: "0755"
- name: Configurer le daemon Docker
ansible.builtin.copy:
content: "{{ docker_daemon_config | to_nice_json }}"
dest: /etc/docker/daemon.json
owner: root
group: root
mode: "0644"
notify: Redémarrer Docker
- name: Activer et démarrer Docker
ansible.builtin.systemd:
name: docker
state: started
enabled: yes
- name: Activer le service containerd
ansible.builtin.systemd:
name: containerd
state: started
enabled: yes
- name: Vérifier l'installation de Docker
ansible.builtin.command:
cmd: docker version
register: docker_version_output
changed_when: false
- name: Afficher la version de Docker
ansible.builtin.debug:
msg: "{{ docker_version_output.stdout_lines[:6] }}"
- name: Vérifier Docker Compose
ansible.builtin.command:
cmd: docker compose version
register: compose_version_output
changed_when: false
- name: Afficher la version de Docker Compose
ansible.builtin.debug:
msg: "{{ compose_version_output.stdout }}"Handlers docker-install
# roles/docker-install/handlers/main.yml
---
- name: Redémarrer Docker
ansible.builtin.systemd:
name: docker
state: restarted
daemon_reload: yesLes modules essentiels de community.docker
La collection community.docker fournit une dizaine de modules. Découvrons les plus importants avec des exemples concrets et détaillés.
docker_image : gérer les images
Le module docker_image permet de pull, build et supprimer des images Docker.
# Exemples d'utilisation de docker_image
- name: Télécharger une image depuis Docker Hub
community.docker.docker_image:
name: nginx
tag: "1.25-alpine"
source: pull
- name: Construire une image depuis un Dockerfile
community.docker.docker_image:
name: "{{ docker_registry_url }}/{{ app_name }}"
tag: "{{ app_version | default('latest') }}"
source: build
build:
path: /opt/app
dockerfile: Dockerfile
pull: yes
args:
BUILD_ENV: production
push: yes
- name: Supprimer une image obsolète
community.docker.docker_image:
name: "old-app"
tag: "v1.0"
state: absent
- name: Télécharger plusieurs images en boucle
community.docker.docker_image:
name: "{{ item.name }}"
tag: "{{ item.tag }}"
source: pull
loop:
- { name: "postgres", tag: "16-alpine" }
- { name: "redis", tag: "7-alpine" }
- { name: "traefik", tag: "v3.0" }
loop_control:
label: "{{ item.name }}:{{ item.tag }}"docker_network : gérer les réseaux
Les réseaux Docker permettent d'isoler les communications entre conteneurs. Le module docker_network gère leur cycle de vie complet.
# Exemples d'utilisation de docker_network
- name: Créer le réseau frontend (accessible depuis Traefik)
community.docker.docker_network:
name: frontend
driver: bridge
ipam_config:
- subnet: 172.20.1.0/24
gateway: 172.20.1.1
state: present
- name: Créer le réseau backend (interne uniquement)
community.docker.docker_network:
name: backend
driver: bridge
internal: yes
ipam_config:
- subnet: 172.20.2.0/24
state: present
- name: Créer un réseau avec des labels
community.docker.docker_network:
name: monitoring
driver: bridge
labels:
environment: production
managed_by: ansible
state: presentdocker_volume : gérer les volumes
Les volumes Docker permettent de persister les données en dehors du cycle de vie des conteneurs. Ce module gère leur création et leur configuration.
# Exemples d'utilisation de docker_volume
- name: Créer le volume pour PostgreSQL
community.docker.docker_volume:
name: postgres_data
driver: local
driver_options:
type: none
o: bind
device: /opt/docker/data/postgres
labels:
service: postgresql
backup: "true"
state: present
- name: Créer le volume pour Redis
community.docker.docker_volume:
name: redis_data
driver: local
state: present
- name: Créer le volume pour les certificats TLS
community.docker.docker_volume:
name: traefik_certs
driver: local
state: present
- name: Créer les répertoires hôtes pour les bind mounts
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "1000"
group: "1000"
mode: "0755"
loop:
- /opt/docker/data/postgres
- /opt/docker/data/redis
- /opt/docker/config/traefik
- /opt/docker/logsdocker_container : gérer les conteneurs
Le module docker_container est le plus utilisé. Il permet de créer, démarrer, arrêter et supprimer des conteneurs avec un contrôle fin de tous les paramètres.
# Exemples d'utilisation de docker_container
- name: Déployer un conteneur PostgreSQL
community.docker.docker_container:
name: postgres
image: "postgres:{{ postgres_version }}-alpine"
state: started
restart_policy: unless-stopped
env:
POSTGRES_DB: "{{ postgres_db }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ postgres_password }}"
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
volumes:
- postgres_data:/var/lib/postgresql/data
- /opt/docker/config/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- name: backend
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }} -d {{ postgres_db }}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
memory: "512m"
memory_swap: "1g"
cpu_period: 100000
cpu_quota: 50000
labels:
service: postgresql
managed_by: ansible
- name: Déployer un conteneur Redis
community.docker.docker_container:
name: redis
image: "redis:{{ redis_version }}"
state: started
restart_policy: unless-stopped
command: >
redis-server
--requirepass {{ redis_password }}
--maxmemory {{ redis_maxmemory }}
--maxmemory-policy allkeys-lru
--appendonly yes
volumes:
- redis_data:/data
networks:
- name: backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "{{ redis_password }}", "ping"]
interval: 10s
timeout: 5s
retries: 5
memory: "512m"
labels:
service: redis
managed_by: ansible
- name: Arrêter un conteneur proprement
community.docker.docker_container:
name: old-service
state: stopped
stop_timeout: 30
- name: Supprimer un conteneur
community.docker.docker_container:
name: old-service
state: absentdocker_compose_v2 : orchestration multi-conteneurs
Le module docker_compose_v2 permet d'utiliser Docker Compose directement depuis Ansible. C'est la méthode recommandée pour déployer des stacks complexes avec plusieurs services interdépendants.
# Utilisation de docker_compose_v2
- name: Déployer la stack via Docker Compose
community.docker.docker_compose_v2:
project_src: /opt/docker/{{ app_name }}
state: present
pull: always
build: never
remove_orphans: yes
register: compose_result
- name: Afficher les services déployés
ansible.builtin.debug:
msg: "{{ compose_result }}"
- name: Arrêter la stack
community.docker.docker_compose_v2:
project_src: /opt/docker/{{ app_name }}
state: absent
remove_volumes: noGérer le cycle de vie des conteneurs
La gestion du cycle de vie des conteneurs est un aspect crucial du déploiement Docker en production. Ansible permet d'orchestrer les mises à jour avec des stratégies de déploiement sophistiquées.
Rolling update
# Mise à jour progressive des conteneurs
- name: Télécharger la nouvelle version de l'image
community.docker.docker_image:
name: "{{ app_image }}"
source: pull
force_source: yes
register: image_pull
- name: Recréer le conteneur si l'image a changé
community.docker.docker_container:
name: "{{ app_name }}"
image: "{{ app_image }}"
state: started
restart_policy: unless-stopped
recreate: "{{ image_pull.changed }}"
env:
DATABASE_URL: "postgresql://{{ postgres_user }}:{{ postgres_password }}@postgres:5432/{{ postgres_db }}"
REDIS_URL: "redis://:{{ redis_password }}@redis:6379/0"
networks:
- name: frontend
- name: backend
labels:
traefik.enable: "true"
traefik.http.routers.app.rule: "Host(`{{ app_domain }}`)"
traefik.http.routers.app.tls.certresolver: "letsencrypt"
traefik.http.services.app.loadbalancer.server.port: "{{ app_port }}"Health checks et monitoring
# Vérification de la santé des conteneurs
- name: Vérifier l'état de santé de tous les conteneurs
community.docker.docker_container_info:
name: "{{ item }}"
register: container_info
loop:
- postgres
- redis
- "{{ app_name }}"
- traefik
- name: Alerter si un conteneur n'est pas healthy
ansible.builtin.debug:
msg: >
ATTENTION : Le conteneur {{ item.item }} est en état
{{ item.container.State.Health.Status | default('unknown') }}
when: >
item.container.State.Health is defined and
item.container.State.Health.Status != 'healthy'
loop: "{{ container_info.results }}"
loop_control:
label: "{{ item.item }}"
- name: Récupérer les logs d'un conteneur en erreur
community.docker.docker_container_info:
name: "{{ app_name }}"
register: app_info
- name: Afficher les logs récents si le conteneur redémarre
ansible.builtin.command:
cmd: "docker logs --tail 50 {{ app_name }}"
when: app_info.container.RestartCount | int > 0
register: app_logs
changed_when: falseDéployer une stack complète : Traefik + App + PostgreSQL + Redis
Passons maintenant au déploiement concret d'une stack de production complète. Cette stack comprend Traefik comme reverse proxy avec terminaison TLS automatique, une application web, PostgreSQL comme base de données et Redis comme cache.
Architecture de la stack
Notre architecture utilise deux réseaux Docker isolés :
- frontend : connecte Traefik à l'application (accessible depuis l'extérieur)
- backend : connecte l'application à PostgreSQL et Redis (réseau interne, isolé)
Cette séparation garantit que les bases de données ne sont jamais exposées directement à internet.
Rôle docker-stack
# roles/docker-stack/tasks/main.yml
---
- name: Créer les répertoires de la stack
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: docker
mode: "0750"
loop:
- "/opt/docker/{{ app_name }}"
- "/opt/docker/{{ app_name }}/config"
- "/opt/docker/{{ app_name }}/config/traefik"
- "/opt/docker/{{ app_name }}/config/traefik/dynamic"
- "/opt/docker/{{ app_name }}/data"
- "/opt/docker/{{ app_name }}/data/postgres"
- "/opt/docker/{{ app_name }}/data/redis"
- "/opt/docker/{{ app_name }}/data/traefik"
- "/opt/docker/{{ app_name }}/logs"
- "/opt/docker/{{ app_name }}/backups"
# -------------------------
# Réseaux Docker
# -------------------------
- name: Créer le réseau frontend
community.docker.docker_network:
name: "{{ app_name }}_frontend"
driver: bridge
state: present
- name: Créer le réseau backend (interne)
community.docker.docker_network:
name: "{{ app_name }}_backend"
driver: bridge
internal: yes
state: present
# -------------------------
# Volumes Docker
# -------------------------
- name: Créer les volumes persistants
community.docker.docker_volume:
name: "{{ item }}"
state: present
loop:
- "{{ app_name }}_postgres_data"
- "{{ app_name }}_redis_data"
- "{{ app_name }}_traefik_certs"
# -------------------------
# Configuration Traefik
# -------------------------
- name: Déployer la configuration statique de Traefik
ansible.builtin.template:
src: traefik.yml.j2
dest: "/opt/docker/{{ app_name }}/config/traefik/traefik.yml"
owner: root
group: docker
mode: "0640"
notify: Redémarrer Traefik
- name: Déployer la configuration dynamique de Traefik
ansible.builtin.copy:
content: |
# Ansible managed
http:
middlewares:
security-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"
customResponseHeaders:
X-Powered-By: ""
Server: ""
rate-limit:
rateLimit:
average: 100
burst: 50
period: 1s
compress:
compress:
excludedContentTypes:
- text/event-stream
dest: "/opt/docker/{{ app_name }}/config/traefik/dynamic/middlewares.yml"
owner: root
group: docker
mode: "0640"
notify: Redémarrer Traefik
# -------------------------
# Déploiement des conteneurs
# -------------------------
- name: Déployer Traefik (reverse proxy)
community.docker.docker_container:
name: "{{ app_name }}_traefik"
image: "traefik:v3.0"
state: started
restart_policy: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "/opt/docker/{{ app_name }}/config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro"
- "/opt/docker/{{ app_name }}/config/traefik/dynamic:/etc/traefik/dynamic:ro"
- "{{ app_name }}_traefik_certs:/letsencrypt"
- "/opt/docker/{{ app_name }}/logs/traefik:/var/log/traefik"
networks:
- name: "{{ app_name }}_frontend"
labels:
traefik.enable: "true"
traefik.http.routers.dashboard.rule: "Host(`{{ traefik_dashboard_domain }}`)"
traefik.http.routers.dashboard.service: "api@internal"
traefik.http.routers.dashboard.tls.certresolver: "letsencrypt"
traefik.http.routers.dashboard.middlewares: "dashboard-auth"
traefik.http.middlewares.dashboard-auth.basicauth.users: "{{ traefik_dashboard_auth }}"
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 5s
retries: 3
memory: "256m"
- name: Déployer PostgreSQL
community.docker.docker_container:
name: "{{ app_name }}_postgres"
image: "postgres:{{ postgres_version }}-alpine"
state: started
restart_policy: unless-stopped
env:
POSTGRES_DB: "{{ postgres_db }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ postgres_password }}"
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --encoding=UTF8 --locale=fr_FR.UTF-8"
volumes:
- "{{ app_name }}_postgres_data:/var/lib/postgresql/data"
networks:
- name: "{{ app_name }}_backend"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }} -d {{ postgres_db }}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
memory: "1g"
memory_swap: "2g"
shm_size: "256m"
labels:
managed_by: ansible
service: postgresql
backup: "true"
- name: Déployer Redis
community.docker.docker_container:
name: "{{ app_name }}_redis"
image: "redis:{{ redis_version }}"
state: started
restart_policy: unless-stopped
command: >
redis-server
--requirepass {{ redis_password }}
--maxmemory {{ redis_maxmemory }}
--maxmemory-policy allkeys-lru
--appendonly yes
--appendfsync everysec
--save 900 1
--save 300 10
--save 60 10000
volumes:
- "{{ app_name }}_redis_data:/data"
networks:
- name: "{{ app_name }}_backend"
healthcheck:
test: ["CMD", "redis-cli", "-a", "{{ redis_password }}", "ping"]
interval: 10s
timeout: 5s
retries: 5
memory: "512m"
labels:
managed_by: ansible
service: redis
- name: Attendre que PostgreSQL soit prêt
community.docker.docker_container_info:
name: "{{ app_name }}_postgres"
register: pg_info
until: pg_info.container.State.Health.Status == "healthy"
retries: 30
delay: 5
- name: Attendre que Redis soit prêt
community.docker.docker_container_info:
name: "{{ app_name }}_redis"
register: redis_info
until: redis_info.container.State.Health.Status == "healthy"
retries: 15
delay: 3
- name: Se connecter au registry privé
community.docker.docker_login:
registry_url: "{{ docker_registry_url }}"
username: "{{ docker_registry_username }}"
password: "{{ docker_registry_password }}"
reauthorize: yes
when: docker_registry_url is defined
- name: Télécharger l'image de l'application
community.docker.docker_image:
name: "{{ app_image }}"
source: pull
force_source: yes
register: app_image_pull
- name: Déployer l'application
community.docker.docker_container:
name: "{{ app_name }}_app"
image: "{{ app_image }}"
state: started
restart_policy: unless-stopped
env:
DATABASE_URL: "postgresql://{{ postgres_user }}:{{ postgres_password }}@{{ app_name }}_postgres:5432/{{ postgres_db }}"
REDIS_URL: "redis://:{{ redis_password }}@{{ app_name }}_redis:6379/0"
APP_ENV: "production"
APP_DEBUG: "false"
APP_PORT: "{{ app_port | string }}"
volumes:
- "/opt/docker/{{ app_name }}/logs/app:/app/logs"
networks:
- name: "{{ app_name }}_frontend"
- name: "{{ app_name }}_backend"
labels:
traefik.enable: "true"
traefik.http.routers.app.rule: "Host(`{{ app_domain }}`)"
traefik.http.routers.app.tls.certresolver: "letsencrypt"
traefik.http.routers.app.middlewares: "security-headers@file,compress@file,rate-limit@file"
traefik.http.services.app.loadbalancer.server.port: "{{ app_port | string }}"
traefik.docker.network: "{{ app_name }}_frontend"
managed_by: ansible
service: app
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:{{ app_port }}/health"]
interval: 15s
timeout: 10s
retries: 5
start_period: 45s
memory: "512m"
memory_swap: "1g"
- name: Vérifier que l'application est healthy
community.docker.docker_container_info:
name: "{{ app_name }}_app"
register: app_info
until: app_info.container.State.Health.Status == "healthy"
retries: 30
delay: 10Template Traefik
# roles/docker-stack/templates/traefik.yml.j2
# Ansible managed - ne pas modifier manuellement
# Configuration statique de Traefik
api:
dashboard: true
insecure: false
ping:
entryPoint: traefik
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
traefik:
address: ":8080"
certificatesResolvers:
letsencrypt:
acme:
email: "{{ traefik_acme_email }}"
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: "{{ app_name }}_frontend"
watch: true
file:
directory: /etc/traefik/dynamic
watch: true
log:
level: INFO
filePath: /var/log/traefik/traefik.log
accessLog:
filePath: /var/log/traefik/access.log
bufferingSize: 100
filters:
statusCodes:
- "400-499"
- "500-599"Handlers docker-stack
# roles/docker-stack/handlers/main.yml
---
- name: Redémarrer Traefik
community.docker.docker_container:
name: "{{ app_name }}_traefik"
state: started
restart: yesGestion des registries privés
En entreprise, vous utiliserez souvent un registry Docker privé pour stocker vos images. Ansible gère parfaitement l'authentification et les interactions avec ces registries.
# Gestion complète des registries privés
- name: Se connecter au registry privé
community.docker.docker_login:
registry_url: "{{ docker_registry_url }}"
username: "{{ docker_registry_username }}"
password: "{{ docker_registry_password }}"
reauthorize: yes
- name: Construire et pousser une image vers le registry
community.docker.docker_image:
name: "{{ docker_registry_url }}/{{ app_name }}"
tag: "{{ lookup('pipe', 'git rev-parse --short HEAD') }}"
source: build
build:
path: /opt/app
pull: yes
nocache: no
push: yes
- name: Taguer l'image comme latest
community.docker.docker_image:
name: "{{ docker_registry_url }}/{{ app_name }}"
tag: "{{ lookup('pipe', 'git rev-parse --short HEAD') }}"
repository: "{{ docker_registry_url }}/{{ app_name }}"
push: yes
source: local
- name: Se déconnecter du registry
community.docker.docker_login:
registry_url: "{{ docker_registry_url }}"
state: absentConseil : Utilisez toujours Ansible Vault pour stocker les identifiants de votre registry privé. Ne stockez jamais de mots de passe en clair dans vos fichiers de variables.
Maintenance Docker automatisée
La maintenance régulière est essentielle pour éviter l'accumulation d'images, conteneurs et volumes inutilisés qui gaspillent de l'espace disque.
Rôle docker-maintenance
# roles/docker-maintenance/tasks/main.yml
---
# -------------------------
# Nettoyage Docker (prune)
# -------------------------
- name: Déployer le script de maintenance Docker
ansible.builtin.copy:
content: |
#!/bin/bash
# Ansible managed - Script de maintenance Docker
set -euo pipefail
echo "=== Maintenance Docker - $(date) ==="
# Supprimer les conteneurs arrêtés
echo "Nettoyage des conteneurs arrêtés..."
docker container prune -f
# Supprimer les images non utilisées (dangling)
echo "Nettoyage des images dangling..."
docker image prune -f
# Supprimer les images non référencées de plus de 24h
echo "Nettoyage des images non utilisées..."
docker image prune -a -f --filter "until=24h"
# Supprimer les volumes orphelins
echo "Nettoyage des volumes orphelins..."
docker volume prune -f
# Supprimer les réseaux non utilisés
echo "Nettoyage des réseaux non utilisés..."
docker network prune -f
# Supprimer le cache de build
echo "Nettoyage du cache de build..."
docker builder prune -f --keep-storage=5GB
# Afficher l'espace récupéré
echo "=== Utilisation disque Docker ==="
docker system df
echo "=== Maintenance terminée - $(date) ==="
dest: /usr/local/bin/docker-maintenance.sh
owner: root
group: root
mode: "0755"
- name: Planifier la maintenance Docker via cron
ansible.builtin.cron:
name: "Maintenance Docker hebdomadaire"
job: "/usr/local/bin/docker-maintenance.sh >> /var/log/docker-maintenance.log 2>&1"
minute: "{{ docker_prune_schedule.split(' ')[0] }}"
hour: "{{ docker_prune_schedule.split(' ')[1] }}"
day: "{{ docker_prune_schedule.split(' ')[2] }}"
month: "{{ docker_prune_schedule.split(' ')[3] }}"
weekday: "{{ docker_prune_schedule.split(' ')[4] }}"
user: root
# -------------------------
# Mise à jour des images
# -------------------------
- name: Déployer le script de mise à jour des images
ansible.builtin.copy:
content: |
#!/bin/bash
# Ansible managed - Script de mise à jour des images Docker
set -euo pipefail
echo "=== Mise à jour des images Docker - $(date) ==="
# Lister toutes les images en cours d'utilisation
IMAGES=$(docker ps --format '{{ '{{' }}.Image{{ '}}' }}' | sort -u)
for IMAGE in $IMAGES; do
echo "Vérification de $IMAGE..."
docker pull "$IMAGE" 2>/dev/null && echo " -> $IMAGE mis à jour" || echo " -> $IMAGE inchangé"
done
echo "=== Mise à jour terminée - $(date) ==="
echo "NOTE: Redémarrez les conteneurs pour appliquer les nouvelles images"
dest: /usr/local/bin/docker-update-images.sh
owner: root
group: root
mode: "0755"
- name: Planifier la vérification des mises à jour
ansible.builtin.cron:
name: "Vérification des mises à jour Docker"
job: "/usr/local/bin/docker-update-images.sh >> /var/log/docker-updates.log 2>&1"
minute: "{{ docker_image_update_schedule.split(' ')[0] }}"
hour: "{{ docker_image_update_schedule.split(' ')[1] }}"
day: "{{ docker_image_update_schedule.split(' ')[2] }}"
month: "{{ docker_image_update_schedule.split(' ')[3] }}"
weekday: "{{ docker_image_update_schedule.split(' ')[4] }}"
user: root
# -------------------------
# Sauvegarde des données Docker
# -------------------------
- name: Déployer le script de sauvegarde PostgreSQL
ansible.builtin.copy:
content: |
#!/bin/bash
# Ansible managed - Sauvegarde PostgreSQL
set -euo pipefail
BACKUP_DIR="/opt/docker/{{ app_name }}/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=14
echo "Sauvegarde PostgreSQL - $(date)"
docker exec {{ app_name }}_postgres pg_dumpall \
-U {{ postgres_user }} \
--clean \
| gzip > "${BACKUP_DIR}/postgres_${DATE}.sql.gz"
echo "Sauvegarde créée : postgres_${DATE}.sql.gz"
echo "Taille : $(du -h ${BACKUP_DIR}/postgres_${DATE}.sql.gz | cut -f1)"
# Rotation des sauvegardes
find "${BACKUP_DIR}" -name "postgres_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
echo "Sauvegardes de plus de ${RETENTION_DAYS} jours supprimées"
dest: /usr/local/bin/docker-backup-postgres.sh
owner: root
group: root
mode: "0755"
- name: Planifier la sauvegarde PostgreSQL quotidienne
ansible.builtin.cron:
name: "Sauvegarde PostgreSQL quotidienne"
job: "/usr/local/bin/docker-backup-postgres.sh >> /var/log/docker-backup.log 2>&1"
minute: "0"
hour: "2"
user: root
# -------------------------
# Monitoring Docker
# -------------------------
- name: Déployer le script de vérification de santé
ansible.builtin.copy:
content: |
#!/bin/bash
# Ansible managed - Health check des conteneurs Docker
set -euo pipefail
UNHEALTHY=$(docker ps --filter "health=unhealthy" --format "{{ '{{' }}.Names{{ '}}' }}: {{ '{{' }}.Status{{ '}}' }}")
EXITED=$(docker ps -a --filter "status=exited" --filter "label=managed_by=ansible" --format "{{ '{{' }}.Names{{ '}}' }}: exited {{ '{{' }}.Status{{ '}}' }}")
if [ -n "$UNHEALTHY" ]; then
echo "ALERTE - Conteneurs unhealthy:"
echo "$UNHEALTHY"
# Envoyer une notification (webhook, email, etc.)
fi
if [ -n "$EXITED" ]; then
echo "ALERTE - Conteneurs arrêtés:"
echo "$EXITED"
fi
if [ -z "$UNHEALTHY" ] && [ -z "$EXITED" ]; then
echo "OK - Tous les conteneurs sont en bonne santé"
fi
dest: /usr/local/bin/docker-healthcheck.sh
owner: root
group: root
mode: "0755"
- name: Planifier la vérification de santé toutes les 5 minutes
ansible.builtin.cron:
name: "Health check Docker"
job: "/usr/local/bin/docker-healthcheck.sh >> /var/log/docker-healthcheck.log 2>&1"
minute: "*/5"
user: root
- name: Configurer logrotate pour les logs Docker de maintenance
ansible.builtin.copy:
content: |
# Ansible managed
/var/log/docker-maintenance.log
/var/log/docker-updates.log
/var/log/docker-backup.log
/var/log/docker-healthcheck.log {
weekly
rotate 4
compress
missingok
notifempty
}
dest: /etc/logrotate.d/docker-maintenance
owner: root
group: root
mode: "0644"Le playbook complet de déploiement
Voici le playbook principal qui orchestre l'installation de Docker, le déploiement de la stack et la configuration de la maintenance :
# site.yml
---
- name: Installer Docker et déployer la stack applicative
hosts: docker_hosts
become: yes
gather_facts: yes
pre_tasks:
- name: Vérifier la connectivité
ansible.builtin.ping:
- name: Vérifier les prérequis système
ansible.builtin.assert:
that:
- ansible_distribution in ['Debian', 'Ubuntu']
- ansible_memtotal_mb >= 2048
- ansible_processor_vcpus >= 2
fail_msg: >
Prérequis non remplis.
OS supporté: Debian/Ubuntu.
RAM minimum: 2 Go (actuel: {{ ansible_memtotal_mb }} Mo).
CPU minimum: 2 vCPU (actuel: {{ ansible_processor_vcpus }}).
- name: Afficher les informations du serveur
ansible.builtin.debug:
msg: >
{{ inventory_hostname }} |
{{ ansible_distribution }} {{ ansible_distribution_version }} |
{{ ansible_processor_vcpus }} vCPU |
{{ ansible_memtotal_mb }} Mo RAM |
Disque: {{ ansible_mounts[0].size_available | human_readable }}
roles:
- role: docker-install
tags: [docker, install]
- role: docker-stack
tags: [docker, stack, deploy]
- role: docker-maintenance
tags: [docker, maintenance]
post_tasks:
- name: Afficher l'état final de la stack
ansible.builtin.command:
cmd: docker ps --format "table {{ '{{' }}.Names{{ '}}' }}\t{{ '{{' }}.Image{{ '}}' }}\t{{ '{{' }}.Status{{ '}}' }}\t{{ '{{' }}.Ports{{ '}}' }}"
register: docker_ps
changed_when: false
- name: Résumé du déploiement
ansible.builtin.debug:
msg:
- "============================================"
- "Stack {{ app_name }} déployée avec succès"
- "============================================"
- ""
- "{{ docker_ps.stdout }}"
- ""
- "============================================"
- "URLs :"
- " Application : https://{{ app_domain }}"
- " Traefik : https://{{ traefik_dashboard_domain }}"
- "============================================"
- "Maintenance :"
- " Prune : {{ docker_prune_schedule }}"
- " Updates : {{ docker_image_update_schedule }}"
- " Backup : Quotidien à 2h"
- "============================================"Comparaison : Ansible vs docker-compose direct
Une question légitime se pose : pourquoi ne pas simplement utiliser docker-compose up -d directement ? Voici une comparaison détaillée pour vous aider à choisir la bonne approche selon votre contexte.
Avantages d'Ansible par rapport à docker-compose direct
- Installation de Docker incluse : Ansible peut provisionner un serveur vierge, installer Docker et déployer la stack en une seule exécution. Docker Compose nécessite que Docker soit déjà installé.
- Gestion multi-serveurs : Ansible déploie sur N serveurs en parallèle. Avec docker-compose, il faut se connecter à chaque serveur individuellement.
- Gestion des secrets : Ansible Vault chiffre les variables sensibles directement dans le code source. Docker Compose nécessite des fichiers
.envnon chiffrés ou des outils tiers. - Pré/post-tâches : vérification des prérequis, migrations de base de données, tests de santé, notifications... Ansible gère toute la chaîne de déploiement.
- Idempotence robuste : Ansible vérifie l'état actuel avant d'appliquer des changements. Un
docker-compose up -drecrée parfois des conteneurs inutilement. - Configuration système : pare-feu, utilisateurs, sysctl, kernel parameters... Ansible configure tout l'environnement, pas seulement les conteneurs.
- Audit et traçabilité : chaque exécution d'Ansible produit un log détaillé des changements appliqués.
Quand docker-compose suffit
- Développement local : pour lancer une stack sur votre machine, docker-compose est plus rapide et plus simple.
- Serveur unique : si vous ne gérez qu'un seul serveur avec une stack simple, l'overhead d'Ansible n'est pas justifié.
- Prototypage rapide : pour tester une idée rapidement, docker-compose est imbattable en termes de vitesse de mise en place.
L'approche hybride recommandée
La meilleure stratégie est souvent une approche hybride : utilisez docker-compose pour définir la stack (le fichier docker-compose.yml reste la source de vérité) et Ansible pour l'orchestration (installation, déploiement, configuration système, maintenance). C'est exactement ce que le module docker_compose_v2 permet de faire.
# Approche hybride : docker-compose.yml géré par Ansible
- name: Déployer le docker-compose.yml depuis un template
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "/opt/docker/{{ app_name }}/docker-compose.yml"
owner: root
group: docker
mode: "0640"
- name: Déployer la stack via Docker Compose
community.docker.docker_compose_v2:
project_src: "/opt/docker/{{ app_name }}"
state: present
pull: always
remove_orphans: yes
register: stack_result
- name: Afficher les changements
ansible.builtin.debug:
var: stack_result.actions
when: stack_result.changedCommandes d'exécution
Voici les commandes essentielles pour utiliser ce playbook :
# Installer les collections requises
ansible-galaxy collection install -r requirements.yml
# Déploiement complet (install Docker + stack + maintenance)
ansible-playbook -i inventory/hosts.yml site.yml
# Installer Docker uniquement
ansible-playbook -i inventory/hosts.yml site.yml --tags install
# Déployer/mettre à jour la stack uniquement
ansible-playbook -i inventory/hosts.yml site.yml --tags deploy
# Mode dry-run
ansible-playbook -i inventory/hosts.yml site.yml --check --diff
# Avec Ansible Vault pour les secrets
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass
# Sur un serveur spécifique
ansible-playbook -i inventory/hosts.yml site.yml --limit web01Bonnes pratiques
Pour conclure, voici les bonnes pratiques essentielles à suivre lorsque vous gérez Docker avec Ansible :
- Utilisez toujours des tags d'images spécifiques en production (jamais
:latest). Cela garantit la reproductibilité des déploiements. - Chiffrez vos secrets avec Ansible Vault. Les mots de passe, tokens et clés API ne doivent jamais être en clair dans vos fichiers.
- Définissez des limites de ressources (mémoire, CPU) pour chaque conteneur afin d'éviter qu'un service défaillant ne consomme toutes les ressources du serveur.
- Implémentez des health checks sur tous vos conteneurs. Sans health check, Docker ne peut pas détecter un service qui ne répond plus.
- Séparez les réseaux : utilisez des réseaux internes pour les communications entre services et n'exposez que les ports strictement nécessaires.
- Sauvegardez vos volumes régulièrement. Les données dans les conteneurs sont éphémères par défaut.
- Versionnez tout : playbooks, variables, templates. Votre infrastructure doit pouvoir être reconstruite à l'identique à partir de Git.
- Testez en staging avant de déployer en production. Molecule est un excellent outil pour tester vos rôles Ansible.
Conclusion
La combinaison Ansible et Docker offre une approche puissante et flexible pour gérer une infrastructure conteneurisée. Ansible apporte la reproductibilité, la gestion multi-serveurs et l'orchestration complète du cycle de déploiement, tandis que Docker fournit l'isolation et la portabilité des applications.
Dans cet article, nous avons couvert l'installation automatisée de Docker, l'utilisation détaillée des modules community.docker, le déploiement d'une stack de production complète avec Traefik, PostgreSQL et Redis, la gestion des registries privés et la maintenance automatisée. Ce playbook constitue une base solide que vous pouvez adapter et étendre selon les besoins spécifiques de votre infrastructure.
L'investissement initial dans la création de ces playbooks est largement compensé par le temps gagné lors des déploiements suivants, la réduction des erreurs humaines et la capacité à reconstruire votre infrastructure en quelques minutes en cas de sinistre. C'est l'essence même de l'Infrastructure as Code.