Esta sección es sumamente importante para la comprensión y creación de imágenes personalizadas.
Aquí veremos:
- Dockerfile
- .dockerignore
- Principales comandos del Dockerfile:
- FROM
- RUN
- COPY
- WORKDIR
- Entre otros
- Asignar tags
- Build
- Buildx (Para múltiples arquitecturas)
Es una sección indispensable para poder crear de forma eficiente nuestras futuras imágenes que desplegaremos en servidores con arquitecturas diferentes a la nuestra computadora.
Dockerizar una aplicación es el proceso de tomar un código fuente y generar una imagen lista para montar y correrla en un contenedor, para ello se hace uso de un archivo llamado Dockerfile
. Cada paso dentro del dicho archivo genera un layer en la imagen. Vamos a crear un directorio llamado cron-ticker
y abrimos una instancia de la terminal en ese directorio, posteriormente inicializamos un proyecto de NodeJS con el siguiente comando:
$: npm init -y
Lo siguiente es crear un archivo app.js
con una impresión en consola y luego definir un script en el package.json
para iniciar la aplicación:
{
...,
"scripts": {
...,
"start": "node src/app.js"
},
...
}
Lo siguiente será instalar una dependencia llamada Node Cron, y lo hacemos con el siguiente comando:
$: pnpm install node-cron
Seguido a esto copiamos el siguiente código dentro de app.js
, lo cual va a ejecutar una impresión en consola cada segundo:
const cron = require('node-cron')
cron.schedule('1-59 * * * * *', () => {
console.count('running a task every second')
})
Si por ejemplo queremos que se ejecute cada 5 segundos debemos divider la expresión 1-59
entre 5:
const cron = require('node-cron')
cron.schedule('1-59/5 * * * * *', () => {
console.count('running a task')
})
Vamos a crear el archivo Dockerfile
dentro del directorio del proyecto, con el fin de definir las instrucciones para crear una imagen de nuestro proyecto. Toda nuestra aplicación necesita empezar por un punto, por lo tanto vamos a usar la versión 19.2-alpine3.16 de NodeJS. El archivo se construirá de la siguiente manera:
-
Dentro del archivo Dockerfile añadimos la instrucción
FROM
para definir la imagen:FROM node:19.2-alpine3.16
-
Definimos luego el directorio de trabajo en la carpeta
/app
:WORKDIR /app
-
Le damos la instrucción de cuales archivos se deben copiar, y el directorio en que debe guardarlos (cómo en el paso anterior definimos el directorio de trabajo, le vamos a indicar que se quede en el mismo):
COPY src/app.js package.json ./
Las anteriores instrucciones son las más usadas dentro de las construcción de imágenes de proyectos. En la próxima lección vamos a ver los pasos siguientes y la manera de construir la imagen.
Ya definimos la imagen, el directorio sobre el que vamos a trabajar, y los archivo iniciales de la imagen. Solo nos restan 2 pasos básicos:
-
Ejecutar un comando para descargar las dependencias:
RUN npm install
-
Definir el comando para levantar el proyecto:
CMD [ "node", "src/app.js" ]
Para construir la imagen vamos a usar el siguiente comando (el .
hace referencia al directorio actual, por lo tanto este comando se debe ejecutar en el lugar donde se tiene el dockerfile, de lo contrario se debe enviar la ruta absoluta):
$: docker build --tag cron-ticker .
Cuando se construye por primera vez la imagen puede ser un poco demorado, cuando se genera una nueva versión, solo validara las instrucciones qu hayan cambiado con la intención de generar nuevos layers, por ello es recomendable que siempre se definan en la parte superior los comandos que no se modifican a menudo ya que una vez detectado un cambio, los demás layers van a cambiar si o si.
Una vez construida la imagen, podremos ejecutar un contenedor para usarla:
$: docker container run cron-ticker
Podemos consultar el peso de la imagen cuando la listamos:
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker latest 9819877b548f 2 minutes ago 173MB
En ocasiones requerimos reconstruir una imagen, ya sea por errores o por actualizaciones. Lo único que debemos hacer es volver a usar el comando de construcción. Por ejemplo vamos a actualizar nuestro archivo Dockerfile al siguiente orden:
FROM node:19.2-alpine3.16
WORKDIR /app
COPY package.json ./
RUN npm install
COPY src/app.js ./
CMD [ "node", "app.js" ]
Los pasos que no presentan cambios quedan en cache, por lo tanto solo los pasos que se modifican y sus siguientes crearán nuevas capas en la imagen.
Podemos definir la versión de la imagen que se va a crear y que no quede sin identificarse. Por ejemplo, si usamos el comando a continuación, vamos a crear la versión 1.0 de la imagen y en ese momento a su vez será la versión latest:
$: docker build -t cron-ticker:1.0.0 .
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker 1.0.0 fa423894d3df 9 seconds ago 173MB
Si añadimos un cambio y deseamos volver a actualizar la imagen, ejecutamos el siguiente comando y veremos que convertimos esta nueva versión en la latest:
$: docker build -t cron-ticker:1.0.1 .
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker 1.0.1 fa423894d3df 3 minutes ago 173MB
cron-ticker 1.0.0 9819877b548f 28 minutes ago 173MB
Si queremos actualizar el tag de una imagen usamos el siguiente comando:
$: docker image tag cron-ticker:1.0.1 cron-ticker:boom
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker 1.0.1 fa423894d3df 5 minutes ago 173MB
cron-ticker boom fa423894d3df 5 minutes ago 173MB
cron-ticker 1.0.0 9819877b548f 31 minutes ago 173MB
Otro ejemplo es en el caso que construyamos una imagen sin tag, pero que no tenga diferencias con la anterior. Se va a crear una versión con el tag latest
:
$: docker build cron-ticker:1.0.2 .
$: docker build cron-ticker .
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker 1.0.2 5553c2b0914a 31 seconds ago 173MB
cron-ticker latest 5553c2b0914a 31 seconds ago 173MB
cron-ticker 1.0.1 fa423894d3df 6 minutes ago 173MB
cron-ticker boom fa423894d3df 6 minutes ago 173MB
cron-ticker 1.0.0 9819877b548f 32 minutes ago 173MB
Para ejecutar una imagen en una versión especifica, construimos el contenedor definiendo la imagen como cron-ticker:<version>
, sino se define se hará uso de la latest.
En esta lección vamos a subir la imagen que creamos en Docker Hub. Para ello necesitamos tener una cuenta en dicha plataforma, y luego podemos pasar a una instancia de terminal para ejecutar el siguiente comando:
$: docker login
Regresamos a la plataforma de Docker Hub y creamos un nuevo repositorio, el cual nos dará un comando para subir la imagen con la siguiente estructura:
$: docker push <username>/<repositoryName>:<tagName>
Debemos crear un nuevo tag de nuestra imagen que tenga el mismo nombre del repositorio:
$: docker tag <image>:<tagName> <repositoryName>:<tagName>
Ahora subimos nuestra imagen:
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker blast 5553c2b0914a 25 hours ago 173MB
cron-ticker latest 5553c2b0914a 25 hours ago 173MB
$: docker tag cron-ticker:blast <username>/cron-ticker:blast
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<username>/cron-ticker blast 5553c2b0914a 25 hours ago 173MB
cron-ticker blast 5553c2b0914a 25 hours ago 173MB
cron-ticker latest 5553c2b0914a 25 hours ago 173MB
$: docker push <username>/cron-ticker:blast
Un apunte adicional a la lección anterior, es que siempre es recomendable por buena práctica subir de últimas la versión latest
de la imagen, similar a cómo lo hacen las imágenes oficiales en Docker Hub:
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
cron-ticker blast 5553c2b0914a 25 hours ago 173MB
cron-ticker latest 5553c2b0914a 25 hours ago 173MB
$: docker tag cron-ticker <username>/cron-ticker
$: docker tag cron-ticker:blast <username>/cron-ticker:blast
$: docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<username>/cron-ticker blast 5553c2b0914a 25 hours ago 173MB
<username>/cron-ticker latest 5553c2b0914a 25 hours ago 173MB
cron-ticker blast 5553c2b0914a 25 hours ago 173MB
cron-ticker latest 5553c2b0914a 25 hours ago 173MB
$: docker push <username>/cron-ticker:blast
$: docker push <username>/cron-ticker:latest
Ahora si vamos al punto central de esta imagen. Vamos a limpiar todas las imágenes que no están siendo usadas en nuestro equipo:
$: docker image prune -a
Haremos la prueba con una de las versiones subidas al repositorio de DockerHub. La primera acción que debe ejecutar el comando, es la descarga de la imagen ya que no la encuentra en local:
$: docker container run <username>/cron-ticker:blast
Las pruebas son una de las partes más importantes del desarrollo de un proyecto. Para la aplicación que estamos usando vamos a instalar jest
con el siguiente comando:
$: pnpm i jest
Lo primero será organizar un poco el código creando la función que se ejecutará dentro del cron.schedule
, y dicha función será guardada en el archivo tasks/sync-db.js
:
let ticks = 0
const syncDB = () => {
console.count('running a task')
ticks++
return ticks
}
module.exports = { syncDB }
const cron = require('node-cron')
const { syncDB } = require('../tasks/sync-db')
cron.schedule('1-59/3 * * * * *', syncDB)
Lo siguiente es crear un archivo para realizar el o los test de la función. En este caso queremos comprobar que el método retorne el número 2:
const { syncDB } = require("../tasks/sync-db")
describe('Pruebas en syncDB', () => {
test('Debe ejecutar el proceso 2 veces', () => {
const times = syncDB()
expect(times).toBe(2)
})
})
Para ejecutar las pruebas debemos añadir el script dentro del package.json
:
{
...,
"scripts": {
"test": "jest",
...
},
...
}
Si ejecutamos los test, vamos a observar que falla y esto es intencional, ya que vamos a implementar la funcionalidad de que no se la imagen a menos que los test pases exitosamente.
Vamos a implementar el comando de pruebas dentro del archivo de Dockerfile, una vez que se haya copiado el archivo de app.js
...
COPY app.js ./
RUN npm run test
CMD [ "node", "app.js" ]
Cuando tratamos de crear la imagen, se va a imprimir un error ya que bo reconoce los archivos de testing. Para solucionar este podemos escribir la siguiente instrucción (el inconveniente es que mapeará todo el directorio del proyecto incluyendo los módulos de node):
...
COPY . .
RUN npm run test
Si intentamos crear la imagen de nuevo, no podemos avanzar, por que los test no logran pasar, pero cuando corregimos el test por el de a continuación, la imagen avanza en su proceso:
const { syncDB } = require("../../tasks/sync-db")
describe('Pruebas en syncDB', () => {
test('Debe ejecutar el proceso 2 veces', () => {
syncDB()
const times = syncDB()
expect(times).toBe(2)
})
})
El inconveniente ahora es que la imagen pesa demasiado.
Lo primero será crear un contenedor con la última imagen que construimos:
$: docker container run -d <username>/cron-ticker:blast
Luego abrimos la ShellCommand del contenedor:
$: docker exec -it b55 /bin/sh
Es importante recordar que de ser necesario se debe ejecutar
export MSYS_NO_PATHCONV=1
cada que crea una instancia de GitBash.
Cuando listamos los ficheros dentro del contenedor, nos encontraremos con lo siguiente:
/app # ls
Dockerfile node_modules package.json tasks
app.js package-lock.json pnpm-lock.yaml tests
Si listamos todos los paquetes de node, nos daremos cuenta que tenemos un gran problema por que hay muchos módulos que no necesitamos, además de que los archivos de testing no son necesarios dentro de la imagen, solo lo usamos para condicionar la construcción de la misma.
Es un archivo similar al .gitignore, en el cual se especifica todo lo que hay que ignorar en el proceso de construcción de la imagen. Por ejemplo, para ignorar algunos archivos escribimos lo siguiente
node_modules/
Dockerfile
package-lock.json
Si volvemos a construir la imagen, podremos observar que la imagen es un poco más pequeña. Cuando creamos un contenedor con dicha imagen y abrimos su ShellCommand, podremos listar los documentos que hay dentro del proyecto y notaremos que tanto el directorio node_modules
y package-lock.json
siguen en la imagen, y no hay que alarmarnos, esto se debe a la instrucción npm install
que se redacto en el Dockerfile.
Una solución empírica para reducir el tamaño de la imagen, es ingresar a la terminal de un contenedor y eliminar los archivos y carpetas que no queremos. También podemos ir al Dockerfile y añadir una instrucción más:
...
RUN npm run test
RUN rm -rf tests && rm -rf node_modules
RUN npm install --prod
CMD [ "npm", "start" ]
Aunque ya no tiene la misma cantidad de archivos o directorios, la imagen pesa más que lo anterior, y esto se debe a que hay más layers en el Dockerfile
Cuando necesitamos especificar una plataforma de manera forzada, cambiamos la instrucción de FROM, por ejemplo:
FROM --platform:arm64 node:19.2-alpine3.16
...
Con el cambio de plataforma, toda la imagen se vuelve a construir, y si la publicamos podremos observar que se actualiza la arquitectura del sistema operativo.
Para saber cuales builders tenemos, ejecutamos el siguiente comando:
$: docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default * docker
default default running 20.10.21 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux docker
desktop-linux desktop-linux running 20.10.21 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
Para crear un nuevo builder ejecutamos el siguiente comando:
$: docker buildx create --name mybuilder --driver docker-container --bootstrap
Una vez descargado el driver podemos usarlo con el siguiente comando:
$: docker buildx use mybuilder
Volvemos a listar los builders:
$: docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder * docker-container
mybuilder0 npipe:////./pipe/docker_engine running v0.11.0 linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default docker
default default running 20.10.21 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux docker
desktop-linux desktop-linux running 20.10.21 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
Podemos inspeccionar el builder actual con el siguiente comando:
$: docker buildx inspect
Name: mybuilder
Driver: docker-container
Nodes:
Name: mybuilder0
Endpoint: npipe:////./pipe/docker_engine
Status: running
Buildkit: v0.11.0
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
Ahora dentro de nuestro Dockerfile vamos a actualizar la sentencia FROM con lo siguiente:
FROM --platform=$BUILDPLATFORM node:19.2-alpine3.16
Y ahora podemos construir la imagen en diversas arquitecturas y subirla al repositorio de DockerHub:
$: docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t <username>/cron-ticker:ninja --push .