Optimizar un Entorno de Desarrollo con Docker y Docker-Compose

Docker ha evolucionado a tal punto de ser una herramienta indispensable no solo para el despliegue de aplicaciones sino también, como veremos en este caso, como una herramienta que facilite el desarrollo.

El problema

En muchas ocasiones como desarrolladores tenemos que trabajar en múltiples proyectos y en diferentes lenguajes y frameworks, a esto se suma una gran cantidad de herramientas y librerías que tenemos que instalar en nuestro sistema ya sea para aprender cosas nuevas o por requerimiento del proyecto. Esto es necesario, y una vez que el proyecto esta funcionado de manera adecuada todo es grandioso, pero que tal si por alguna razón tenemos que cambiar de computador o migrar a otro sistema operativo, el proceso que tenemos que hacer para poner nuevamente nuestro entorno de desarrollo en marcha puede consumir mucho tiempo y esfuerzo.

¿Por qué utilizar Docker con Docker Compose?

Docker y Docker Compose nos brindan algunos beneficios:

  • No necesitamos instalar ni mantener software adicional en nuestro equipo
  • Podemos tener todo nuestro entorno de desarrollo en un único repositorio, por ejemplo, tenemos el backend, el frontend y las configuraciones de la base de datos en un mismo repositorio lo que facilita a los desarrolladores el poder colaborar de mejor manera en el proyecto.
  • Levantar todo el entorno de desarrollo se limita a un solo comando docker-compose up

Pero no todo es color de rosa, también tenemos una desventaja que está enfocada al rendimiento, ya que a pesar de que los contenedores están enfocados a ser eficientes, siguen consumiendo recursos de la maquina anfitrión tales como procesador y memoria por lo que si la cantidad de contenedores que están corriendo al mismo tiempo es grande o si los contenedores son pesados al momento de crearse y levantarse, podrían llevar a cuelgues del sistema y cosas similares.

Instalación

Docker Compose depende del motor de Docker (Docker Engine) por lo que antes de empezar a utilizar Docker Compose debemos ya tener instalado Docker en el Sistema.

En Windows o en Mac: Docker Compose ya viene incluido dentro del paquete de instalación de Docker Desktop.

En Linux primero debemos instalar el motor de Docker y luego seguir unos sencillos pasos descritos en la documentación oficial: https://docs.docker.com/engine/install/

Ejemplo práctico

El código de este ejemplo se encuentra disponible en github: https://github.com/JhymerMartinez/tutorial-docker-compose

Vamos a construir una aplicación para gestionar listas de tareas, tanto el backend como el frontend están en carpetas distintas e independientes. Los componentes que conforman este ejemplo son:

  • Backend: Api Rest construida en Express JS encargada de la creación, recuperación y eliminación de tareas.
  • Base de Datos: MongoDB será la base de datos no relacional donde se almacenarán los datos.
  • Frontend: Construido en React JS y es el encargado de presentar de manera visual las tareas mediante el uso de componentes.

Paso 1. Agregar los Dockerfile

El Dockerfile es un archivo en texto plano en el cual se especifica una serie de instrucciones a manera de receta para crear una imagen. A una imagen la podemos considerar como una plantilla base para crear el contenedor que contendrá el código de nuestra aplicación.

En el Backend:


# Imagen base de node con una distribución ligera 
# de Linux llamada Alpine
FROM node:12.16.0-alpine

# Directorio que se crea en el contenedor y 
# en donde se ejecutarán otras instrucciones como CMD, 
# COPY o RUN
WORKDIR /app

# Copiar archivo con las dependencias del backend
COPY package.json ./

# Instalar dependencias
RUN npm install

# Copiar todos los demás archivos excepto los 
# ignorados en el archivo .dockerignore
COPY . .

# Puerto que se expone hacia el sistema anfitrión 
EXPOSE 8000

# Valores por defecto para el contenedor 
# en este caso se incluye un ejecutable
CMD ["npm", "start"]

En el Frontend:

# Imagen base de node con una distribución ligera 
# de Linux llamada Alpine
FROM node:12.16.0-alpine

# Directorio que se crea en el contenedor y 
# en donde se ejecutarán otras instrucciones como CMD, 
# COPY o RUN
WORKDIR /app

