Tutorial docker compose - perfeccion-ar/infraestructura-clasica-y-avanzada GitHub Wiki
En mi trabajo heredé un proyecto antiguo hecho en Laravel 6, mientras que la versión LTS actual es la 11, y está desarrollado con PHP 7.2, cuando la versión actual es la 8.3. El proyecto venía con un archivo docker-compose.yaml, por lo que, debido a la necesidad y urgencia, tuve que aprender a usar Docker. A continuación, redacté un documento detallando el proceso.
Hasta el día de hoy, logré levantar Docker y funciona de maravillas, pero la aplicación de Laravel aún no funciona. -Catoto
Docker es una de las herramientas más útiles que debe aprender un Data Scientist. Pero hay veces que necesitas no uno, sino muchos contenedores trabajando conjuntamente: una base de datos y una aplicación, por ejemplo. En estos casos, se hace imprescindible saber qué es y cómo funciona Docker Compose. ¡Empezemos!
Docker Compose es una extensión de Docker que permite trabajar con varios contenedores de forma simultánea haciendo que estos se conecten y relacionen entre sí de la forma en la que tú decidas.
Docker Compose surge porque muchas aplicaciones requieren de más de un microservicio. Pero claro, la idea de Docker es que únicamente ejecute un único microservicio por contenedor y no varios a la vez.
Por tanto en esta situación podemos hacer dos cosas:
- Ejecutar dos microservicios en un mismo Docker. Lo bueno en esta situación es que no necesitamos aprender nada para ello, simplemente con Docker podríamos lograrlo. Sin embargo, esto es muy delicado, ya que, si uno de los dos servicios fallase, fallarían todos los servicios. Además, sería poco escalable, ya que se compartirían todos los recursos y se tendría que escalar todo a la vez, lo cual no tiene mucho sentido. Lo lógico sería que si, por ejemplo, una API que tenemos en Docker recibe muchas peticiones, escalemos solo esa API, y no todo lo demás.
- Utilizar Docker Compose: con Docker Compose puedes crear varios contenedores y definir cómo quieres que se relacionen y cómo quieres que gestionen los datos que generan. De esta forma, si uno de los contenedores fallase, podrías (depende cómo lo configures) permitir que los otros servicios sigan funcionando.
Además, de cara a la puesta en producción del sistema, Docker Compose puede desplegarse con herramientas como Portainer o Docker Swarm. Incluso, si cuentas con una infraestructura Kubernetes, Kompose te permite pasar de fichero compose.yaml a manifests de Kubernetes.
Cómo ves, Docker Compose es una herramienta muy útil. En mi opinión, es imprescindible en cualquier persona que quiera desplegar proyectos a producción, o incluso desarrollarlos, como veremos más adelante. Dicho esto, veamos cómo funciona Docker Compose.
Para crear un Docker Compose necesitamos crear un fichero compose.yaml. Este fichero es donde indicaremos qué servicios queremos que se ejecuten y de qué manera.
En este sentido, comentar que Docker Compose cuenta con varios apartados:
- Definición de la versión (Opcional, deprecado)
- Servicios (Requerido)
- Redes
- Volúmenes
- Configs
- Secrets
Para que la explicación sea más fácil de seguir, vamos a poner un caso típico: un proyecto que cuenta con una aplicación, una base de datos y una API.
La documentación actual de Docker Compose, indica que le definición de la versión del Docker Compose no debería definirse hoy en día. Sin embargo, seguramente lo veas mucho en ficheros compose.yaml que puedas llegar a ver, así que personalmente prefiero explicarlo.
La selección de la versión en Docker es muy sencillo, simplemente debemos indicar la palabra versión, seguida del valor de versión de Docker Compose que queramos usar, tal como se muestra en el siguiente código:
version: 3.2
Ahora la pregunta que seguramente te hagas es, ¿y yo, qué versión debo definir? Pues como el propio Docker recomienda: siempre la última versión. Es por ello que recomiendan no definir la versión, ya que de esta forma se estará eligiendo siempre la última versión.
Si encuentras un Docker-Compose con version 2, seguirás pudiendo usarlo. En cualquier caso, aquí puedes encontrar el listado de funcionalidades que se han eliminado de la versión 2 a la 3, así como las nuevas funcionalidades que se han añadido.
El primer punto es sencillo, lo mejor de todo, no definir nada. Así pues, ahora veamos el siguiente punto: la definición de servicios.
##Servicios en Docker Compose (Requerido)
La definición de los servicios en Docker Compose es la única sección requerida y la más importante. En ella, definimos cada uno de los microservicios que vamos a ejecutar, con el nombre que queramos.
Además, para cada servicio se suelen definir los siguientes aspectos:
- build: especifica la ubicación de un fichero Dockerfile para el build de una imagen a nivel local. Ejemplo:
services:
app:
build: './application'
- image: especifíca la imagen que se usará para crear el contenedor. Únicamente se aplica si no se ha especificado el campo build, en cuyo caso se aplicará lo especificado en build. El nombre de la imagen siempre debe seguir el siguiente formato: [/][/]
[:|@]. Además, si tenemos que descargar la imagen, se puede controlar dicha política con el campo pull_policy. Ejemplo:
services:
database:
image: 'postgres:15.1'
- ports: permite indicar qué puerto o puertos del contenedor se van a exponer. La forma de exponerlos debe ser: [HOST:]CONTAINER[/PROTOCOL]. Por ejemplo, supongamos que queremos hacer la base de datos Postgres, que corre internamente en el puerto 5432, en el puerto 5555. La forma de hacerlo sería la siguiente:
services:
database:
image: 'postgres:15.1'
ports:
- "5555:5432"
-
restart: define la política a aplicar cuando termina la ejecución de un contenedor (por ejemplo, cuando se da un error. Los valores posibles son:
- no: el contenedor no se reinicia.
- always: el contenedor siempre se reinicia.
- on-failure: el contenedor se reinicia solo si hay un error.
services:
database:
image: 'postgres:15.1'
restart: always
app:
build: './application'
restart: always
depends_on: - database
- env_file: añade variables de entorno a un contenedor basado en uno o varios fichero .env. Ejemplo:
PORT=8080
DB_USER=user
DB_PASS=password
services:
database:
image: 'postgres:15.1'
restart: always
app:
build: './application'
restart: always
depends_on:
- database
env_file: ./application/.env
- environment: si, por el contrario, preferimos definir las variables de forma manual (porque no estamos acostumbrados a trabajar con ficheros .env, por ejemplo, podemos hacerlo también definiendo las variables en environment.
services:
database:
image: 'postgres:15.1'
restart: always
app:
build: './application'
restart: always
depends*on:
- database
environment:
- PORT=8080
- DB_USER=user
- DB_PASS=password
_Personalmente recomiendo utilizar los ficheros .env, ya que facilita mucho no filtrar las contraseñas a lugares sensibles, como puede ser Docker. Añadir las contraseñas directamente en el código no es una buena práctica.*
- volumes: define rutas de mount o volúmenes que deben ser accesibles mediante otros servicios. En otras palabras, nos permite indicar qué carpetas queremos que sean copiadas de local a nuestro contenedor. Además, si queremos que esa carpeta sea accesible por otros servicios, deberemos indicarlo en una sección superior, tal como veremos más adelante. Ejemplo:
services:
database:
image: 'postgres:15.1'
restart: always
volumes: - ./db-data:/var/lib/postgresql/data
app:
build: './application'
restart: always
depends_on:
- database:
- env_file: ./application/.env
- volumes:
- './application:/application'
Estas serían los parámetros principales que se suelen usar a la hora de definir un servicio. Sin embargo, hay muchos más y los puedes encontrar aquí.
Ahora, sabiendo esto veamos cómo definiríamos los servicios de nuestra app, base de datos y API:
services:
database:
image: 'postgres:15.1'
restart: always
volumes:
- ./db-data:/var/lib/postgresql/data
app:
build: './application'
restart: always
depends_on:
- database - api
env_file: ./application/.env
volumes:
- './application:/application'
app:
build: './api'
restart: always
env_file: ./api/.env
volumes:
- './api:/api'
Perfecto, ahora que ya sabemos cómo definir un servicio, veamos cómo definir las redes en Docker Compose.
Las redes es la capa que permite conectar a los servicios entre sí. Por ejemplo, en nuestro caso queremos que la aplicación pueda acceder a la base de datos. Además, la aplicación deberá poder acceder también a la API, pero la API y la base de datos no tienen por qué estar conectadas.
El funcionamiento del network es el siguiente:
- A cada servicio indicamos el nombre del network al que pertenece, usando el parámetro networks.
- En la sección networks indicamos la configuración de cada uno de los networks.
Dicho esto, aunque la configuración puede ser mucho más compleja, yo personalmente recomiendo utilizar el funcionamiento predefinido, ya que es lo más simple. Para ello, para cada network tendremos que indicar que el driver sea bridge.
Una bridge network genera un espacio de red privado para que los contenedores se comuniquen entre sí, junto con la resolución automática de DNS para los nombres de host de los contenedores.
En este caso, una opción sería crear dos redes: una para el frontend y otra para el backend. Ejemplo:
services:
database:
image: 'postgres:15.1'
restart: always
volumes:
- ./db-data:/var/lib/postgresql/data
app:
build: './application'
restart: always
depends_on:
- database - api
env_file: ./application/.env
volumes:
- './application:/application'
app:
build: './api'
restart: always
env_file: ./api/.env
volumes:
- './api:/api'
networks:
- backend
networks:
database:
driver: bridge
backend:
driver: bridge
Importante, a nivel de código, para acceder desde un servicio a otro, la forma más sencilla (y recomendada) de hacerlo es usando el alias del contenedor. Por ejemplo, si yo me quiero conectar desde app a database, sería indicando que el host donde se ubica la base de datos database (nombre del servicio).
Los volúmenes son formas de persistir la información, tal como hemos visto previamente en la sección de servicios. En este sentido, si queremos que varios contenedores accedan al mismo volumen, deberemos crear la sección volumes.
Por ejemplo, supongamos que vamos a crear un sistema de backup de los datos de la base de datos. Para ello, tendremos:
- La base de datos que guardará su carpeta correspondiente.
- Otro servicio que tendrá que acceder a dichos datos y realizar el proceso de backup.
Este ejemplo se realizaría de la siguiente manera:
services:
database:
image: 'postgres:15.1'
volumes:
- db-data:/var/lib/postgresql/data
backup:
image: backup-service
volumes:
- db-data:/var/lib/backup/data
volumes:
db-data:
Al igual que en el caso de la redes, existen parámetros para configurar aún más la gestión de volúmenes. Sin embargo, lo más recomendado suele ser que Docker lo gestione automáticamente (sin definir parámetros). Sin embargo, si quieres profundizar al respecto puedes aprender más sobre dichos parámetros aquí.
Al igual que la sección volumes permite la gestión y persistencia de datos, la sección configs sigue la misma idea, pero para la configuración de servicios.
Supongamos, por ejemplo, que tienes un servidor Apache dentro de tu Docker Compose. Es probable que quieras cambiar la configuración de dicho servicio, pero claro, tener que hacer un build de la imagen cada vez que lo modificas no es algo muy óptimo.
En su lugar, a la hora de definir el servicio puedes indicar que se le debe aplicar una configuración y, en la sección config puedes explicar dicha configuración.
En este sentido, hay tres formas de definir la configuración:
- file: la configuración se crea a partir de un fichero en local.
- external: si se fija como True indica que la configuración ya se ha creado. Sirve para asegurarnos de que no se modifica algo que ya se ha configurado previamente.
- name: el nombre del config en Docker. Se puede fijar en caso de haber indicado external: True.
Ejemplo:
services:
redis:
image: redis:latest
configs:
- redis_conf
configs:
redis_conf:
file: ./redis/redis.conf
La sección secrets es una idea similar a la de configs, pero para permitir acceso a información sensible, tales como contraseñas o API Keys. Al igual que en el caso de config, se pueden definir secrets de varias formas:
- file: el secret es creado con el contenido de un fichero.
- environment: el secret se crea con el valor de una variable de entorno de tu sistema.
- external: si se fija como True indica que la configuración ya se ha creado. Sirve para asegurarnos de que no se modifica algo que ya se ha configurado previamente.
- name: el nombre del secret en Docker. Se puede fijar en caso de haber indicado external: True.
services:
myapp:
image: myapp:latest
secrets:
- api
secrets:
my_secret:
file: ./my_secret.txt
Perfecto, ahora ya sabemos qué es Docker compose y cómo podemos definir nuestro dichero compose.yaml (o docker-compose.yaml, que aunque esté deprecado su uso, en internet seguro que lo encuentras así). Ahora, veámos cómo puedes levantar tus servicios, pararlos, etc.
Docker Compose cuenta con varios comandos. Aunque puedes encontrarlos todos listados aquí, te voy a hacer un breve resumen de los comandos más importantes de Docker Compose:
- docker compose up: es el comando más utilizado, ya que con este comando automáticamente hace el build, empieza los contenedores, los añade al servicio, crea las redes… En definitiva, si quieres levantar un servicio usando Docker Compose, este es tu comando. Aunque admite muchos argumentos (enlace), el más utilizado suele ser -d, ya que permite ejecutar los contenedores en el background.
Si ejecutas docker-compose up y ya hay servicios que ya se están ejecutando, Docker simplemente iniciará aquellos servicios que no se estén ejecutando y creará las redes y volúmenes que no estén ya creadas.
- docker compose down: este comando sirve para parar y eliminar los contenedores, redes, volúmenes e imágenes creadas por docker compose up. Los argumentos más comunes son: --rmi el cual elimina las imágenes usadas por el servicio y -v el cual elimina los volúmenes definidos en la sección volumes.
- docker compose ps: permite listar los contenedores de un proyecto de Compose, indicando su estado actual y los puertos que tienen expuestos. Suele ser interesante aplicarlo tras docker compose up para comprobar que todo está bien.
- docker compose logs: permite ver los logs de un contenedor en concreto. Este comando se suele utilizar cuando levantas el Compose en forma dettached (docker compose up -d). El argumento más utilizado es -f para seguir los logs que genere el servicio.
Así pues, el flujo de trabajo en Docker Compose suele ser el siguiente:
- Definir el fichero compose.yaml.
- Hacer un docker-compose up -d para hacer el build de las imágenes y lanzar todo el Compose.
- Ejecutar docker-compose ps para comprobar el estado de los diferentes contenedores que se han creado.
- Ejecutar docker compose logs -f <service_name> para ver los logs que genera un servicio en concreto, en caso de que haga falta hacer debugging.
- Usar docker compose down cuando no vayas a usar más el servicio.
Como ves, no son tantos los comandos de Docker Compose y el flujo de trabajo es también bastante simple.
Como puedes ver, Docker Compose es una herramienta muy potente para el desarrollo de proyectos basados en Docker. Pero no solo eso, y es que un aspecto muy interesante de Docker Compose es que permite cambiar el flujo de trabajo, pasando de trabajar en local para desplegar en un entorno a, directamente, poder desarrollar en un entorno.
Y es que, siguiendo el ejemplo del proyecto con front, base de datos y API, en vez de tener la base de datos en local y desarrollar todo en local, podríamos crear todo en Docker, crear volúmenes, de tal forma que los cambios que hagamos en los ficheros se vayan aplicando al contenido de los contenedores y así, podríamos trabajar directamente sobre el entorno final.
Personalmente esto me parece muy interesante, puesto que evita uno de los grandes problemas que pueden llegar a darse cuando pones cosas en producción: incompatibilidades por problemas de versiones, Sistemas Operativos, etc. Usar Docker Compose para desarrollo es una buena forma de evitar todos esos quebraderos de cabeza.
Cristian Chiera