Introducción. ¿Qué se puede hacer con Docker?
Con Docker, podés:
- Crear y ejecutar una imagen como un contenedor.
- Compartir imágenes usando Docker Hub.
- Implementar aplicaciones Docker utilizando varios contenedores con una base de datos.
- Ejecutar aplicaciones con Docker Compose.
Docker está construido sobre un LXC, un Linux Container, que es una tecnología de virtualización en el
nivel del sistema operativo; trabaja en una tecnología chroot.
Docker Engine: gestión de containers en ejecución.
Docker Client: cualquier herramienta que usa Docker Engine. Es la CLI de Docker.
Docker Registry: contiene a las imágenes, descarga de imágenes.
¿Qué es un container de Docker?
- es una instancia para correr una imagen,
- con DockerAPI or CLI se puede manejar,
- se puede ejecutar en máquinas locales, máquinas virtuales o implementarse en la nube,
- se puede correr en cualquier sistema operativo,
- está aislado de otros contenedores y ejecuta su propio software, archivos binarios y
configuraciones.
¿Qué es una imagen de Docker?
La imagen es la que contiene a todos los archivos (aislados) para poder correr el container. Una imagen
de Docker es un modelo que se usa para crear containers de Docker. Tiene todos los componentes
necesarios para ejecutar una aplicación o servicio, incluyendo el código, las bibliotecas, las dependencias
y las configuraciones.
Posee nombre (repositorio donde se almacena la imagen)
+ etiqueta (versión específica de la imagen).
Las imágenes pueden estar basadas en otras imágenes.
¿Qué es el Docker Daemon?
Docker Daemon es el componente principal del sistema Docker. Es un servicio que se ejecuta en segundo
plano en el sistema operativo y maneja todas las tareas de gestión de contenedores Docker.
El Docker Daemon es responsable de la creación, inicio, parada y eliminación de contenedores Docker.
También se encarga de la gestión de imágenes Docker, el almacenamiento de capas de imágenes, la
redirección de puertos y la comunicación con el Docker Hub o un registro privado de Docker.
El Docker Daemon es responsable de mantener el aislamiento y la seguridad de los contenedores,
asegurando que los contenedores no afecten negativamente a otros procesos en el sistema operativo
host.
¿Qué es el Docker Client?
Docker Client: herramienta de línea de comandos que se utiliza para interactuar con el Docker Daemon y
gestionar contenedores, imágenes y otros recursos de Docker en un sistema.
[Link]
¿Cómo correr una aplicación dentro de un container?
Primer paso: instalar Docker
[Link]
Clonarse el repositorio getting-started:
git clone [Link]
Buildear la imagen del contenedor de la aplicación:
Para buildear la imagen del contenedor, necesitamos crear un Dockerfile:
cd /path/to/app
touch Dockerfile
Pegar dentro de Dockerfile el siguiente texto:
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/[Link]"]
EXPOSE 3000
FROM solamente puede ser precedido por variables declaradas por ARG. Las variables declaradas con
ARG pueden utilizarse en cualquier instrucción para definir argumentos que se pueden pasar al momento
de construir la imagen de Docker, ya sea antes o después de la instrucción FROM. El alcance de las
variables ARG es global en el archivo de Dockerfile.
ARG CODE_VERSION=latest
FROM base:${CODE_VERSION}
FROM Inicializa un nuevo build y setea la imagen base para instrucciones subsecuentes. Un Dockerfile
válido debe comenzar con FROM.
FROM puede aparecer varias veces para crear muchas imágenes o usar un build como dependencia para
otro build: hacés una nota del ID de la última imagen que se encuentra en el output del commit, antes de
cada nuevo FROM. Cada FROM limpia cualquier estado creado por instrucciones previas.
WORKDIR setea el directorio de trabajo para las instrucciones subsecuentes.
Buildear la imagen usando el comando:
Unset
docker build -t getting-started .
El comando docker build utiliza el Dockerfile para construir una nueva imagen y descarga la imagen
node:18-alpine.
Después de que Docker descargó la imagen, las instrucciones del Dockerfile copian la aplicación y utilizan
yarn para instalar las dependencias de la misma. La directiva CMD especifica el comando predeterminado
que se ejecutará al iniciar un contenedor a partir de esta imagen.
Finalmente, la flag -t etiqueta a la imagen. Refiero a la imagen getting-started cuando ejecuto un
contenedor.
El punto (.) al final del comando docker build le indica a Docker que debe buscar el Dockerfile en el
directorio actual.
Correr un contenedor de la App
Unset
docker run -dp [Link]:3000:3000 getting-started
-d flag (--detach) corre el container en background. -p flag (--publish) crea un puerto mapeando entre el
host y el container; toma un valor en formato HOST:CONTAINER (HOST es la dirección del host y
CONTAINER es la dirección del puerto). Sin el mapeo, no podríamos acceder a la aplicación desde el host.
Después de unos segundos, abrir [Link] para visualizar la app.
Correr docker ps para listar los containers.
Actualizando la aplicación
Si actualizo la aplicación y deseo generar una nueva imagen, primero debo eliminar el contenedor
anterior.
Unset
docker rm <the-container-id>
Buildear la nueva imagen:
Unset
docker build -t getting-started .
E iniciar un nuevo contenedor:
Unset
docker run -dp [Link]:3000:3000 getting-started
Compartiendo la aplicación
Para compartir imágenes de Docker, debemos usar un registry de Docker. Docker Hub es el registro de
contenedores por default; permite compartir, almacenar y distribuir contenedores Docker.
Crear Docker ID en:
[Link]
Dentro de Docker Hub, crear un repositorio público getting-started
Listar imágenes de Docker:
Unset
docker image ls
Loguear a tu usuario:
Unset
docker login -u indilozano19
Etiquetar a la imagen que tenemos, llamada ‘getting-started’, que podemos ver cuando ejecutamos el
comando ‘docker ps’, como ‘user/getting-started’.
Unset
docker tag getting-started indilozano19/getting-started
Y pushear un nuevo tag a este repositorio usando: docker push new-repo:tagname
Unset
docker push indilozano19/getting-started-app-docker:latest
Podés ingresar a [Link] con tu cuenta de Docker Hub -> Start -> ADD NEW
INSTANCE. Esto abre una terminal en donde podés correr el comando
docker run -dp [Link]:3000:3000 YOUR-USER-NAME/getting-started
para descargar la imagen e iniciar la aplicación.
Persistir los datos
Cada container usa varias capas de una imagen para su sistema de archivos y tiene su propio espacio
para crear, actualizar, o eliminar archivos. Los cambios no se ven entre un container y otro, aunque estén
usando la misma imagen. Los cambios se pierden cuando se elimina un container. Con volumes
(volúmenes), podemos cambiar esto.
¿Qué hacen los volúmenes (volumes)?
Los volúmenes ayudan a conectar rutas específicas del sistema de archivos del container a la máquina
host. Si montamos un directorio en el contenedor, los cambios en ese directorio también se ven en la
máquina host. Si montamos ese mismo directorio al reiniciar el contenedor, veremos los mismos archivos.
Hay dos tipos principales de volúmenes. Eventualmente usaremos ambos, pero comenzamos con
montajes de volumen.
De forma predeterminada, la aplicación de tareas pendientes almacena sus datos en una base de datos
SQLite en /etc/todos/[Link] en el sistema de archivos del contenedor. SQLite es una base de
datos relacional que almacena todos los datos en un solo archivo. Si bien esto no es lo mejor para
aplicaciones a gran escala, funciona para demostraciones pequeñas. Más adelante veremos cómo
cambiar esto a un motor de base de datos diferente.
Dado que la base de datos es un solo archivo, si puede conservar ese archivo en el host y ponerlo a
disposición del siguiente contenedor, debería poder continuar donde lo dejó el último. Al crear un volumen
y adjuntarlo (a menudo llamado "montarlo") al directorio donde almacenó los datos, puede conservar los
datos. A medida que su contenedor escriba en el archivo [Link], los datos persistirán en el host del
volumen.
Como se mencionó, vas a utilizar un soporte de volumen. Montar un volumen es tener un depósito opaco
de datos. Docker administra completamente el volumen, incluida la ubicación de almacenamiento en el
disco. Sólo necesitás recordar el nombre del volumen.
Volúmenes montados (mounted). ¿Cómo crear un volumen y correr el
container?
Crear un volumen usando:
Unset
docker volume create todo-db
Parar y eliminar el todo app container una vez más con
Unset
docker rm -f <id>
que todavía está corriendo sin el volumen persistido.
Startear el todo app container, pero con –mount para montar el volumen. Poner el nombre del volumen y
montarlo en /etc/todos en el contenedor, lo cual captura todos los archivos creados en esa ruta.
Unset
docker run -dp [Link]:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos
getting-started
¿Cómo verificar que la data se persistió?
Cuando se inicia el contenedor, abrir la app y agregar algunos ítems.
Parar y eliminar el container de la todo app:
Unset
docker rm -f <id>
Start un container nuevo usando los pasos previos:
Unset
docker run -dp [Link]:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos
getting-started
Abrir la app. Deberías poder ver la lista de items.
Volver a eliminar el container al finalizar.
HINT: para saber dónde está almacenada la data del volumen.
Unset
docker volume inspect todo-db
Mountpoint es donde está localizada la data en el disco. Para acceder a esa ubicación, es probable que
necesitemos acceso root.
Volúmenes vinculados (bind, de enlace)
Un volumen vinculado es otro tipo de montaje, que permite compartir un directorio desde el sistema de
archivos del host en el container. Podemos usar un montaje bind para montar el código fuente en el
contenedor. El contenedor ve los cambios en el código inmediatamente, apenas guardamos un archivo.
Esto significa que podemos ejecutar procesos en el contenedor que detectan cambios en el sistema de
archivos y responden a ellos.
nodemon: herramienta para observar los cambios en los archivos y luego reiniciar la aplicación
automáticamente.
Volumen montado (mounted) Volumen vinculado (binded)
Ubicación del anfitrión Docker elige Yo decido
Ejemplo de montaje (usando type=volume,src=my-volume,tar type=bind,src=/path/to/data,targ
--mount) get=/usr/local/data et=/usr/local/data
Llena un nuevo volumen con Sí No
el contenido del contenedor.
Admite controladores de Sí No
volumen
Abrir una terminal, ir al directorio de getting-started-app y correr el comando para iniciar bash en un
container de Ubuntu con un volumen vinculado.
Unset
docker run -it --mount type=bind,src="$(pwd)",target=/src ubuntu bash
--mount le dice a Docker que cree un volumen vinculado, src es el directorio en donde estamos en
nuestra máquina host, y target es donde debería aparecer ese directorio dentro del container (/src).
Después de correr el comando, Docker inicia una sesión bash interactiva en el directorio raíz del sistema
de archivos del container.
root@ac1237fad8db:/# pwd
Ir al /src:
Unset
cd src
Crear un archivo [Link]:
Unset
touch [Link]
Abrir el directorio de getting-started-app y observar que se creó [Link].
Eliminar desde la máquina al archivo [Link].
Al listar nuevamente el contenido de la app desde el container, veremos que el archivo ya no está:
Frenar la sesión interactiva del container con Ctrl + D.
Contenedores de desarrollo
El uso de volúmenes bind es común para las configuraciones de desarrollo local. La ventaja es que
nuestra máquina no necesita tener instalados todos los entornos y herramientas de compilación. Con un
solo comando de ejecución de Docker, Docker extrae dependencias y herramientas.
Correr la aplicación en un contenedor de desarrollo
con un volumen vinculado (bind mount) que:
- Monta el código fuente dentro del container.
- Instala todas las dependencias.
- Inicia nodemon para ver los cambios en el sistema de archivos.
Podemos usar el CLI o el Docker Desktop para correr el container con un volumen vinculado.
CLI (Linux)
1- Asegurarse de no tener ningún getting-started container corriendo.
2- Correr el comando desde el directorio getting-started-app.
Unset
docker run -dp [Link]:3000:3000 \
-w /app --mount type=bind,src="$(pwd)",target=/app \
node:18-alpine \
sh -c "npm install && npm run dev"
-w /app setea el directorio de trabajo o el directorio actual desde donde se corre el comando.
--mount type=bind,src="$(pwd)",target=/app para vincular volumen desde el directorio actual
del host al directorio /app en el contenedor.
node:18-alpine es la imagen base a usar.
sh -c "npm install && npm run dev" - the command.
Estás iniciando un shell usando sh (alpine no tiene bash) y ejecutando yarn install para instalar
paquetes, luego ejecutas npm run dev para iniciar el servidor de desarrollo. Si miras en el
[Link], verás que el script dev inicia nodemon.
docker logs -f <container-id> para ver los logs del container.
Desarrollar la aplicación con el contenedor de desarrollo
Actualiza tu aplicación en tu máquina host y verás los cambios reflejados en el contenedor.
En el archivo src/static/js/[Link], en la línea 109, cambiá "Add Item" para que diga solamente
"Add":
- {submitting ? 'Adding...' : 'Add Item'}
+ {submitting ? 'Adding...' : 'Add'}
Ctrl + S para guardar el archivo.
Actualizá la página en tu navegador web y deberías ver el cambio reflejado casi de inmediato debido al
montaje del enlace. Nodemon detecta el cambio y reinicia el servidor. Es posible que el servidor de Node
tarde unos segundos en reiniciarse. Si recibís un error, actualizá después de unos segundos.
Hacés un cambio -> guardás el archivo -> se refleja el cambio debido al bind mounting.
Cuando Nodemon detecta un cambio, reinicia la aplicación dentro del contenedor automáticamente.
Cuando hayas terminado, detené el contenedor y creá su nueva imagen usando:
Unset
docker build -t getting-started .
Ctrl + C para detener el container.
Docker también admite otros tipos de montaje y controladores de almacenamiento para manejar casos de
uso más complejos y especializados.
Próximos pasos
Para preparar la aplicación para producción, hay que migrar la base de datos de SQLite a algo que pueda
escalar un poco mejor. Seguimos usando una base de datos relacional y cambiamos la aplicación para
😀
usar MySQL. Pero, ¿cómo deberías ejecutar MySQL? ¿Cómo permites que los contenedores se
comuniquen entre sí? Continuará…
Aplicaciones con múltiples contenedores
En general, cada contenedor debe hacer una cosa y hacerla bien. Aquí tienes algunas razones para
ejecutar el contenedor por separado:
- Existe la posibilidad de que necesitemos escalar las API y las interfaces de usuario de manera
diferente que las bases de datos.
- Los contenedores separados nos permiten versionar y actualizar las versiones de manera aislada.
- Aunque podamos usar un contenedor para la base de datos localmente, es posible que queramos
usar un servicio administrado para la base de datos en producción. No queremos enviar el motor de
nuestra base de datos junto con nuestra aplicación.
- Ejecutar varios procesos requerirá un administrador de procesos (el contenedor solo inicia un
proceso), lo que agrega complejidad al inicio/apagado del contenedor.
Y hay más razones. Así que, como se muestra en el siguiente diagrama, lo mejor es ejecutar nuestra
aplicación en múltiples contenedores.
Conectividad entre contenedores
Por defecto, los contenedores se ejecutan de forma aislada y no saben nada acerca de otros procesos o
contenedores en la misma máquina. Si colocamos ambos contenedores en la misma red, podrán
comunicarse entre sí.
Iniciar MySQL
Existen dos formas de poner un contenedor en una red:
- Asignar la red al iniciar el contenedor.
- Conectar un contenedor que ya está en ejecución a una red.
Creamos la red primero y luego adjuntamos el contenedor de MySQL al iniciar.
Crear la red.
Unset
docker network create todo-app
Iniciar un contenedor MySQL y conectarlo a la red.
Definimos algunas variables de entorno que la base de datos usará para inicializarse. Para obtener más
información sobre las variables de entorno de MySQL, está la sección "Variables de entorno" en la lista de
MySQL Docker Hub.
Unset
docker run -d \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:8.0
El volume todo-mysql-data es montado en /var/lib/mysql, que es donde MySQL guarda su
información.
Sin embargo, nunca ejecutamos un comando de creación de volume de Docker. Docker reconoce que
desea usar un volumen con nombre y crea uno automáticamente.
Conectarse a la base de datos y verificar que está corriendo:
Unset
docker exec -it <mysql-container-id> mysql -u root -p
Poner la contraseña secret.
En el shell de MySQL, listar las bases de datos y verificar que se vea la base todos.
Unset
SHOW DATABASES;
Salir del MySQL shell y volver al shell de nuestra máquina con exit.
Conectarse a MySQL
MySQL está funcionando, puede usarse. Si se ejecuta otro contenedor en la misma red, cada contenedor
tiene su propia dirección IP.
Para comprender mejor las redes de contenedores, usaremos el contenedor nicolaka/netshoot, que viene
con muchas herramientas que son útiles para solucionar o depurar problemas de redes.
Iniciamos un nuevo contenedor usando la imagen nicolaka/netshoot y nos aseguramos de conectarlo a la
misma red.
Unset
docker run -it --network todo-app nicolaka/netshoot
Dentro del contenedor, usamos el comando dig, que es una herramienta DNS útil. Buscamos la dirección
IP del nombre de host mysql.
Unset
dig mysql
En la "ANSWER SECTION", hay un registro A para mysql que se resuelve en [Link] (lo más
probable es que su dirección IP tenga un valor diferente). mysql normalmente no es un nombre de host
válido, pero Docker pudo resolverlo en la dirección IP del contenedor que tenía ese alias de red.
Recordemos que usamos --network-alias anteriormente.
Entonces, la aplicación simplemente necesita conectarse a un host llamado mysql y se comunicará con
la base de datos.
Ejecutar la aplicación con MySQL
La aplicación todo admite la configuración de algunas variables de entorno para especificar la
configuración de conexión MySQL:
MYSQL_HOST - el nombre de host del servidor MySQL en ejecución.
MYSQL_USER - el nombre de usuario que se usará para la conexión.
MYSQL_PASSWORD - la contraseña que se usará para la conexión.
MYSQL_DB - la base de datos que se usará una vez conectado.
Aunque es común usar variables de entorno para configuración en desarrollo, no se recomienda en
producción.
Ahora podemos iniciar nuestro contenedor listo para desarrollo.
Especificamos cada una de las variables de entorno anteriores, y conectamos el contenedor a la red de la
aplicación. Hay que estar en el directorio de la aplicación de introducción cuando ejecutamos este
comando.
Unset
docker run -dp [Link]:3000:3000 \
-w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:18-alpine \
sh -c "yarn install && yarn run dev"
Si observamos los registros del contenedor (docker logs -f <container-id>), debería ver un
mensaje similar al siguiente, que indica que está usando la base de datos mysql.
Unset
nodemon src/[Link]
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] starting `node src/[Link]`
Connected to mysql db at host mysql
Listening on port 3000
Abrimos la aplicación en el navegador y agregamos algunos ítems a la lista.
Nos conectamos a la base de datos MySQL:
Unset
docker exec -it <mysql-container-id> mysql -p todos
Y en el shell de mysql, ejecutamos lo siguiente, para ver que los ítems se han escrito en la base:
Unset
select * from todo_items;
Hasta acá, tenemos una aplicación que ahora almacena sus datos en una base de datos externa que se
ejecuta en un contenedor separado.
Usar Docker Compose
Docker Compose es una herramienta que ayuda a definir y compartir aplicaciones de varios
contenedores. Con Compose, podemos crear un archivo YAML para definir los servicios y, con un solo
comando, poner en marcha todo o desmontarlo por completo.
La gran ventaja de usar Compose es que se puede definir la configuración de la aplicación en un archivo,
mantenerlo en la raíz del repositorio del proyecto (ahora versionado) y permitir fácilmente que otra
persona contribuya al mismo. Alguien solo tendría que clonar el repositorio y poner en marcha la
aplicación usando Compose. De hecho, es posible que veamos muchos proyectos en GitHub/GitLab
haciendo exactamente esto en la actualidad.
Crear el archivo de Compose
En el directorio de la getting-started-app, crear un archivo [Link].
Definir el servicio de la aplicación
Anteriormente, usamos este comando para iniciar el servicio de la aplicación:
Unset
docker run -dp [Link]:3000:3000 \
-w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:18-alpine \
sh -c "yarn install && yarn run dev"
Ahora, definiremos este servicio en el [Link].
1. Abrimos el archivo y comenzamos definiendo el nombre y la imagen del primer servicio (o
contenedor) que deseamos ejecutar como parte de la aplicación. El nombre se convertirá
automáticamente en un alias de red, lo que será útil al definir el servicio MySQL.
Unset
services:
app:
image: node:18-alpine
2. Normalmente, hay un command cerca de la definición de la image, aunque no es necesario realizar el
pedido. Agregamos el command al archivo [Link].
Unset
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
3. Migramos la parte -p [Link]:3000:3000 definiendo los ports para el servicio.
Unset
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- [Link]:3000:3000
4. Migramos el directorio de trabajo (-w /app) y el mapeo de volumen (-v "$(pwd):/app") usando
working_dir y definición de volumes.
Una ventaja de las definiciones de volumes de Docker Compose es que podemos usar rutas relativas
desde el directorio actual.
Unset
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- [Link]:3000:3000
working_dir: /app
volumes:
- ./:/app
5. Finalmente, migramos las definiciones de variables de entorno usando la clave environment.
Unset
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- [Link]:3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
Definir el servicio MySQL
El comando usado para eso en el container fue:
Unset
docker run -d \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:8.0
Definimos el nuevo servicio y lo nombramos mysql así obtiene el alias de la red automáticamente.
Especificamos la imagen a usar.
Unset
services:
app:
# The app service definition
mysql:
image: mysql:8.0
A continuación, definimos el mapeo de volumes. Cuando ejecutamos el contenedor con docker run,
necesitamos crear manualmente el volume con nombre, pero no cuando lo ejecutamos con Compose.
Necesitamos definir el volume en la sección de volumes de nivel superior y luego especificar el punto
de montaje en la configuración del servicio. Al proporcionar solo el nombre del volume, se utilizan las
opciones predeterminadas.
Unset
services:
app:
# The app service definition
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
volumes:
todo-mysql-data:
Finalmente, especificamos las variables de entorno..
Unset
services:
app:
# The app service definition
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
El [Link] debería verse completo, de esta forma:
Unset
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- [Link]:3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
Ejecutar la pila de aplicaciones
Ahora que tenemos el [Link], podemos iniciar la solicitud.
1. Eliminar otras copias de los contenedores primero. Usamos docker ps para enumerar los
contenedores y docker rm -f <ids> para eliminarlos.
2. Iniciamos la pila de aplicaciones usando el docker compose up. Agregamos la flag -d para ejecutar
todo en segundo plano.
Unset
docker-compose up -d
Deberíamos ver un resultado así:
Unset
Creating network "app_default" with the default driver
Creating volume "app_todo-mysql-data" with default driver
Creating app_app_1 ... done
Creating app_mysql_1 ... done
Docker Compose creó el volumen y también una red. Docker Compose crea automáticamente una red
específica para la pila de aplicaciones (razón por la cual no definimos una en el archivo Compose).
3. Con docker-compose logs -f se ven los registros de cada uno de los servicios entrelazados en
una sola secuencia. Esto es útil cuando deseamos estar atentos a problemas relacionados con el
tiempo. La flag -f sigue el registro, por lo que brindará resultados en vivo a medida que se genera.
Ejecutado el comando, vemos un resultado así:
Unset
mysql_1 | 2019-10-03T[Link].083639Z 0 [Note] mysqld: ready for connections.
mysql_1 | Version: '8.0.31' socket: '/var/run/mysqld/[Link]' port: 3306 MySQL Community
Server (GPL)
app_1 | Connected to mysql db at host mysql
app_1 | Listening on port 3000
El nombre del servicio se muestra al principio de la línea (a menudo en color) para ayudar a distinguir los
mensajes. Si quiero ver los registros de un servicio específico, puedo agregar el nombre del servicio al
final del comando de registros (por ejemplo, docker compose logs -f app).
4. Aquí ya deberíamos poder abrir la aplicación en [Link] y verla correr.
Ver la pila de aplicaciones en Docker Dashboard
En el Docker Dashboard hay un grupo llamado Getting-started-app que es el nombre del proyecto de
Docker Compose y se usa para agrupar los contenedores. De forma predeterminada, el nombre del
proyecto es simplemente el nombre del directorio [Link] en el que se encuentra.
Si expandimos la pila, vemos los dos contenedores que definimos en el archivo Compose. Los nombres
también son un poco más descriptivos, ya que siguen el patrón de
<service-name>-<replica-number>. Por ende, es muy fácil ver rápidamente qué contenedor es
nuestra aplicación y qué contenedor es la base de datos MySQL.
Eliminar todo
Cuando queremos finalizar todo, ejecutamos docker compose down o presionamos la papelera en
Docker Dashboard para toda la aplicación. Los contenedores se detendrán y la red será eliminada.
Advertencia:
De forma predeterminada, los volumes con nombre en nuestro archivo de redacción no se eliminan
cuando ejecutamos docker compose down. Si deseamos eliminar los volumes, debemos agregar
--volumes.
Docker Dashboard no elimina volúmenes cuando elimina la pila de aplicaciones.
Mejores prácticas de creación de images
Capas de image
Usando docker image history vemos el comando que se usó para crear cada capa dentro de una
imagen.
Deberíamos obtener algo similar a:
Unset
IMAGE CREATED CREATED BY SIZE COMMENT
a78a40cbf866 18 seconds ago /bin/sh -c #(nop) CMD ["node" "src/index.j⦠0B
f1d1808565d6 19 seconds ago /bin/sh -c yarn install --production 85.4MB
a2c054d14948 36 seconds ago /bin/sh -c #(nop) COPY dir:5dc710ad87c789593⦠198kB
9577ae713121 37 seconds ago /bin/sh -c #(nop) WORKDIR /app 0B
b95baba1cfdb 13 days ago /bin/sh -c #(nop) CMD ["node"] 0B
<missing> 13 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry⦠0B
<missing> 13 days ago /bin/sh -c #(nop) COPY file:238737301d473041⦠116B
<missing> 13 days ago /bin/sh -c apk add --no-cache --virtual .bui⦠5.35MB
<missing> 13 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.21.1 0B
<missing> 13 days ago /bin/sh -c addgroup -g 1000 node && addu⦠74.3MB
<missing> 13 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.14.1 0B
<missing> 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 13 days ago /bin/sh -c #(nop) ADD file:e69d441d729412d24⦠5.59MB
Cada línea representa una capa en la image. La pantalla aquí muestra la base en la parte inferior con la
capa más nueva en la parte superior. Con esto, vemos rápidamente el tamaño de cada capa, lo que
ayuda a diagnosticar images grandes.
Varias de las líneas están truncadas. Si agregamos --no-trunc, vemos el resultado completo.
Unset
docker image history --no-trunc getting-started
Almacenamiento en caché de capas
Para ayudar a reducir los tiempos de compilación de las imágenes de su contenedor, debemos saber que
una vez que una capa cambia, todas las capas posteriores también deben recrearse.
Mirando el Dockerfile creado:
Unset
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/[Link]"]
Volviendo a la salida del historial de images, vemos que cada comando en Dockerfile se convierte en una
nueva capa en la imagen. No tiene mucho sentido enviar las mismas dependencias cada vez que
construimos.
Para solucionarlo, necesitamos reestructurar el Dockerfile para ayudar a admitir el almacenamiento en
caché de las dependencias. Para aplicaciones basadas en nodos, esas dependencias se definen en el
[Link]. Podemos copiar solo ese archivo primero, instalar las dependencias y luego copiar todo
lo demás. Luego, solo recrearemos las dependencias de hilo si hubo un cambio en el archivo
[Link].
Actualizamos el Dockerfile para copiar el [Link] primero, instalar las dependencias y luego
copiar todo lo demás.
Unset
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY [Link] [Link] ./
RUN yarn install --production
COPY . .
CMD ["node", "src/[Link]"]
Creamos un archivo con el nombre .dockerignore en la misma carpeta que Dockerfile con el siguiente
contenido.
Unset
node_modules
Los archivos son una manera fácil de copiar selectivamente solo archivos relevantes de imágenes. Más
info: acá. En este caso, la carpeta node_modules debe omitirse en el segundo paso COPY porque, de
lo contrario, posiblemente se sobrescribirían los archivos creados por el comando en el RUN.
Creamos una nueva imagen usando docker build:
Unset
docker build -t getting-started .
Deberíamos ver un resultado como el siguiente:
Unset
[+] Building 16.1s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 175B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for [Link]/library/node:18-alpine
=> [internal] load build context
=> => transferring context: 53.37MB
=> [1/5] FROM [Link]/library/node:18-alpine
=> CACHED [2/5] WORKDIR /app
=> [3/5] COPY [Link] [Link] ./
=> [4/5] RUN yarn install --production
=> [5/5] COPY . .
=> exporting to image
=> => exporting layers
=> => writing image
sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d25
=> => naming to [Link]/library/getting-started
Ahora, hacemos un cambio en el src/static/[Link]. Por ejemplo, cambiamos <title> a
"The Awesome Todo App".
Creamos una nueva imagen usando docker build:
Unset
docker build -t getting-started .
y esta vez se verá algo distinto el resultado:
Unset
[+] Building 1.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 37B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for [Link]/library/node:18-alpine
=> [internal] load build context
=> => transferring context: 450.43kB
=> [1/5] FROM [Link]/library/node:18-alpine
=> CACHED [2/5] WORKDIR /app
=> CACHED [3/5] COPY [Link] [Link] ./
=> CACHED [4/5] RUN yarn install --production
=> [5/5] COPY . .
=> exporting to image
=> => exporting layers
=> => writing image
sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda
=> => naming to [Link]/library/getting-started
La compilación fue mucho más rápida y varios pasos usan capas previamente almacenadas en caché.
Enviar y extraer esta imagen y sus actualizaciones también será mucho más rápido.
Construcciones de varias etapas
Las compilaciones de varias etapas son una herramienta increíblemente poderosa que ayuda a utilizar
varias etapas para crear una imagen. Hay varias ventajas:
- Separar las dependencias en tiempo de compilación de las dependencias en tiempo de ejecución.
- Reducir el tamaño general de la imagen enviando solo lo que la aplicación necesita para
ejecutarse.
Ejemplo de Maven/Tomcat
Al crear aplicaciones basadas en Java, necesitamos un JDK para compilar el código fuente en código de
bytes de Java. Sin embargo, ese JDK no es necesario en producción. Además, es posible que estemos
utilizando herramientas como Maven o Gradle para ayudar a crear la aplicación, los cuales tampoco son
necesarios en la imagen final. Ayuda para compilaciones de varias etapas.
Unset
# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package
FROM tomcat
COPY --from=build /app/target/[Link] /usr/local/tomcat/webapps
En este ejemplo, utilizamos una etapa (llamada build) para realizar la compilación de Java real usando
Maven. En la segunda etapa (comenzando en FROM tomcat), copiamos archivos de la build. La image
final es solo la última etapa que se crea, que se puede anular usando la --target.
Ejemplo de reacción
Al crear aplicaciones React, necesitamos un entorno Node para compilar el código JS (normalmente
JSX), las hojas de estilo SASS y más en HTML, JS y CSS estático. Si no estamos realizando renderizado
del lado del servidor, ni siquiera necesitamos un entorno Node para la compilación de producción.
Podemos enviar los recursos estáticos en un contenedor nginx estático.
Unset
# syntax=docker/dockerfile:1
FROM node:18 AS build
WORKDIR /app
COPY package* [Link] ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
En el ejemplo anterior de Dockerfile, utilizamos la image node:18 para realizar la compilación
(maximizando el almacenamiento en caché de la capa) y luego copiamos la salida en un contenedor
nginx.
¿Qué sigue?
Podés seguir investigando aquí:
[Link]
Troubleshooting:
si no podés stopear / eliminar containers por permisos denegados, usá sudo aa-remove-unknown.
Links de interés:
Why you shouldn't use ENV variables for secret data
CLI Cheat Sheet