# Copiar archivo con las dependencias del frontend
COPY package.json ./

# Instalar dependencias
RUN yarn install

# Copiar todos los demas archivos excepto los 
# ignorados en el archivo .dockerignore
COPY . .

# Puerto que se expone hacia el sistema anfitrión 
EXPOSE 3000

# Valores por defecto para el contenedor 
# en este caso se incluye un ejecutable
CMD ["yarn", "start"]

Paso 2. Agregar las variables de entorno

Docker Compose nos facilita el poder utilizar un archivo independiente con las variables de entorno necesarias para el funcionamiento del proyecto.

En el backend crearemos un archivo  .env.development en /todo-backend con la siguiente estructura:

NODE_ENV=development
MONGODB_URL=mongodb://mdb:27017/todo-app-db
PORT=8000

Lo que podemos notar en este archivo es que para realizar la conexión con el contenedor de MongoDB desde el contenedor del Backend utilizamos el nombre del servicio que vamos a especificar en el archivo docker-compose.yml, en este caso es mdb. Esta es la manera en la que se puede realizar la comunicación entre contenedores que han sido especificados como servicios en Docker Compose.

Ahora en el frontend crearemos un archivo  .env.development en /todo-frontend, aquí simplemente tenemos la ruta en la que estará el API:

REACT_APP_API_URL=http://localhost:8000

Paso 3. Agregar el archivo docker-compose.yml

version: '3'
services:
  # Nombre del servicio
  backend:
    # Nombre de la imagen que será generada
    image: todo-backend-image
    # Ruta en donde se encuentra el archivo Dockerfile
    build: ./todo-backend
    # Nombre del contenedor resultante
    container_name: todo-backend
    # Reiniciar el contenedor en caso de ocurrir algún error
    restart: always
    # Ruta al archivo con las variables de entorno
    env_file:
      - ./todo-backend/.env.development
    # Puerto que se expone hacia el sistema anfitrión 
    # desde el contenedor 
    ports:
      - "8000:8000"
    # Directorios del sistema anfitrión que se enlazan 
    # con los del contenedor. El primero es para el
    # código del backend y el segundo para las dependencias
    volumes:
      - ./todo-backend:/app
      - ./todo-backend/node_modules:/app/node_modules
    # Otros servicios de los cuales depende este contenedor 
    depends_on:
      - mdb
    # Red de contenedores a la que se agrega el 
    # contenedor del backend
    networks:
      - todo-network
    # Similar a los especificados en el Dockerfile
    command: sh -c "npm install && npm start"

  frontend:
    image: todo-frontend-image
    build: ./todo-frontend
    container_name: todo-frontend
    restart: always
    # Mantiene la conexión con el terminal del contenedor
    # en modo interactivo
    stdin_open: true
    env_file:
      - ./todo-frontend/.env.development
    ports:
      - "3000:3000"
    volumes:
      - ./todo-frontend:/app
      - ./todo-frontend/node_modules:/app/node_modules
    networks:
      - todo-network
    command: sh -c "yarn install && yarn start"

  mdb:
    image: mongo:3.4
    container_name: todo-mongodb
    restart: always
    volumes:
      - ./mongodb/data:/data/db
    ports:
      - "27017:27017"
    networks:
      - todo-network

networks:
  todo-network:

Paso 4. Levantar el proyecto

Una vez lista la configuración solo nos resta ejecutar docker-compose up en la raíz del repositorio. Este comando se encargara de construir, iniciar y enlazar los contenedores como servicios. Este proceso puede tomar un poco de tiempo si es la primera vez debido a que tienen que descargarse y construirse las imágenes (si no han sido descargadas previamente) y también descargarse las dependencias del backend y del frontend. Una vez terminado este proceso podemos ir al navegador  y ver el siguiente resultado:

Conclusión

La opción que te he presentado aquí es solo una manera en que podemos utilizar Docker y Docker Compose para facilitarnos el desarrollo, podemos probar con distintos entornos y con diferentes lenguajes, la idea es conseguir optimizar al máximo nuestro entorno de desarrollo y enfocarnos en el principal objetivo que es escribir código de calidad.