If you use Ansible to deploy your servers, you can go further by also integrating the deployment of your applications with Docker Compose.

This is the first post in a series of 3 articles about Docker and Ansible:
Part I: from Docker Compose to Ansible
Part II: using variables
Part III: using vault to encrypt sensitive information

Install Docker and Docker Compose

You'll find the instructions here.

Install Ansible

For installing the latest stable version of Ansible on CentOS :

$ yum install epel-release && yum install ansible

Personally I use the version under development to be able to take advantage of the latest features (for example, creating an internal network is not possible in the stable version 2.7) :

pip install git+https://github.com/ansible/[email protected]

Create your playbook

We will deploy a stack with Nginx, PHP and mySQL.
Create your working directory :

$ mkdir ~/ansible-docker && cd ~/ansible-docker

Docker Compose Syntax

Here is a classic file with 3 services :

version: '3.6'

services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./app.conf:/etc/nginx/conf.d/
      - php-app:/var/www/app
    restart: unless-stopped
  php:
    image: php:7-fpm
    volumes:
      - php-app:/var/www/app
    restart: unless-stopped
  db:
    image: mysql:latest
    volumes:
      - mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret_root
      MYSQL_DATABASE: database_name
      MYSQL_USER: sql_user
      MYSQL_PASSWORD: secret_user
    restart: unless-stopped

volumes:
  mysql:
  
networks:
  network_app:
    driver: bridge
    ipam:
     config:
       - subnet: 172.16.98.0/24
  

Ansible way

Create your Nginx : app.conf :

$ vi app.conf
server {
    listen 80;
    server_name your_ip;

    client_max_body_size 4M;
    client_body_buffer_size 128k;

    root /var/www/app;
    index index.php;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_index index.php;
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include /etc/nginx/fastcgi_params;
    }
}

Create your Ansible playbook :

$ vi deploy.yml

And the same 3 services :

---
- hosts: localhost

  tasks:

  - name: Create network
    docker_network:
      name: network_app
      ipam_options:
        subnet: '172.16.98.0/24'
        
  - name: Run Nginx container
    docker_container:
      name: 'nginx'
      recreate: true
      restart_policy: unless-stopped
      image: 'nginx:latest'
      published_ports:
        - "80:80"
        - "443:443"
      volumes:
        - "./app.conf:/etc/nginx/conf.d/app.conf"
        - "php-app:/var/www/app"
      networks:
        - name: "network_app"        - 

  - name: Run PHP container
    docker_container:
      name: 'php'
      recreate: true
      restart_policy: unless-stopped
      image: 'php:7-fpm'
      volumes:
        - "php-app:/var/www/app"
      networks:
        - name: "network_app"
        
  - name: Run Percona container
    docker_container:
      name: 'percona'
      recreate: true
      restart_policy: unless-stopped
      image: 'percona:latest'
      volumes:
        - "percona:/var/lib/mysql"
      env:
        MYSQL_ROOT_PASSWORD: "secret_root"
        MYSQL_DATABASE: "db"
        MYSQL_USER: "db_user"
        MYSQL_PASSWORD: "secret_user"
      networks:
        - name: "network_app"

Deploy

You've just to do :

$ ansible-playbook deploy.yml

And you'll see the output :

$ ansible-playbook deploy.yml
 [WARNING]: No inventory was parsed, only implicit localhost is available

 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAY [localhost] *************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *******************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Run Nginx container] ***************************************************************************************************************************************************************************************************************
changed: [localhost]

TASK [Run PHP container] *****************************************************************************************************************************************************************************************************************
changed: [localhost]

TASK [Run Percona container] *************************************************************************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP *******************************************************************************************************************************************************************************************************************************
localhost                  : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Check if everything is up :

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                        NAMES
f7bed03b6674        percona:latest      "/docker-entrypoint.…"   4 seconds ago       Up 3 seconds        3306/tcp                                     percona
5761d15e793e        php:7-fpm           "docker-php-entrypoi…"   8 seconds ago       Up 7 seconds        9000/tcp                                     php
517dbb31f0d6        nginx:latest        "nginx -g 'daemon of…"   10 seconds ago      Up 9 seconds        0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx

Test the deployment

Add a info.php in the php-app volume :

$ vi /var/lib/docker/volumes/php-app/_data/info.php

add :

<?php

// Show all information, defaults to INFO_ALL
phpinfo();

?>

Get the info.php :

$ curl -I http://127.0.0.1/info.php
HTTP/1.1 200 OK
Server: nginx/1.15.9
Date: Mon, 25 Mar 2019 16:00:37 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/7.3.3

It's all good!

Destroy

It's very simple, for example :

- name: remove Nginx container
  docker_container:
    name: nginx
    state: absent
- name: remove PHP container
  docker_container:
    name: php
    state: absent
- name: remove Percona container
  docker_container:
    name: percona
    state: absent
- name: Delete network, disconnecting all containers
  docker_network:
    name: network_app
    state: absent
    force: yes

But it's more simple to integrate directly in the first file with the state option, we'll see this here.

Resources :

https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html
https://docs.ansible.com/ansible/2.7/plugins/connection/docker.html
https://docs.ansible.com/ansible/2.7/modules/docker_container_module.html
https://docs.ansible.com/ansible/2.7/modules/docker_network_module.html
https://docs.ansible.com/ansible/latest/modules/docker_network_module.html

Feel free to correct me if you see any typo or if something seems wrong to you.