Docker Compose

Es una herramienta que permite definir, correr múltiples contenedores y cómo los mismos se conectan entre sí. Esto se logra usando ciertas reglas y especificaciones otorgadas por la herramienta.

Correr docker-compose consta de tres pasos principalmente

  1. Definir todos los Dockerfile que necesitamos para cada aplicación.

  2. Definir nuestro archivo docker-compose.yml. En él debemos colocar las definiciones y características que deben tener nuestros contenedores. Aquí se invocan a los archivos Dockerfile.

  3. Levantar los contenedor usando el comandodocker-compose up.

Creando nuestra primera dgo app con compose

Antes de todo, vamos a crear una nueva máquina para esta aplicación

$ docker-machine create -d virtualbox django_docker
$ eval $(docker-machine env django_docker)

Preparando la carpeta con el código de Django.

Creamos el directorio de nuestro proyecto django_docker y dentro de él, directorio webdonde estará el código de Django.

$ mkdir django_docker && cd django_docker
$ mkdir web

Dentro de web creamos el archivo requirements.txt , donde colocaremos las dependencias de python que usaremos.

Colocamos:

Django==1.10.5

Por el momento sólo Django.

Instalamos nuestro archivo dependencias para instalar Django

$ pip install -r requirements.txt

Luego creamos el proyecto

$ django-admin startproject webflix

Ahora, dentro de nuestra carpeta web deberíamos tener el código generador por el comando de django-admin con nuestro proyecto webflix.

Luego, creamos el Dockerfile para el app de Django dentro de la carpeta web.

Creamos dichi archivo con este contenido

FROM python:3.5
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code

Qué estamos haciendo, línea por línea:

  1. Comenzamos a construir la imagen tomando como base la imagen oficial de python 3.5. Para luego agregar todo lo que necesitemos sobre la misma.

  2. Luego usamos RUN para correr el comando mkdir y crear la carpeta /code dentro del contenedor.

  3. Usamos WORKDIR para establecer el directorio de trabajo, en este caso sería /code. Esta instrucción puede tener la analogía de hacer `cd code`. Todos los comandos a partir de acá se ejecutarán desde el directorio establecido.

  4. Instalamos los requerimientos establecidos en el requirements.txt

  5. Añadimos el código que se encuentra en “.” que sería todos los archivos que están al mismo nivel de mi Dockerfile, es decir, todos los archivos dentro de la carpeta web. Los mismos son agregados al directorio /code. Básicamente, estamos dando la instrucción de copiar los archivos locales hacia una carpeta dentro del contenedor.

Comandos

Para consultar los comandos disponibles por el docker-compose

$ docker-compose

Define and run multi-container applications with Docker.

Usage:
  docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
  docker-compose -h|--help

Options:
  -f, --file FILE             Specify an alternate compose file (default: docker-compose.yml)
  -p, --project-name NAME     Specify an alternate project name (default: directory name)
  --verbose                   Show more output
  -v, --version               Print version and exit
  -H, --host HOST             Daemon socket to connect to

  --tls                       Use TLS; implied by --tlsverify
  --tlscacert CA_PATH         Trust certs signed only by this CA
  --tlscert CLIENT_CERT_PATH  Path to TLS certificate file
  --tlskey TLS_KEY_PATH       Path to TLS key file
  --tlsverify                 Use TLS and verify the remote
  --skip-hostname-check       Don't check the daemon's hostname against the name specified
                              in the client certificate (for example if your docker host
                              is an IP address)
  --project-directory PATH    Specify an alternate working directory
                              (default: the path of the Compose file)

Commands:
  build              Build or rebuild services
  bundle             Generate a Docker bundle from the Compose file
  config             Validate and view the Compose file
  create             Create services
  down               Stop and remove containers, networks, images, and volumes
  events             Receive real time events from containers
  exec               Execute a command in a running container
  help               Get help on a command
  images             List images
  kill               Kill containers
  logs               View output from containers
  pause              Pause services
  port               Print the public port for a port binding
  ps                 List containers
  pull               Pull service images
  push               Push service images
  restart            Restart services
  rm                 Remove stopped containers
  run                Run a one-off command
  scale              Set number of containers for a service
  start              Start services
  stop               Stop services
  top                Display the running processes
  unpause            Unpause services
  up                 Create and start containers
  version            Show the Docker-Compose version information

Definiendo nuestros servicios con el archivo Docker-compose.yml

El docker-compose.yml es un archivo en el formato YAML, el cual nos permite definir services(servicios), networks(redes) y (volumes)volúmenes.

Los servicios son conjunto de reglas que se le deben aplicar al contenedor, podemos decir que es análogo a los parámetros que se le pasan cuando usamos docker run. Las redes, permiten definir la conexión que abarcará a los contenedores, el análogo sería el comando docker network create y por último, los volúmenes, por lo general representa la forma de cómo almacenar datos que persistan.

