En este tutorial voy a explicar todo lo que necesitas para aprender a usar Docker. Con Docker simplificarás mucho tanto tu desarrollo en local como tus despliegues en producción. Al final de este tutorial, sabrás crear contenedores, administrarlos y gestionarlos. Además de ver Docker, también veremos Docker Compose, una herramienta que te permitirá sincronizar todos tus contenedores a partir de archivos muy sencillos.
Contenidos
- 1 Requisitos
- 2 Introducción a Docker
- 3 Instalación de Docker
- 4 Gestión de Imágenes en Docker
- 5 Gestión de Contenedores en Docker
- 5.1 Cómo crear un contenedor
- 5.2 Cómo iniciar un contenedor
- 5.3 Cómo ver una lista de contenedores
- 5.4 Cómo parar un contenedor
- 5.5 Cómo eliminar un contenedor
- 5.6 Cómo nombrar un contenedor
- 5.7 Cómo asignar un puerto a un contenedor
- 5.8 Cómo ver el log de un contenedor
- 5.9 Ejecución directa de imágenes con run
- 6 Gestión de redes internas en Docker
- 7 Gestión de aplicaciones con Docker
- 8 Gestión con Docker Compose
- 9 Gestión de Volúmenes con Docker
- 10 Entornos de desarrollo con Docker
Requisitos
Antes de comenzar el tutorial, necesitarás ciertos conocimientos básicos de la terminal de comandos y de Node,.js. Si no los tienes, puedes consultar los siguientes tutoriales.
- Aprende a usar la línea de comandos: Tutorial de introducción a la línea de comandos.
- Aprende a usar Node.js: Tutorial de introducción a Node.js.
Y esto es todo. A continuación veremos una introducción a Docker y a la virtualización.
Introducción a Docker
Antes de comenzar, vamos a explicar ciertos conceptos, como cuáles son los tipos de virtualización que existen o qué es un contenedor.
Virtualización
La virtualización es un proceso de emulación en donde un sistema operativo anfitrión o host emula un sistema operativo cliente. Por ejemplo, podías emular Debian Linux sobre Ubuntu o Windows como anfitriones.
Existen tres tipos de virtualización, que son los siguientes:
- Paravirtualización: Es un tipo de virtualización en donde un sistema operativo anfitrión entrega la mayor cantidad de acceso a su hardware al sistema cliente, por lo que apenas se virtualizan componentes hardware.
- Virtualización parcial: Mediante este tipo de virtualización, solamente algunos componente hardware del cliente se virtualizan en el sistema anfitrión.
- Virtualización completa: Mediante una virtualización completa, todos los componentes hardware del cliente son virtualizados por el anfitrión.
Docker no usa ninguno de estos tipos de virtualización, ya que usa directamente el Kernel del sistema anfitrión. Esto se traduce en un rendimiento muy superior a todas las alternativas de virtualización.
Qué es un contenedor
Un contenedor es una forma de empaquetar tus aplicaciones junto con todas las dependencias que tenga, incluyendo sus archivos de configuración.
Usando contenedores podremos generar un paquete que contendrá el código HTML, el código PHP, el código JS o el código de cualquier otro lenguaje de nuestras aplicaciones. Estos paquetes son portables, por lo que podremos compartirlos de una forma muy sencilla con otros desarrolladores o administradores. Se comparten mediante imágenes creadas a partir de los contenedores.
En comparación con una máquina virtual, un contenedor es muy ligero. Un imagen de un contenedor puede pesar unos pocos MB, mientras que una máquina virtual puede llegar a pesar varios GB.
Una máquina virtual necesita un Hardware, un Kernel y finalmente las aplicaciones que usarás de la máquina virtual. Este Kernel podría ser el de Linux o el de Windows. En el caso de Docker, únicamente se virtualiza la capa de aplicaciones, ya que el Kernel que se usa es el del propio sistema operativo anfitrión.
Por qué usar contenedores
Antes de usar contenedores, teníamos un problema, y es que un usuario puede estar usando Windows con la versión 18 de Node.js, mientras que otro podría estar usando la versión 20 de Node.js desde Linux. Esto mismo podría repetirse con MySQL u otra dependencia. Esto puede ocasionar conflictos, ya que ciertas funcionalidades pueden funcionar correctamente desde un entorno pero no desde otro. Además, este mismo esquema se repite para cualquier desarrollador que se una al equipo, siendo muy extraño que todos los desarrolladores compartan exactamente el mismo Stack.
La instalación de las dependencias en el mismo sistema operativo puede también ser muy diferente, por lo que en algún momento está claro que tendremos conflictos. Las aplicaciones tienen múltiples dependencias, por lo que muchas veces habrá alguna dependencia que difiera con la de otro desarrollador.
Afortunadamente, si usas contenedores, podrás automatizar todo este proceso mediante unos pocos comandos. Lo que haremos será generar un empaquetado que contenga el código de nuestra aplicación con independencia del lenguaje que usemos, así como Node.js, MySQL y los archivos de configuración o variables de entorno.
Tipos de contenedores
Los contenedores se almacenan en repositorios especializados en contenedores del mismo modo que almacenas tu código en GitHub. Existen dos tipos de repositorios para contenedores:
- Repositorios públicos: Son repositorios de acceso público. El repositorio público más conocido es DockerHub, en donde encontrarás contenedores con Node.js, MySQL, Postgres, Python, Golang y más. También encontrarás contenedores con las diferentes distribuciones de Linux.
- Repositorios privados: Son repositorios privados que no se comparten de forma pública, por lo que tendrás que obtener permiso de acceso.
Imágenes de los contenedores
Los contenedores se comparten mediante imágenes. Si por ejemplo necesitas un contenedor que contenga Alpine Linux, entonces te descargarás la imagen de Alpine Linux desde DockerHub o cualquier otro repositorio. Podrás poner a funcionar este contenedor un tu ordenador una vez descargado. Pero no solo esto, ya que además podrías tener otras imágenes tanto de Alpine Linux como de Ubuntu o de cualquier otra distribución funcionando a la vez y sin conflictos entre ellas
Una imagen es un empaquetado que contiene tanto las dependencias como el código necesario para que una aplicación funcione. La imagen es aquello que compartirás con tus compañeros de desarrollo. A partir de las imágenes, se crearán los contenedores en tu sistema. Mediante la ejecución de un comando mantendrás todas las dependencias funcionando en consonancia con el entorno.
Una imagen no es lo mismo que un contenedor. Las imágenes pueden existir sin un contenedor, mientras que los contenedores necesitan ejecutar una imagen. Un contenedor Docker es una aplicación o servicio de software autónomo y ejecutable. Por otro lado, una imagen de Docker es la plantilla cargada en el contenedor para ejecutarlo, como un conjunto de instrucciones. Almacenarás imágenes para compartirlas y reutilizarlas, pero crearás y destruirás contenedores continuamente.
Por otro lado, los contenedores son capas de imágenes en donde la capa inferior suele ser una distribución de Linux. La más usada es Alpine Linux por ser bastante ligera. Sobre esta imagen se montan más imágenes hasta llegar a la capa de aplicación, que puede incluir Node.js, Python, PHP, Laravel o cualquier otra aplicación. La gran ventaja de los contenedores es que su estructura de capas las hace muy ligeras.
Despliegue con contenedores
Cuando no usas contenedores, el equipo de operaciones debe obtener el código el equipo de desarrollo, asegurarse de que no existen conflictos entre las dependencias, revisar los archivos de configuración y levantar nuevos servidores si es necesario. Esto es muy propenso a acarrear problemas, ya que el equipo de desarrollo puede haber incluido código que funciona con una versión de una dependencia pero no con la versión de producción. Nadie sabrá la existencia del problema hasta que la aplicación esté en producción y falle.
Estos problemas no existen cuando usas contenedores, ya que los desarrolladores utilizan una imagen que contiene las mismas versiones de todas las dependencias necesarias. Esta imagen necesita únicamente que los desarrolladores tengan instalado el entorno de ejecución de Docker en su sistema.
Instalación de Docker
Vamos a ver cómo instalar la herramientas de escritorio de Docker, llamada Docker Desktop, que además también incluye otras herramientas como Docker Compose:
- Docker Desktop: Docker Desktop es una máquina virtual que permite ejecutar contenedores y que funciona con Linux, estando optimizada para este sistema. La máquina virtual permite acceder tanto al sistema de archivos como a la red tanto interna como externa.
- Docker Compose: Docker Compose es una herramienta de línea de comandos que, tal y como veremos más adelante, permite orquestrar contenedores.
En Windows, Docker funcionará de forma nativa gracias a WSL2. Se trata del subsistema de Linux para Windows, que viene activado por defecto en las versiones recientes de Windows. Si usas una versión antigua, entonces consulta el tutorial de activación de WSL en Windows.
Para instalar Docker accede a la página de instalación de Docker Desktop. Esta página mostrará todas las alternativas de instalación según tu sistema. En Windows se mostrará un enlace de descarga a la herramienta de instalación. Esto es por ejemplo lo que verás en Windows:
Si instalas Docker en MacOS, verás un enlace de descarga para Mac con Apple Chip. Docker Desktop tiene también paquetes precompilados para Debian, Fedora, Ubuntu o Arch. Lo único que necesitarás para instalar Docker son permisos de administrador.
Una vez instalado Docker, ejecuta el acceso directo creado para Docker Desktop en tu escritorio. Una vez iniciado, deberías poder ver algo así:
Desde aquí podrás ver accesos a los contenedores, imágenes, volúmenes y entornos. Sin embargo, en este tutorial usaremos la línea de comandos para casi todo.
Si accedes a Docker Hub podrás encontrar una librería de imágenes de contenedores creados por otros usuarios. Realmente, aquí encontrarás contenedores para todo aquello que puedas necesitar.
En Docker Hub podrás encontrar diferentes verisones para cada imagen, funcionando con diferentes sistemas o versiones. Por ejemplo, podrías querer usar un contenedor con la versión 5 de MySQL y otro con la versión 8. Además podrás ver los comandos necesarios para descargar cada imagen.
Gestión de Imágenes en Docker
Vamos a ver ahora los comandos necesarios para gestionar imágenes en Docker. Para ejecutarlos, deberás asegurarte de que Docker Desktop está funcionando. Para comenzar, debes iniciar la terminal de comandos. Si usas Windows, es recomendable que uses una terminal de Linux o Git Bash, por ejemplo.
Cómo ver una lista de imágenes
Para ver una lista de imágenes debes ejecutar el comando docker images
, que mostrará un listado completo con todas las imágenes descargadas. Al principio la lista estará vacía. En mi caso, estas son las imágenes instaladas:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
node 20 281da7fc7f6f 3 weeks ago 1.1GB
node latest 1b9d5f3b36bf 3 weeks ago 1.1GB
mongo latest b8df2163f9aa 5 weeks ago 755MB
mysql latest a88c3e85e887 7 weeks ago 632MB
Cómo descargar una imagen
Para descargar una imagen, debes usar el comando docker pull IMAGEN
, reemplazando IMAGEN
por el nombre de la imagen a descargar. Por ejemplo, para descargar la imagen de Node.js usarías este comando:
$ docker pull node
Esto descargará la imagen de la última versión de Node.js por defecto. Tal y como podrás apreciar en la terminal, se descargarán varias capas, que son necesaras para la ejecución de Docker. Si estas capas son necesarias para la ejecución de otros contenedores, estas se descargarán una única vez, siendo así un proceso muy eficiente.
Si ahora ejecutas de nuevo el comando docker images
, verás que se habrá descargado la imagen de Node, con la etiqueta latest
. Verás también el identificador o ID de la imagen, la fecha de creación y el tamaño que ocupa.
Ahora vamos a descargar una versión específica de la imagen anterior, indicando una etiqueta con la versión. Para ello debes ejecutar el comando docker pull IMAGEN:VERSION
, reemplazando IMAGEN
por el nombre de la imagen a descargar y VERSION
por la versión deseada. En este ejemplo, descargaremos la versión 20 de Node.js:
docker pull node:18
Ahora verás que la ejecución es muy rápida, ya que no se descargarán las capas ya descargadas.
Si quieres saber el nombre de imagen a descargar, así como las versiones disponibles, siempre puedes consultar Docker Hub. Por ejemplo, podrás ver que el nombre de la imagen de MySQL es myslql
. Para decargar la última versión de MySQL, deberías usar este comando:
docker pull mysql
Para descargar la imagen de la última versión de MongoDB usarías este comando:
docker pull mongo
Cómo eliminar una imagen
Ahora vamos a ver cómo puedes eliminar las imágenes descargadas. Para ello usaremos el comando docker image rm IMAGEN:VERSION
, reemplazando IMAGEN
por el nombre de la imagen a eliminar y VERSION
por la versión deseada. Por ejemplo, para eliminar la imagen de la versión 18 de Node.js usarías este comando:
docker image rm node:18
Si ahora ejecutas el comando docker images
, verás que la imagen de la versión 18 de Node.js se ha eliminado.
Gestión de Contenedores en Docker
En este apartado veremos cómo crear contenedores a partir de imágenes, cómo eliminar contenedores, cómo gestionarlos y cómo conectarnos a ellos, entre otras cosas.
Cómo crear un contenedor
Para crear un contenedor a partir de una imagen debes usar el comando docker create IMAGEN
, reemplazando IMAGEN
por el nombre de la imagen.
Para comenzar, a modo de ejemplo, vamos a descargar la imagen de MongoDB usando este comando:
mongo pull mongo
Para crear un contenedor a partir de la imagen descargada, usarías este comando:
$ docker create mongo
dc941500655c1b452be85cbd2117e0032331a160be4671f3bd5b11cc4bbcf6b9
Se mostrará un identificador como resultado, que es el ID del contenedor creado. Lo necesitaremos para ejecutar el contenedor. También podemos usar el comando docker container create IMAGEN
. De nuevo, reemplazando IMAGEN
por el nombre de la imagen. El resultado será el mismo.
Cómo iniciar un contenedor
Una vez creado un contenedor, podrás iniciarlo usando su identificador mediante el comando docker start ID
, reemplazando ID
por el ID del contenedor que quieras iniciar.
Para iniciar el contenedor que hemos creado, usaremos este comando:
$ docker start dc941500655c1b452be85cbd2117e0032331a160be4671f3bd5b11cc4bbcf6b9
dc941500655c1b452be85cbd2117e0032331a160be4671f3bd5b11cc4bbcf6b9
Se devolverá de nuevo el identificador del contenedor creado.
Cómo ver una lista de contenedores
Para ver una lista con los contenedores creados, debes usar el comando docker ps
. A continuación puedes ver el resultado, en donde verás el contenedor creado:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dc941500655c mongo "docker-entrypoint.s…" 5 minutes ago Up About a minute 27017/tcp gallant_dubinsky
Tal y como ves, se mostrarán varios datos:
- CONTAINER ID: Es un ID más corto. Se trata de otro identificador que puedes usar en lugar del más largo.
- IMAGE: La imagen a partir de la cual se ha creado el contenedor, que en nuestro ejemplo es
mongo
. - COMMAND: El comando que ha usado el contenedor para ejecutarse.
- CREATED: La fecha de creación del contenedor.
- STATUS: El estado actual del contenedor, que en este caso nos indica que lleva activo un minuto.
- PORTS: Los puertos que usa el contenedor, que en este caso es el puerto TCP estandarizado 27017 que usa MongoDB.
- NAMES: El nombre que Docker le ha dado al contenedor y que podrás usar en lugar de su ID.
El comando docker ps
mostrará la lista con los contenedores en ejecución, pero si quisieras ver incluso aquellos que no están en ejecución, tendrás que usar el comando docker ps -a
.
Cómo parar un contenedor
Para parar la ejecución de un contenedor, debes usar el comando docker stop ID
, reemplazando ID
por el ID del contenedor que quieras parar.
Para frenar la ejecución del contenedor que hemos creado en nuestro ejemplo, usarás este comando:
$ docker stop dc941500655c
dc941500655c
Se mostrará como resultado el ID corto del contenedor que hemos parado. Si ejecutas el comando docker ps
, verás que ya no hay ninguno en ejecución, aunque este siga creado.
Si ejecutas el comando docker ps -a
verás que el contenedor ya no está en ejecución:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dc941500655c mongo "docker-entrypoint.s…" 15 minutes ago Exited (0) 3 minutes ago gallant_dubinsky
Aquí podemos ver que ya no hay puertos expuestos y que el contenedor se ha frenado hace tres minutos.
Cómo eliminar un contenedor
Para eliminar un contenedor tendrás que usar el comando docker rm ID
, reemplazando ID
por el ID del contenedor que quieras eliminar.
En este ejemplo eliminamos el contenedor que hemos creado usando su ID:
$ docker container rm dc941500655c
dc941500655c
También podrías usar su nombre para eliminarlo, así como para realizar cualquier otra gestión:
$ docker container rm gallant_dubinsky
gallant_dubinsky
Tal y como ves, usar nombres puede ser más cómodo. La asignación de nombres es aleatoria, pero también puedes decidir un nombre por ti mismo.
Cómo nombrar un contenedor
Puedes darle un nombre específico a un contenedor cuando lo creas. Para ello desbes usar el comando docker create --name NOMBRE IMAGEN
, reemplazando IMAGEN
por el nombre de la imagen y NOMBRE
por el nombre que quieras darle al contenedor.
En este ejemplo creamos un contenedor de mongo con el nombre goku
:
$ docker create --name goku mongo
349e70d5c1639fbe7e7e022b2b540e9b00aefe623bc77010bc4523540dadbda2
Desde ahora, ya podemos hacer referencia al contenedor usando el nombre goku
. Vamos a iniciar el contenedor. Por ejemplo para iniciarlo, podemos usar este comando:
$ docker start goku
goku
Si usamos el comando docker ps
, podemos ver el contenedor en ejecución:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
349e70d5c163 mongo "docker-entrypoint.s…" About a minute ago Up 34 seconds 27017/tcp goku
Cómo asignar un puerto a un contenedor
Los contenedores usan puertos, pero no se mapean por defecto con los puertos de nuestro sistema. Por ejemplo, si un contenedor de Docker como puede ser uno de Node.js usa el puerto 3000
, no accederemos al servidor usando este puerto si no lo redireccionamos. Lo mismo ocurriría con el puerto 27017
de MongoDB.
Para asignar un puerto a un contenedor, usaremos el comando docker create -p:PUERTO_SISTEMA:PUERTO_CONTENEDOR IMAGEN
, reemplazando IMAGEN
por el nombre de la imagen, PUERTO_SISTEMA
por el puerto de nuestro sistema y PUERTO_CONTENEDOR
por el puerto del contenedor.
En este ejemplo vamos a crear un contenedor de mongo en el que redireccionaremos el puerto 27017
de nuestro sistema al puerto 27017
del contenedor. Además le daremos el nombre de bulma
:
$ docker create -p27017:27017 --name bulma mongo
414215c8690cca5cc7f748e470df1fd5b888fd1c39cab4123fbde84c68472356
Ahora inicia el contenedor con el comando docker start bulma
:
$ docker start bulma
bulma
Si ahora ejecutas el comando docker ps
, verás que en el campo PORTS
, se muestra la redirección de puertos que hemos asignado:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
414215c8690c mongo "docker-entrypoint.s…" About a minute ago Up 43 seconds 0.0.0.0:27017->27017/tcp bulma
349e70d5c163 mongo "docker-entrypoint.s…" 10 minutes ago Up 9 minutes 27017/tcp goku
Si no especificamos un puerto en nuestro sistema, Docker escogerá el puerto a usar arbitrariamente. En este ejemplo especificamos únicamente el puerto 27017
del contenedor y además le damos el nombre de vegeta
:
$ docker create -p27017 --name vegeta mongo
238ae5dc3379e3f84827ebc123aad97aea15faf4b71907d1a6a92efc0864757a
Iniciaremos el contenedor mediante el comando docker start
:
$ docker start vegeta
vegeta
Si ahora ejecutas el comando docker ps
, verás que Docker ha decidido redirigir el puerto 51808
de nuestro sistema al puerto 27017
del contenedor:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
238ae5dc3379 mongo "docker-entrypoint.s…" About a minute ago Up 54 seconds 0.0.0.0:51808->27017/tcp vegeta
414215c8690c mongo "docker-entrypoint.s…" 6 minutes ago Up 5 minutes 0.0.0.0:27017->27017/tcp bulma
349e70d5c163 mongo "docker-entrypoint.s…" 15 minutes ago Up 13 minutes 27017/tcp goku
Es recomendable que seas tu quien decida los puertos en lugar de Docker.
Antes de continuar, para la ejecución de los contenedores goku
y vegeta
con el comando docker stop
y elimínalos con el comando docker rm
:
Cómo ver el log de un contenedor
Para ver todo lo que ha sucedido con un contendor puedes consultar el log del mismo. Para ello puedes usar el comando docker logs ID
, reemplazando ID
por el identificador o el nombre del contenedor.
Para ver el log del contenedor bulma
usarías este comando:
$ docker logs bulma
Los logs se muestran en formato JSON.
Para ver en tiempo real los logs de un contenedor, debes usar el flag --follow
para que el comando no te devuelva a la línea de comandos:
$ docker logs --follow bulma
Pulsa CTRL+C
para cerrar el log.
El comando docker run
usa por defecto la opción --follow
.
Ejecución directa de imágenes con run
Existe un método alternativo que te permitirá ejecutar un contenedor sin la necesidad de descargar la imagen del contenedor. Para ello debes ejecutar el comando docker run
:
docker run mongo
Se mostrarán por defecto los logs del contenedor. Pulsa CTRL+C
para dejar de escuchar los logs.
Para ejecutar el comando sin que se escuchen los logs, debes agregar el flag -d
:
docker run -d mongo
Si ejecutas docker ps
podrás ver que el contenedor está en ejecución:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aa43863f91f3 mongo "docker-entrypoint.s…" 15 seconds ago Up 14 seconds 27017/tcp pedantic_shamir
Mediante el comando docker run
también también podrás asignar puertos y nombres a tus contenedores:
docker run --name radix -p27017:27017 -d mongo
Gestión de redes internas en Docker
Los contenedores de Docker se comunican entre sí mediante redes internas. Estas redes se gestionan mediante el comando docker network
.
Muetra una lista de redes
Accede a la terminal y ejecuta el siguiente comando para listar todas las redes configuradas en Docker:
docker network ls
Verás varias redes preconfiguradas por defecto en Docker. Sin embargo, vamos a crear nuestra propia red.
Crea una red
Para crear una red usa el siguiente comando, reemplazando NOMBRERED
por el nombre de la red a crear:
docker network create NOMBRERED
Vamos a crear una red a la que le daremos el nombre de mired
:
docker network create mired
Si ahora ejecutas el comando docker network ls
, deberías ver la nueva red en la lista.
Es importante destacar que el nombre de red asignado representará el host al que nos conectamos Docker.
Elimina una red
Para eliminar una red, debes usar este comando, reemplazando NOMBRERED
por el nombre de la red a eliminar:
docker network rm NOMBRERED
Para eliminar la red mired
, usaríamos este comando:
docker network rm mired
Gestión de aplicaciones con Docker
Vamos dockerizar esta aplicación de ejemplo. Sin embargo, primero veremos cómo conectarnos a un contenedor existente desde la misma.
Antes de continuar, clona la aplicación en un directorio de tu sistema y ejecuta el comando npm install
para instalar sus dependencias.
Se trata de una aplicación muy sencilla que nos permite obtener un listado de platos de una base de datos creada con Mongo. Esto se lleva a cabo mediante una petición GET al endpoint /platos
.
La aplicación también nos permitie crear platos mediante una petición POST el endpoint /platos
, aceptando como valores el nombre y el tipo de plato, que podría ser primero, segundo o postre, por poner un ejemplo.
Crea tu aplicación
Vamos a describir la aplicación que hemos creado. Para empezar, hemos importado el framework Express y el ODM Mongoose, que simplificará el acceso y el trabajo con la base de datos NoSQL Mongo:
import express from 'express'
import mongoose from 'mongoose'
Luego hemos creado la aplicación Express y le hemos indicado que acepte JSON como entrada:
const app = express();
app.use(express.json());
Seguidamente hemos definido el esquema para el modelo Plato
, que contiene los atributos nombre
y tipo
, ambos de tipo cadena.
const Plato = mongoose.model('Plato', new mongoose.Schema({
tipo: String,
tipo: String,
}))
Luego le indicamos a Mongoose que se conecte a la base de datos de Mongo, pasándole la URL de conexión como argumento:
mongoose.connect('mongodb://edu:testpass@localhost:27017/restaurante?authSource=admin')
En esta URL hemos indicado el usuario edu
, el password testpass
y el servidor y el puerto a los que conectarnos, que son localhost
y 27017
respectivamente. Finalmente hemos indicado la base de datos a la que conectarnos que será restaurante
, además del usuario que se conectará, que es el usuario admin
.
El método GET /platos
nos devuelve la lista completa de platos:
app.get('/platos', async (_req, res) => {
console.log('Mostrando lista de platos...')
const platos = await Plato.find();
return res.send(platos)
})
El método POST /platos
, nos permitirá crear un plato tras comprobar que tanto los valores nombre
y tipo
están presentes. Se devolverá un error si no es así o si ocurre algo inesperado:
app.post('/platos', async (req, res) => {
try {
console.log('Creando plato...');
const { nombre, tipo } = req.body;
if (!nombre || !tipo) {
return res.status(400).send('Los campos nombre y tipo son obligatorios');
}
await Plato.create({ nombre, tipo }); // Utiliza las variables extraídas para crear el plato
return res.send('Plato creado con éxito');
} catch (error) {
console.error('Error creando el plato:', error);
return res.status(500).send('Ocurrió un error al crear el plato');
}
});
Finalmente, hacemos que la aplicación escuche en el puerto 3000
:
app.listen(3000, () => console.log('escuchando...'));
Este sería el código completo de la aplicación:
import express from 'express'
import mongoose from 'mongoose'
const app = express()
app.use(express.json());
const Plato = mongoose.model('Plato', new mongoose.Schema({
nombre: String,
tipo: String,
}))
mongoose.connect('mongodb://edu:testpass@localhost:27017/restaurante?authSource=admin')
app.get('/platos', async (_req, res) => {
console.log('Mostrando lista de platos...')
const platos = await Plato.find();
return res.send(platos)
})
app.post('/platos', async (req, res) => {
try {
console.log('Creando plato...');
const { nombre, tipo } = req.body;
if (!nombre || !tipo) {
return res.status(400).send('Los campos nombre y tipo son obligatorios');
}
await Plato.create({ nombre, tipo }); // Utiliza las variables extraídas para crear el plato
return res.send('Plato creado con éxito');
} catch (error) {
console.error('Error creando el plato:', error);
return res.status(500).send('Ocurrió un error al crear el plato');
}
});
app.listen(3000, () => console.log('escuchando...'))
Conéctate a un contenedor
En lugar de instalar Docker en nuestro sistema local, vamos a usar la imagen de Mongo, así que buscaremos la imagen de Mongo en Docker Hub. Verás que existen instrucciones de configuración de Mongo. En concreto son necesarias estas dos variables de entorno:
- MONGO_INITDB_ROOT_ USERNAME: Usuario con el que conectarse a la base datos.
- MONGO_INITDB_ROOT_PASSWORD: La contraseña con la que conectarnos.
Has de saber que cada contenedor se configura de una forma diferente, por lo que las variables necesarias o pasos a seguir siempre serán diferentes.
Vamos a descargarnos la imagen de mongo. Para ello ejecuta este comando desde tu terminal de comandos:
docker pull mongo
Ahora vamos a crear el contenedor de mongo indicando tanto el puerto local como el de la imagen, así como el nombre del contenedor y las variables de entorno antes mencionadas, que se pasan mediante la opción -e
. Finalmente indicamos la imagen a partir de la cual crear el contenedor, que será mongo:
docker create -p27017:27017 --name imongo -e MONGO_INITDB_ROOT_USERNAME=edu -e MONGO_INITDB_ROOT_PASSWORD=testpass mongo
Obtendremos un identificador tal que así como respuesta:
7d6bfd1e3043fee8b09ab36cbd12900d7081ef6a1ace93804340658b9b9d4cb6
Ahora vamos a iniciar el contenedor mediante el comando docker start
:
docker start imongo
Si ejecutas el comando docker ps
, podrás ver que el contenedor imongo
está funcionando en el puerto 27017
.
Ejecutando tu aplicación
Ahora vamos a ejecutar la aplicación que hemos creado. Para ello usa este comando:
node index.js
Deberías poder ver el mensaje escuchando...
en pantalla.
Si accedes a la URL http://localhost:3000/platos
, deberías ver un array vacío []
por pantalla, ya que todavía no hay platos creados.
Para crear un plato, tendremos que enviar una petición POST, para lo cual podemos usar una aplicación como Postman. En postman, realiza una petición de tipo POST a la URL http://localhost:3000/platos
. Debes seleccionar raw como formato del body y seleccionar JSON. Luego pega este JSON en el body de la petición a modo de ejemplo:
{
"nombre": "Helado",
"tipo": "postre"
}
Al enviar la petición, deberías ver un mensaje indicando que el plato se ha creado con éxito. Aquí te dejo una captura de pantalla con la petición desde Postman:
Si ahora accedes de nuevo a la URL http://localhost:3000/platos
desde tu navegador, deberías ver que ahora ya existe un plato creado:
http://localhost:3000/platos
Deberías ver algo así:
[
{
"_id": "660ebf4397f6130265cc20af",
"nombre": "Helado",
"tipo": "postre",
"__v": 0
}
]
Tal y como ves, hemos usado el contenedor de MongoDB desde nuestra aplicación. Ahora, el siguiente paso será Dockerizar tu aplicación.
Crea un contenedor para tu aplicación
Vamos a aprender a crear un contenedor para nuestra aplicación. El primer paso será crear un archivo llamado Dockerfile
en la carpeta raíz de la aplicación. Este nombre de archivo es un estándar, así que no podrá tener otro nombre.
Lo primero que vamos a hacer en el archivo es indicar la imagen a partir de la cual queremos crear el contenedor, que en este caso será la versión 20 de node. Para ello usamos FROM
seguido del nombre de la imagen y su versión:
FROM node:20
Ahora vamos a crear una carpeta en la cual incluir el código de nuestra aplicación. Para ello ejecutaremos el comando mkdir
en el interior del contenedor. Para ejecutar comandos desde el archivo Dockerfile se usa la sentencia RUN
:
RUN mkdir -p /home/app
Luego copiaremos el contenido del directorio en el que estamos actualmente al directorio que hemos creado mediante el comando COPY
:
COPY . /home/app
El .
indica el directorio en el que se encuentra el archivo Dockerfile. Desde aquí es desde donde se copiará el código de la aplicación. Por otro lado, el directorio /home/app
, hace referencia al sistema de archivos del contenedor.
Luego vamos a exponer el puerto 3000 que usará nuestra aplicación mediante el comando EXPOSE
:
EXPOSE 3000
Finalmente, indicamos el comando que ejecutará nuestra aplicación, además de sus argumentos. Podemos hacerlo mediante el comando CMD:
CMD ["node", "/home/app/index.js"]
Es importante que uses la ruta completa /home/app/index.js
, ya que se supone que estamos en el directorio raíz del contenedor.
Como alternativa, vamos a usar el comando WORKDIR /home/app
para situarnos en el directorio de la aplicación antes de iniciarla.
Este será el código completo del archivo Dockerfile que hemos creado:
FROM node:20
RUN mkdir -p /home/app
COPY . /home/app
WORKDIR /home/app
EXPOSE 3000
CMD ["node", "index.js"]
Gestiona la red de tu aplicación
Ya hemos terminado con el archivo Dockerfile, pero ahora tenemos un nuevo problema. El servidor localhost
que hemos indicado en nuestra aplicación al conectarnos a Mongo, hacía referencia a nuestro sistema local, cuyo puerto estaba mapeado al contenedor.
El problema es que ahora localhost hace referencia al propio contenedor. Para ello debemos cambiar la ruta que usamos para conectarnos a Mongo tal que así:
mongoose.connect('mongodb://edu:testpass@imongo:27017/restaurante?authSource=admin')
Es decir, hemos reemplazado localhost
por el nombre del contenedor en donde se encuentra Mongo, que es el contenedor imongo
.
Ahora vamos recrear e nuevo el contenedor imongo
que hemos creado antes, de forma que esté en la misma red que nuestra aplicación. Para ello, primero para la ejecución del contenedor con el comando docker stop imongo
. Luego elimínalo con el comando docker rm imongo
.
Después crea el contenedor de nuevo, pero indicándole la red mediate la opción --network
:
docker create -p27017:27017 --name imongo --network mired -e MONGO_INITDB_ROOT_USERNAME=edu -e MONGO_INITDB_ROOT_PASSWORD=testpass mongo
Dockeriza tu aplicación
Hemos llegado al último paso, en el que crearemos la imagen de nuestra aplicación. Para ello usaremos el comando docker build
.
El comando build
creará la imagen a partir del archivo Dockerfile. El primer argumento será el nombre que queramos asignar a la imagen, junto a su etiqueta. El segundo argumento será la ruta donde se encuentre el proyecto junto con su archivo Dockerfile.
Vamos a crear la imagen platos
, con versión 1
desde el directorio donde nos encontramos, que es el de la aplicación:
docker build -t platos:1 .
Una vez finalice el proceso, si ejecutas el comando docker images
, podrás ver la imagen platos
creada en tu sistema.
Ahora vamos a crear el contenedor de la aplicación, indicándole que use la red mired
mediante la opción --network
:
docker create -p3000:3000 --name iplatos --network mired platos:1
Hemos mapeado el puerto 3000
del contenedor al puerto 3000
de nuestro sistema. También le hemos dado el nombre de iplatos
al contenedor, que hemos creado a patir de la versión 1
de la imagen platos
.
Ahora inicia el contenedor de Mongo:
docker start imongo
Seguidamente inicia el contenedor de la aplicación:
docker start iplatos
Si ahora accedes a la ruta http://localhost:3000/platos
deberías ver de nuevo un array vacío como respueta.
Ahora envía una la petición POST a la ruta http://localhost:3000/platos
de antes con este body mediante Postman:
{
"nombre": "Helado",
"tipo": "postre"
}
Deberías obtener que el plato se ha creado con éxito como respuesta.
Si ejecutas el comando docker logs iplatos
, deberías ver esto como resultado:
escuchando...
Mostrando lista de platos...
Creando plato...
Gestión con Docker Compose
Tal y como hemos visto, los pasos para dockerizar una aplicación y crear otros contenedores, asignando puertos, redes y creando variables de entorno, es un proceso muy tedioso. Sin embargo, existe una herramienta incluida con Docker que se llama Docker Compose que facilita todo este proceso.
Docker compose nos permite configurar los contenedores desde un archivo YAML con extensión .yml
.
La configuración de Docker Compose se realiza en el archivo docker-compose.yml
. Crea este archivo en el directorio raíz del proyecto.
La primera línea del archivo será la versión de Docker a usar, que en nuestro ejemplo será la 4.27
:
version: "4.27"
Luego vamos a agregar nuestros contenedores mediante la sentencia services:
services:
A continuación vamos a agregar los nombre de los contenedores a usar, que serán los contenedores iplatos
e imongo
. Los contenedores deben indicarse tras una indentación, siguiendo el estándar YAML:
version: "4.27"
services:
iplatos:
// configuración de iplatos
imongo:
// configuración de imongo
Vamos a ver primero el archivo completo con la configuración de los contenedores y, seguidamente, vamos a explicar la configuración:
version: "4.27"
services:
iplatos:
build: .
ports:
- "3000:3000"
links:
- imongo
imongo:
image: mongo
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=edu
- MONGO_INITDB_ROOT_PASSWORD=testpass
En el contenedor iplatos
, hemos indicado que este se creará a partir del archivo Dockerfile localizado en el directorio donde está el archivo docker-compose.yml
. Para ello hemos usado la sentencia build: .
.
Mediante la sentencia ports
podemos mapear todos los contenedores de la aplicación. En este caso mapeamos únicamente el puerto 3000
del contenedor al puerto 3000
de nuestro sistema. Los puertos se escriben con comillas dobles.
Finalmente, mediante la sentencia links
, definimos todos los contenedores de los que depende el contenedor iplatos
. Los contenedores se indican sin comillas.
En cuanto al contenedor imongo
, hemos usado la sentencia image
para indicar la imagen a partir de cual crearlo, que es la imagen mongo
.
Al igual que antes, hemos mapeamos el puerto 27017
del contenedor al puerto 27017
de nuestro sistema mediante la sentencia ports
.
Luego indicamos las variables de entorno usando la sentencia environment
. Hemos indicado las variables de entorno que necesita Mongo para su conexión.
Finalmente guardamos el archivo,
Si todavía tenías los contenedores iplatos
e imongo
en funcionamiento, para su ejecución mediante el comando docker stop
y elimínalos con el comando docker rm
.
Ahora ejecuta el comando docker compose up
para iniciar la configuración del archivo docker-compose.yml
y crear los contenedores:
docker compose up
Tras finalizar al proceso, la aplicación debería funcionar tal y como antes. Por defecto se mostrarán todos los logs.
Para frenar la ejecución de Docker Compose, pulsa CTRL+C
.
Si ejecutas el comando docker ps -a
, verás que se muestran los dos contenedores creados por Docker Compose.
Si quisieras eliminar estos contenedores rápidamente, podrás podrás usar el comando docker compose down
:
docker compose down
Esto eliminará tanto los contenedores como la red creada por Docker Compose.
Gestión de Volúmenes con Docker
Los datos creados en el contenedores, como por ejemplo los datos de Mongo, no persistirán una vez se eliminen los contenedores. Esto no suele ser deseable, ya que habitualmente querrás que los datos de la base de datos persistan.
Para lograr esto existen los volúmenes
. Los volúmenes permiten montar una carpeta del contenedor en el sistema operativo anfitrión, de forma que los datos persistan en tu sistema.
Existen tres tipos de volúmenes:
- Volúmenes anónimos: Docker decidirá el mejor lugar para almacenar estos datos en tu sistema, con el inconveniento de que luego no podrás referenciar esta carpeta para que por ejemplo pueda ser usada por otro contenedor.
- Volumen de anfitrión: En este caso, eres tú el que decides qué carpeta montar y dónde montarla.
- Volumen con nombre: En este caso el volumen es como el anónimo, con la diferencia de que podrás referenciar este volumen cuando vayas a usarlo con otro contenedor, pudiendo así usarlo desde varias imágenes.
Vamos a asignar un volumen a nuestra aplicación. Para ello usaremos la sentencia volumes en nuestro archivo docker-compose.yml
.
Tras la sentencia services
, con el mismo nivel de indentación, debes agregar la sentencia volumes
y luego una referencia. Le daremos el nombre de mongo-data
.
volumes:
mongo-data:
Aquí definiríamos todos los volúmenes que fuesen a usar nuestros contenedores.
Luego, en el interior del servicio imongo, definimos la sentencia volumes y referenciamos el volumen mongo-data
seguido de dos puntos :
y la ruta dentro del contenedor en donde va a ser montado el volumen mongo-data
:
volumes:
- mongo-data:/data/db
Hemos escogido este directorio porque Mongo guarda por defecto los datos en el directorio /data/db
. Si usases MySQL usarías el directorio /var/lib/mysql
y si usases PostgreSQL usarías el directorio /var/lib/postgresql/data
.
Este sería el archivo docker-compose.yml
final:
version: "4.27"
services:
iplatos:
build: .
ports:
- "3000:3000"
links:
- imongo
imongo:
image: mongo
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=edu
- MONGO_INITDB_ROOT_PASSWORD=testpass
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
Tras guardar el archivo, ejecuta de nuevo el comando docker compose up
.
Seguidamente crear un nuevo plato mediante una petición POST a la ruta http://localhost:3000/platos
de antes con este body mediante Postman:
{
"nombre": "Helado",
"tipo": "postre"
}
Ahora ejecuta el comando docker compose down
para frenar los contenedores.
Si ahora ejecutas de nuevo el comando docker compose up
y accedes al la ruta http://localhost:3000/platos
desde tu navegador, podrás ver que el plato que hemos creado antes se ha guardado.
Entornos de desarrollo con Docker
Estamos trabajando en local. Sin embargo, habitualmente tendrás múltiples entornos de desarrollo con una configuración diferente. Esto requerirá la creación de más archivos Dockerfile
y docker-compose.yml
, de forma que exista uno para cada entorno.
Vamos a dar por hecho que los archivos Dockerfile
y docker-compose.yml
se usarán en producción.
Crea empezar, crea el archivo Dockerfile.dev
y copia y pega los conteneidos de tu archivo Dockerfile
en su interior.
Cuando trabajas en local e inicias una aplicación con el comando node
, no se detectarán los cambios automáticamente en el archivos. Para ello se suele usar la herramienta nodemon. Si no la tienes instalada en tu sistema, puedes instalarla con este comando:
npm install -g nodemon
Sin embargo, lo ideal sería instalarla dentro del archivo Dockerfile
para que se instale desde el contenedor. Para ello tendrías que agregar la siguiente línea al archivo Dockerfile.dev
:
RUN npm i -g nodemon
Además, al ejecutar la aplicación, lo harías con el comando nodemon
y no con node
:
CMD ["nodemon", "index.js"]
Además, vamos a eliminar la línea COPY . /home/app
, ya que a continuación vamos a agregar un volumen que cree un enlace simbólico en la ruta /home/app
de forma que incluya el código de la aplicación.
Este sería el archivo Dockerfile.dev
final:
FROM node:20
RUN npm i -g nodemon
RUN mkdir -p /home/app
WORKDIR /home/app
EXPOSE 3000
CMD ["nodemon", "index.js"]
A continuación crea el archivo docker-compose-dev.yml
. Copia y pega en su interior el código del archivo docker-compose.yml
.
Vamos a mostrar primero el resultado final del archivo para luego explicar los cambios:
version: "4.27"
services:
iplatos:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
links:
- imongo
volumes:
- .:/home/app
imongo:
image: mongo
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=edu
- MONGO_INITDB_ROOT_PASSWORD=testpass
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
Comenzando por el contenedor iplatos
, hemos expandido la sentencia build
, que ahora constará de los atributos context
y dockerfile
.
Mediante al sentencia context: .
indicamos el contexto o la aplicación en donde estamos trabajando.
Hemos indicado también que se use el archivo Dockerfile.dev
mediante la sentencia dockerfile: Dockerfile.dev
, de forma que no se use el archivo Dockerfile
por defecto.
Además, hemos indicado un volumen anónimo en el contenedor iplatos
. Mediante el .
hemos indicado que la ruta actual es la que ha de ser montada en el volumen. Seguidamente hemos escrito dos puntos :
y luego la ruta de destino /home/app
dentro del contenedor.
El contenedor imongo
lo vamos a dejar tal y como está.
Para ejecutar Docker Compose con la nueva configuración de desarrollo, debemos ejecutar el comando docker compose
con el flag -f
seguido del archivo docker-compose
que se debe usar:
docker compose -f docker-compose-dev.yml up
Ahora, cada vez que realices cambios a tu archivo index.js
, los cambios se verán reflejados al instante gracias a nodemon
.
Esto ha sido todo. Espero que te haya sido útil.