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
Definir todos los Dockerfile que necesitamos para cada aplicación.
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.Levantar los contenedor usando el comando
docker-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 web
donde 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:
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.
Luego usamos
RUN
para correr el comandomkdir
y crear la carpeta/code
dentro del contenedor.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.Instalamos los requerimientos establecidos en el
requirements.txt
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:
Estamos definiendo el servicio llamado
backend
Con
build
indicamos en cuál carpeta el servicio debe buscar el Dockerfile para crear la imagen y correr el contenedor.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.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.
- En el directorio de nuestro proyecto
django_docker
corremos el comando:
$ docker-compose up
- 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.yml
y 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:
Le indica a nginx que debe escuchar por el puerto 80
Las peticiones deben venir del host localhost
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:
Indicamos que deseamos utilizar la imagen oficial de NGINX como base
Agregamos el archivo
web
dentro de la carpeta/etc/nginx/sites-enabled/
en el contenedorAñ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 nombrebackend
.
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:
Usamos la imagen oficial de postgres, la última versión, para construir nuestro contenedor.
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\