Creamos nuestro docker-compose.yml y agregamos lo siguiente:

version: '2'
services:
  backend:
    build: ./web
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000

Qué estamos haciendo:

  1. Estamos definiendo el servicio llamado backend

  2. Con build indicamos en cuál carpeta el servicio debe buscar el Dockerfile para crear la imagen y correr el contenedor.

  3. Con command especificamos el comando que se debe correr al terminar de construir el contenedor. En este caso, estamos levantando el servidor de pruebas de Django.

  4. Finalmente usamos ports para mapear los puertos desde el host/huésped hasta el contenedor. En este caso, decimos, mapea el puerto 8000 al 8000 del contenedor.

Docker-compose up

Ya habiendo definido nuestro servicio, vamos a correrlo.

  1. En el directorio de nuestro proyecto django_dockercorremos el comando:
$ docker-compose up
  1. Si todo sale bien, vamos a nuestro navegador y visitamos la ip de nuestra máquina en el puerto 8000. Nuestra app debería estar corriendo. En la consola podremos ver los logs de nuestro contenedor.

Ahora, entonces, cómo funciona esto de docker-compose y por qué es tan útil. Simple, acabamos de especificar en un archivo como queremos correr nuestro contenedor, y en este archivo podemos añadir muchísimos otros servicios/contenedores y conectarlos entre sí. Es decir, esto es mucho más simple que ejecutar docker run para cada contenedor/servicio que necesitemos.

Para detener el contenedor, podemos salirnos con Ctrl+C.

Entonces, ¿ Cómo se traduciría esta especificación a comandos simples de docker ?

Antes de seguir, intentemos hacerlo por nuestra cuenta

….. no leas…. intentalo.

Bueno, los comandos serían los siguientes:

$ docker build -t backend web
$ docker run -p 8000:8000 -i backend -c “python manage.py runserver 0.0.0.0:8000”

Si queremos probar, corramos estos comandos y abramos el navegador para verificar.

Servicios en modo segundo plano

Tal vez nos dimos cuenta que al hacer docker-compose up, si cerramos la consola o cortamos la ejecución, nuestro contenedor se destruye. Si queremos correr nuestros contenedores en segundo plano, agregamos el flag -d.

$ docker-compose up -d

Nota: docker-compose <comando> equivale a docker-compose -f docker-comose.yml <comando>. El parámetro -f es para indicar de cuál archivo se debe leer los servicios para construir los contenedores, por defecto es docker-compose.yml, que es el mismo que tenemos en nuestro directorio raíz.

Listando servicios creados

Si queremos ver los servicios que están corriendo actualmente, usamos:

docker-compose ps
Name                 Command             State  Ports
composetest_web_1    /bin/sh -c python   Up     8000->8000/tcp

Ahí nos indica, el nombre del contenedor, el comando que está corriendo, el estado y el mapeo de puertos.

Agregando NGINX

Por lo general para las aplicaciones web, debemos tener un servidor que se encargue de manejar las peticiones, en nuestro caso, vamos usar NGINX como servidor web.

Creamos una carpeta donde estarán nuestros archivo de configuración para nginx y el Dockerfile.

$ mkdir nginx

Nota: La carpeta debe estar al mismo nivel del docker-compose.ymly las otras carpetas de nuestra app.

Creamos la configuración general a usar por nginx dentro del directorio anteriormente creado

$ touch nginx.conf
$ vi nginx.conf

