Docker Compose para administración de contenedores

abr. 1, 2024

Este artículo forma parte de una serie:

Habiendo explorado los fundamentos de Docker y su poder para desplegar aplicaciones encapsuladas, nos adentramos ahora en el dominio de Docker Compose, una herramienta esencial para el manejo de aplicaciones compuestas por múltiples contenedores. En este artículo desvelaremos cómo Docker Compose simplifica la vida de los desarrolladores, permitiendo la definición, ejecución y gestión de servicios multi-contenedor con facilidad.

La instalación de Docker Compose se puede realizar junto con Docker Desktop para Windows y Mac, mientras que para Linux, se debe instalar manualmente con un simple apt install docker-compose en distribuciones derivadas de Debian como Ubuntu. Una vez instalado, el primer paso es crear un archivo docker-compose.yml en la raíz de tu proyecto, definiendo los servicios, redes y volúmenes que tu aplicación necesite. A veces podrás verlos con la extensión .yaml en lugar de.yml, ambos son válidos.

En este artículo, profundizaremos en el uso de Docker Compose, una herramienta indispensable para definir y ejecutar aplicaciones Docker multi-contenedor de manera eficiente. A partir del ejemplo del capítulo anterior, donde desplegamos un servidor Nginx usando el comando docker run.

Docker Compose permite organizar la configuración de nuestros servicios en un archivo YAML, simplificando el proceso de despliegue y gestión de contenedores que forman parte de una misma aplicación. Veamos cómo se estructura este archivo para el caso de nuestro Nginx:


    version: '3.3'
    services:
      mynginx:
        image: nginx
        ports:
          - "8080:80"
        restart: always
  

En este archivo, la clave version indica la versión de la sintaxis de Docker Compose que estamos utilizando, mientras que la clave services define los servicios que componen nuestra aplicación. En este caso, hemos definido un servicio llamado mynginx que utiliza la imagen oficial de Nginx, exponiendo el puerto 80 del contenedor en el puerto 8080 de nuestro host. Además, hemos especificado que el servicio se reinicie automáticamente en caso de fallo.

El resultado final en la práctica es el mismo, pero como podemos ya intuir, la gestión de la aplicación se simplifica enormemente. Bastaría con ejecutar el comando docker-compose up en la misma carpeta donde se encuentra el archivo docker-compose.yml para lanzar nuestro contenedor. Podríamos acceder al servidor Nginx en el puerto 8080 tal y cómo hacíamos en el ejemplo anterior a través de la dirección http://localhost:8080.

Para casos como este en los que gestionaremos una única “aplicación”, puede parecer exagerado. Sin embargo, cuando se trata de aplicaciones más complejas, con múltiples servicios y dependencias, Docker Compose se convierte en una herramienta indispensable. En futuros artículos exploraremos cómo facilita la gestión de aplicaciones más complejas, permitiendo la definición de redes, volúmenes y dependencias entre servicios.

En nuestro último capítulo de la serie hemos creado una API creada con ExpressJS virtualizada utilizando Docker. Vamos a convertirla a nuestra nueva metodología con docker-compose. Antes almacenábamos los datos en memoria por lo que no había persistencia de datos tras el reinicio, aprovecharemos la flexibilidad de docker-compose para añadirle después una base de datos a la que conectaremos para conservar los datos de nuestra aplicación. A continuación el fichero docker-compose.yml que lanzaría un contenedor similar al de nuestro ejemplo de la API:


    version: '3.8'
    services:
      app:
        image: node:14-alpine
        command: sh -c "npm install && node app.js"
        volumes:
          - ./:/app
        working_dir: /app
        ports:
          - "3000:3000"
        environment:
          NODE_ENV: development
  

En este docker-compose.yml hemos definido un servicio llamado app que utiliza la imagen oficial de Node.js en su versión 14 con Alpine Linux. Hemos especificado un comando que instala las dependencias de nuestra aplicación y arranca el servidor, así como un volumen que monta el directorio actual en el contenedor. Además, hemos expuesto el puerto 3000 hacia el host y hemos definido una variable de entorno NODE_ENV con el entorno de desarrollo de Node (NODE_ENV) establecido con el valor development.

Para que esto funcione es necesario iniciar el proyecto NPM con npm init -y y crear un archivo app.js con el siguiente contenido, extraído de nuestro capítulo creando una API de ejemplo:


    const express = require('express');
    const app = express();
    const PORT = 3000;
    
    app.use(express.json());
    
    app.get('/', (req, res) => {
        res.send('¡Hola Mundo con Express y Docker!');
    });
    
    app.listen(PORT, () => {
        console.log(`Servidor corriendo en http://localhost:${PORT}`);
    });
    

Ahora utilizaremos una base de datos PostgreSQL para hacer persistentes los datos de nuestra aplicación y que no se pierdan. Para ello añadimos el siguiente servicio a nuestro docker-compose.yml y el parámetro depends_on al servicio, este hace que la aplicación espere hasta que la base de datos esté lista antes de arrancar:


    version: '3.8'
    services:
      app:
        image: node:14-alpine
        command: sh -c "npm install && node app.js"
        depends_on:
          - db
        volumes:
          - ./:/app
        working_dir: /app
        ports:
          - "3000:3000"
        environment:
          NODE_ENV: development
      db:
        image: postgres:13
        ports:
          - "5432:5432"
        environment:
          POSTGRES_USER: user
          POSTGRES_PASSWORD: password
          POSTGRES_DB: app
  

La parte de la base de datos db es bastante sencilla, basándonos en la imagen oficial de PostgreSQL en su versión 13, asociamos el puerto 5432 de PostgreSQL en el contenedor ( a la derecha) con el puerto 5432 del host (a la izquierda), en este caso coinciden pero no tiene por qué ser así, como ya hemos visto antes. Además, hemos definido tres variables de entorno para configurar la base de datos: POSTGRES_USER, POSTGRES_PASSWORDy POSTGRES_DB. No está de más mencionar que estos datos son de ejemplo y no deberían utilizarse nunca en producción.

Con esto ya tenemos creada la definición de nuestra API Express en nuestro docker-compose junto a una base de datos PostgreSQL que podremos levantar juntos con un simple comando:


    docker-compose up -d
    

El comando -d es opcional y significa que se ejecutará en segundo plano, por lo que puedes cerrar el terminal desde el que lo has ejecutado y el servicio seguirá corriendo. Llegados a este punto, podríamos conectar a nuestra base de datos para ver que realmente se encuentra ahí utilizando un cliente de base de datos como por ejemplo DBeaver, un programa open source que nos permite conectar a múltiples tipos de bases de datos, visualizar su contenido y administrarlas. Por ahora, debería contener una base de datos de nombre app y estar vacía.

En el próximo artículo, crearemos las tablas de nuestra base de datos para almacenar los de nuestro ejemplo de la API y exploraremos cómo conectar nuestra aplicación. Veremos también un caso práctico y profundo sobre cómo analizar los logs y cómo consultar las diferentes configuraciones que Docker ha definido para nuestros contenedores. Te invito a seguir investigando por tu cuenta y a estar atento a nuestro próximo capítulo de la serie.

Artículos relacionados

Quizá te puedan interesar