Y copiamos lo siguiente:

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}
http {

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent""$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;
    sendfile on;

    keepalive_timeout 65;
    gzip on;
    gzip_static on;
    #include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Creamos el archivo de configuración para manejar las peticiones a nuestro servidor

$ touch web
$ vi web

y agregamos lo siguiente

server {
    listen 80;
    server_name localhost;

    charset utf-8;

    location / {
        proxy_pass 
        http://backend:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Qué hace ese archivo:

  1. Le indica a nginx que debe escuchar por el puerto 80

  2. Las peticiones deben venir del host localhost

  3. Por último, se redirecciona todas las peticiones a nuestro servidor de Django que está escuchando en el puerto 8000 en el hostname backend. Pero entonces, cómo sabe nginx que debe enviar las peticiones a un servicio llamado backend que hace referencia a nuestro contenedor de Django, simple, con un poco de magía, nuestro contenedor sabrá a cuál ip le corresponde ese nombre y esa ip será usada para redireccionar las peticiones.

Ahora nuestro Dockerfile

$ touch Dockerfile
$ vi Dockerfile

y agregamos:

FROM nginx
ADD backend /etc/nginx/sites-enabled/
ADD nginx.conf /etc/nginx/

Qué estamos haciendo:

  1. Indicamos que deseamos utilizar la imagen oficial de NGINX como base

  2. Agregamos el archivo web dentro de la carpeta /etc/nginx/sites-enabled/ en el contenedor

  3. Añadimos nuestra configuración de nginx dentro de /etc/nginx/donde por convención debe estar.

Ahora, creamos el servicio nginx. Editamos nuestro docker-compose.yml y agregamos:

nginx:
  build: ./nginx/
  ports:
    - "80:80"
  links:
    - backend
  • Indicamos la carpeta donde debe buscarse el Dockerfile para hacer build

  • Abrimos el puerto 80 de nuestro contenedor.

  • Con directiva links es la forma en que conectamos nuestros contenedores. Aquí estamos indicando que el contenedor de nginx debe esperar por el contenedor backend y el mismo debe ser enlazado. De esta forma, puede saber lo que denominamos anteriormente como magia: cuál ip/contenedor le corresponde al nombre backend.

Hacemos build

$ docker-compose build

Iniciamos los servicios

$ docker-compose up -d

Ahora, si visitamos la ip de nuestro máquina en el puerto 80 (http://ip_máquina\, nuestra aplicación debería estar funcionando.

Ya agregamos nuestro servidor web, ahora es momento de agregar un manejador de base de datos.

Agregando contenedor de PostgreSQL

Toda aplicación web necesita de un manejador de base de datos, para nuestro caso, usaremos Postgres.

Modificamos nuestro docker-compose.yml y agregamos nuestro servicio de postgres

db:
  image: postgres:latest
  environment:
    - POSTGRES_PASSWORD=docker
    - POSTGRES_USER=docker
    - POSTGRES_DB=docker_db

Qué hicimos:

  1. Usamos la imagen oficial de postgres, la última versión, para construir nuestro contenedor.

  2. Usando environment especificamos variables de entorno en nuestro contenedor. Internamente la imagen oficial de postgres usa las variables POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB para crear la base de datos y establecer las credenciales.

Ahora debemos conectar nuestro contenedor de backend con el de nuestro servidor de base de datos db

backend:
  build: ./web
  command: python manage.py runserver 0.0.0.0:8000
  ports:
    - "8000:8000"
  links:
    - db

Nuestro docker-compose.yml quedaría de la siguiente manera:

version: '2'
services:
  backend:
    build: ./web
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"
    links:
      - db
  nginx:
    build: ./nginx/
    ports:
      - "80:80"
    links:
      - backend
  db:
    image: postgres:latest
    environment:
      - POSTGRES_PASSWORD=docker
      - POSTGRES_USER=docker
      - POSTGRES_DB=docker_db

Ahora, hacemos build y levantamos nuestros servicios.

$ docker-compose build
$ docker-compose up -d

Para probar nuestro contenedor de postgres, nos conectamos al mismo.

$ docker exec -i -t maquina_db_1 bash

Dentro de nuestro contenedor, nos conectamos a la base de datos docker_db

$ psql -h localhost -p5432-d docker_db -U docker --password docker
>SELECT current_date;

Si todo va bien, debimos poder conectarnos e imprimir la fecha actual.

Salimos de la consola de postgres y del contenedor.

>\q
$ exit

Ahora, debemos especificar en nuestra aplicación de Django que debemos usar la base de datos previamente creada.

En nuestro archivo /web/webflix/settings.py modificamos nuestra variable DATABASE, añadiendo las credenciales y el engine a usar.

DATABASES = {
    'default': {
    'ENGINE': 'django.db.backends.postgresql',
    'NAME': docker_db,
    'USER': docker,
    'HOST': 'db',
    'PASSWORD': 'docker',
    'PORT': 5432,
    }
}

Reconstruimos nuestros contenedores

$ docker-compose build
$ docker-compose up -d

Nuestra base de datos debe estar vacía, para crear el esquema de nuestra base de datos, usamos migrate.

$ docker exec -i -t dwn_web_1 python manage.py migrate

Si no hubo ningún error, significa que ya tenemos nuestra aplicación conectada a nuestro contenedor de base de datos.

Eliminando todos los contenedores

Si deseamos eliminar todos los contenedores usando un solo comando, podemos usar down

$ docker-compose down

Si listamos los contenedores

$ docker-compose ps

Debemos obtener que ningún contenedor está corriendo.

Ahora, surge un problema, qué pasó con nuestra base de datos. Al borrar el contenedor(también si se detiene), se elimina toda la data que está dentro de él, por lo tanto todo lo que teníamos almacenado en nuestra db desapareció. Esto no es lo que queremos a la hora de construir nuestra aplicación.

Entonces, cómo podemos evitar esto y persistir la data, simple, usando volumes, data containers o data volumes.

Docker volumes

Básicamente representan un tipo especial de directorio que se encuentra dentro de un contenedor. Sus características principales:

  • Se crean cuando se inicializa el contenedor

  • Sirven para compartir información entre contenedores

  • Mantienen la información aun cuando el contenedor es eliminado (muy importante)

Para crear un volumen dentro de nuestro contenedor de postgres, redefinimos nuestro servicio para docker compose.

db:
  volumes:
    - /var/lib/postgresql/data
  image: postgres:latest
  environment:
    - POSTGRES_PASSWORD=docker
    - POSTGRES_USER=docker
    - POSTGRES_DB=docker_db

Qué hicimos:

Agregamos la definición volumes con el directorio /var/lib/postgresql/data , el cual es donde postgres almacena la data por defecto.

Ahora, hacemos build y levantamos nuestro contenedores (anteriormente los destruimos con down).

$ docker-compose build
$ docker-compose up -d

Corremos de nuevo migrate

$ docker exec -i -t dwn_web_1 python manage.py migrate

Ahora, paramos y volvemos a correr el contenedor de postgres

$ docker-compose stop db
$ docker-compose start db

Chequeamos que nuestra data continue

$ docker exec -i -t dwn_web_1 python manage.py migrate

Al intentar volver a correr el comando migrate, debería comunicarnos que no hay nada que migrar, esto nos indica que nuestra data aún persiste.

Si eliminamos el contenedor, es decir, usando down or stop y rm , el volumen todavía quedará pero con un estado de “no referenciado”.

Data containers

Por lo general, nuestra base de datos puede ser usada por más de un contenedor o servicio, entonces cuando necesitamos tener data que sea persistente y se pueda compartir entre contenedores, usamos lo que se conoce como data containers, que son contenedores donde definimos un volumen y luego otros contenedores pueden referenciarlo.

Añadimos nuestro data container.

data:
  restart: always
  image: postgres:9.4
  volumes:
     - /var/lib/postgresql/data

Ahora para usar el volumen de ese contenedor, modificamos nuestro servicio db

db:
  image: postgres:latest
  links:
    - data
  environment:
    - POSTGRES_PASSWORD=docker
    - POSTGRES_USER=docker
    - POSTGRES_DB=docker_db

Qué hicimos:

Usando la definición links, le indicamos a nuestro servicio que además de poderse conectar con el servicio data, debe también poder acceder a todos los volumenes que estén definidos en el mismo, en este caso /var/lib/postgresql/data

NOTA: Esto básicamente creará un directorio /var/lib/postgresql/data en el contenedor db que será una referencia al directorio /var/lib/postgresql/data del contenedor data, donde se almacenará toda nuestra información.

Eliminemos y reecremos todos los contenedores de nuevo

$ docker-compose down
$ docker-compose build
$ docker-compose up -d

Hacemos migrate para persistir la data en nuestro nuevo data container.

$ docker exec -i -t dwn_web_1 python manage.py migrate

Para probar que funciona, eliminemos y creemos el contenedor de postgres nuevamente

$ docker-compose stop db
$ docker-compose rm db
$ docker-compose build db
$ docker-compose up -d db

Intentamos correr migrate y debería aparecer de nuevo que no tenemos nada que migrar, queriendo decir así, que nuestra data ha persistido.

$ docker exec -i -t dwn_web_1 python manage.py migrate

Data volumes

Pero, aún hay más, si queremos crear un espacio para persistir nuestra data que no sea un contenedor, que no tenga tengamos que preocuparnos por su ciclo de vida, que sea independiente del host. Docker nos permite crear volumes directos.

Generalmente se usa el siguiente comando para su creación, pero nosotros lo vamos a crear a través de docker compose.

$ docker create volumes nombre_volume (No ejecutar, solo para ejemplificar)

Para agregar uno, usando docker compose, necesitamos añadir lo siguiente a nuestro archivo de docker-compose.yml

volumes:
  - pgvolumen

Creamos el volumen

$ docker-compose up -d

Vamos usarlo en nuestro data container

data:
  restart: always
  image: postgres:9.4
  volumes:
    - pgvolume:/var/lib/postgresql/data

Qué hicimos:

Agregamos pgvolume:/var/lib/postgresql/data indicando que la carpeta con la ruta /var/lib/postgresql/data debe ser montada/referenciada hacia el volumen pgvolume.

Recreamos nuestros contenedores y corremos migrate para persistir la data en nuestro volumen creado

$ docker-compose build
$ docker-compose up -d
$ docker exec -i -t dwn_web_1 python manage.py migrate

Ahora, ya estamos listo para poder manejar nuestra data evitando que se pierda al manipular los contenedores.

Si necesitamos información adicional, la podemos encontrar [acá](https://docs.docker.com/engine/tutorials/dockervolumes/#data-volumes\

results matching ""

    No results matching ""