Imágenes Docker
DockerHub
Las imágenes de Docker provienen de Docker Hub por defecto. Docker Hub funciona como si fuera un GitHub de imágenes. Es el repositorio de imágenes oficial de Docker, que tiene como función principal almacenar imágenes.
¿Existe solo Docker Hub? No, existen varios otros registries:
- DTR Docker Trusted Registry (Aparece en el examen)
- ECR de AWS
- ACR de Azure
- GitHub Package Registry
- GitLab Container Registry
- GAR Google Artifact Registry
- Harbor Container Registry
- Sonatype Nexus
- JFrog Artifactory
¿Docker Hub solamente almacena imágenes? No
- Aloja las imágenes
- Autentica usuario
- Automatiza el proceso de construcción de imágenes, a través de triggers y webhooks
- Integración con otros repositorios: GitHub, Bitbucket, GitLab, etc.
Para subir una imagen propia a Docker Hub es necesario crear una cuenta. Para buscar una imagen no es necesario estar logueado.
Ejemplo de una búsqueda de la imagen de ubuntu https://hub.docker.com/search?q=ubuntu
Para iniciar sesión en docker hub
vagrant@master:~$ docker login -u davidpuziol # iniciando sesión
Password:
WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
vagrant@master:~$
Esta advertencia significa que la contraseña estará sin encriptar en el directorio indicado. Si realizas una decodificación base64 del hash dentro de ~/.docker/config.json verás la contraseña real de hecho.
# Decodificación de token base64
vagrant@master:~$ echo "token" | base64 --decode
davidpuziol:senha
Si alguien inicia sesión en la misma máquina donde ejecutaste el login de tu Docker Hub, esta persona debe conseguir tus credenciales, por eso es necesario siempre hacer un logout que hará que config.json se borre.
vagrant@master:~$ docker logout # cerrando sesión
Removing login credentials for https://index.docker.io/v1/
vagrant@master:~$ cat ~/.docker/config.json
{
"auths": {}
}
Image
¿Qué es una imagen? Es un paquete ejecutable. Es un programa, solo que todo lo que ese programa necesita para funcionar está dentro. No es solo el programa, sino también todas sus dependencias. Tiene bibliotecas, variables de entorno, archivos de configuración, código del programa que será ejecutado, etc.
Un detalle sobre image es que hoy existe un estándar a ser respetado para que todas las imágenes funcionen agnósticas a la plataforma de contenedor que estés usando. Este movimiento es realizado por la OCI (Open Container Initiative) opencontainers.org que tiene gobernanza por la propia Linux Foundation. Básicamente son las reglas de cómo una imagen debe ser definida para ejecución o creación. Docker hizo la donación de sus manifiestos para ejecución y formato de contenedores, para la OCI y otros proyectos comenzaron a nacer.
Las imágenes trabajan en base a capas. Solamente la capa en la parte superior de la pila puede ser escrita y las demás debajo son solo lectura. Por eso puedes utilizar una imagen como base y crear otra. Esta es una forma de reutilización de imágenes.

Los contenedores utilizan la imagen base y solamente en su bloque Read Write trabaja con el diff en memoria. De esta forma no es necesario tener una copia de la imagen en cada uno de los contenedores, sino compartir la imagen, ahorrando un recurso precioso de espacio en disco. Esta tecnología se llama COW (Copy On Write). Por eso esta capa es solamente Read.

Observando esta imagen podemos entender que todos los contenedores juntos ocupan la imagen_base + diff container1 + diff container 2 + diff container 3 + diff container n. Si fuera en máquinas virtuales cada contenedor además de su diff tendría una imagen extra en cada una de las VMs.
Un grupo de capas read only es lo que llamamos imagen.
history e inspect
Este comando se utiliza para ver las capas de una imagen.
}vagrant@master:~$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
debian latest 4eacea30377a 3 weeks ago 124MB
vagrant@master:~$ docker image history debian # verificando el history
IMAGE CREATED CREATED BY SIZE COMMENT
4eacea30377a 3 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:dd3d4b31d7f1d4062… 124MB
vagrant@master:~$
Para inspeccionar una imagen
agrant@master:~$ docker image inspect debian:latest # inspeccionando la imagen
[
{
"Id": "sha256:4eacea30377a698ef8fbec99b6caf01cb150151cbedc8e0b1c3d22f134206f1a",
"RepoTags": [
"debian:latest"
],
"RepoDigests": [
"debian@sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510"
],
"Parent": "",
"Comment": "",
"Created": "2022-05-28T01:20:12.59253565Z",
"Container": "86b72732f393d3e9fa438dd5261a9c9e1903338d14171c687b3f3e7b1ede253f",
"ContainerConfig": {
"Hostname": "86b72732f393",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"bash\"]"
],
"Image": "sha256:d31d2b49944f50ccb549e957eb19f6115d9f810044fa211c6ae20f3583a8e391",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"DockerVersion": "20.10.12",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"bash"
],
"Image": "sha256:d31d2b49944f50ccb549e957eb19f6115d9f810044fa211c6ae20f3583a8e391",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"Architecture": "amd64",
"Os": "linux",
"Size": 124005260,
"VirtualSize": 124005260,
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/f9386627907896f35bffaae2876719e9f7d303361b00702e6fdf75aeb4e9807b/merged",
"UpperDir": "/var/lib/docker/overlay2/f9386627907896f35bffaae2876719e9f7d303361b00702e6fdf75aeb4e9807b/diff",
"WorkDir": "/var/lib/docker/overlay2/f9386627907896f35bffaae2876719e9f7d303361b00702e6fdf75aeb4e9807b/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [ # aquí podemos observar las capas
"sha256:e7597c345c2eb11bce09b055d7c167c526077d7c65f69a7f3c6150ffe3f557ea"
]
},
"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]
vagrant@master:~$
Creando una imagen a partir de un contenedor en ejecución
Vamos a levantar una imagen de debian y allí dentro instalar nginx. Después de todo instalado vamos a hacer un commit para crear una imagen en base a lo que instalamos.
vagrant@master:~$ docker container run -dit --name server-debian debian # ejecutando el contenedor
vagrant@master:~$ docker container exec server-debian apt-get update # ejecutando un comando dentro del contenedor
(removido para mejorar la lectura)
docker container exec server-debian apt-get install nginx -y # ejecutando otro comando en el contenedor
(removido para mejorar la lectura)
vagrant@master:~$ docker container commit server-debian webserver-nginx # haciendo commit del contenedor como está
vagrant@master:~$ docker image ls # verificando si la imagen de hecho ahora está disponible
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver-nginx latest 8e3f55a1d009 55 seconds ago 211MB
debian latest 4eacea30377a 3 weeks ago 124MB
vagrant@master:~$
Esto es considerado una chapuza. Cuando creamos una imagen a partir de un contenedor en ejecución, tenemos mucha basura dentro de esa imagen, como logs por ejemplo, archivos temporales, etc. Esto no es lo correcto.
Vamos a hacer un análisis de una imagen pura de nginx.
vagrant@master:~$ docker image pull nginx # descargando la imagen de nginx directo del dockerhub
Using default tag: latest
latest: Pulling from library/nginx
42c077c10790: Pull complete
62c70f376f6a: Pull complete
915cc9bd79c2: Pull complete
75a963e94de0: Pull complete
7b1fab684d70: Pull complete
db24d06d5af4: Pull complete
Digest: sha256:2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
vagrant@master:~$ docker image ls # verificando
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver-nginx latest 8e3f55a1d009 5 minutes ago 211MB
nginx latest 0e901e68141f 3 weeks ago 142MB
debian latest 4eacea30377a 3 weeks ago 124MB
Vea que la imagen de nginx oficial tiene 142 mb contra 211 de la que creamos. Si no pasas la versión él descarga la última latest
save e load
Vamos a guardar la imagen que creamos en un archivo
vagrant@master:~$ docker container ls --all # listando todos los contenedores existentes
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ad7131235d91 debian "bash" 3 hours ago Exited (255) About a minute ago server-debian
vagrant@master:~$ docker image ls # listando las imágenes del sistema
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver-nginx latest 8e3f55a1d009 3 hours ago 211MB
nginx latest 0e901e68141f 3 weeks ago 142MB
debian latest 4eacea30377a 3 weeks ago 124MB
vagrant@master:~$ docker image save webserver-nginx -o webserver-ngin.tar # guardando la imagen en un archivo tar
vagrant@master:~$ ls -lha # verificando para ver si guardó
total 207M
drwxr-xr-x 5 vagrant vagrant 4.0K Jun 20 07:20 .
drwxr-xr-x 4 root root 4.0K Jun 19 18:30 ..
-rw------- 1 vagrant vagrant 5.0K Jun 20 05:07 .bash_history
-rw-r--r-- 1 vagrant vagrant 220 Jun 15 21:53 .bash_logout
-rw-r--r-- 1 vagrant vagrant 3.7K Jun 15 21:53 .bashrc
drwx------ 2 vagrant vagrant 4.0K Jun 19 18:30 .cache
drwx------ 2 vagrant vagrant 4.0K Jun 20 03:20 .docker
-rw-r--r-- 1 vagrant vagrant 807 Jun 15 21:53 .profile
drwx------ 2 vagrant vagrant 4.0K Jun 19 18:30 .ssh
-rw-rw-r-- 1 vagrant vagrant 0 Jun 20 01:26 5000
-rw-rw-r-- 1 vagrant vagrant 20K Jun 19 18:35 get-docker.sh
-rw------- 1 vagrant vagrant 207M Jun 20 07:20 webserver-ngin.tar
vagrant@master:~$ docker container rm -f server-debian
server-debian # removiendo el contenedor forzadamente
vagrant@master:~$ docker container ls --all # Verificando
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
vagrant@master:~$ docker image rm webserver-nginx
Untagged: webserver-nginx:latest # removiendo la imagen
Deleted: sha256:8e3f55a1d0097d1664ee33c2b418ecafa41ee9abc94fc287a0fc95ac2a982f9f
Deleted: sha256:fd023343dfcf8099a714adb7c80405f489a47c9ccfb2c7d91a9b285d78314c58
vagrant@master:~$ docker image ls # verificando
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 0e901e68141f 3 weeks ago 142MB
debian latest 4eacea30377a 3 weeks ago 124MB
vagrant@master:~$ docker image load -i webserver-ngin.tar # cargando la imagen
2cafde399cfb: Loading layer [==================================================>] 87.71MB/87.71MB
Loaded image: webserver-nginx:latest
vagrant@master:~$ docker image ls # verificando
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver-nginx latest 8e3f55a1d009 3 hours ago 211MB
nginx latest 0e901e68141f 3 weeks ago 142MB
debian latest 4eacea30377a 3 weeks ago 124MB
vagrant@master:~$
Durante la carga de la imagen él cargó 87MB, pero aparece que webserver-nginx tiene 211, ¿por qué? Una de las capas de ese archivo era el debian que estaba disponible con 124 MB. 124MB de debian + 87MB del save = 211MB. Él solo cargó la parte que no tenía.
Si hacemos el delete tanto de debian como de webserver y cargar nuevamente. Vea que ahora él va a cargar dos imágenes. Podemos constatar que él siempre carga las capas y no las imágenes completamente. Luego hice un pull en la imagen de debian y él muestra que ya existe.
vagrant@master:~$ docker image rm $(docker image ls -q) # removiendo todas las imágenes
vagrant@master:~$ docker image load -i webserver-ngin.tar # cargando la imagen guardada
e7597c345c2e: Loading layer [==================================================>] 129.2MB/129.2MB
2cafde399cfb: Loading layer [==================================================>] 87.71MB/87.71MB
Loaded image: webserver-nginx:latest
vagrant@master:~$ docker image ls # listando las imágenes para verificar
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver-nginx latest 8e3f55a1d009 4 hours ago 211MB
vagrant@master:~$ docker pull debian # descargando la imagen de debian
Using default tag: latest
latest: Pulling from library/debian
e756f3fdd6a3: Already exists 3 # vea que ya existe
Digest: sha256:3f1d6c17773a45c97bd8f158d665c9709d7b29ed7917ac934086ad96f92e4510
Status: Downloaded newer image for debian:latest
docker.io/library/debian:latest
Dockerfile
https://docs.docker.com/engine/reference/builder/

Los archivos que vamos a utilizar están dentro de dockerfiles
El Dockerfile es la manera correcta de crear una imagen. A través del dockerfile podemos crear una o varias imágenes.
El Dockerfile necesita ser escrito de esta manera Dockerfile con la D mayúscula. El Dockerfile es una secuencia de comandos que serán ejecutados para generar la imagen.
Algunos comandos esenciales:
- FROM - Cuál es la imagen base
- COPY - Copia archivos o directorios de origen local a la imagen del contenedor
- RUN - Ejecuta un comando dentro del contenedor
- ADD - Casi igual que copy pero acepta origen no local como url y alterar permisos
- EXPOSE - Expone un puerto para el daemon. Avisa a docker cuál es el puerto de red que va a utilizar
- ENTRYPOINT - Es lo que mantiene el contenedor vivo
- CMD - Argumentos con el entrypoint.
Para entender el dockerfile vamos a empezar a montar algunos.
Diferencia entre ENTRYPOINT y CMD
Vamos a utilizar el dockerfile
El ENTRYPOINT es el programa que mantiene el contenedor vivo. El CMD son los argumentos que pasamos a ese entrypoint.
Para entender mejor esta diferencia podemos crear un archivo Dockerfile y mostrar la diferencia.
Vamos a crear una carpeta para trabajar dentro del master.
mkdir -p dockerfiles/echo-container
cd dockerfiles/echo-container
cat << EOF > Dockerfile
FROM alpine
ENTRYPOINT [ "echo" ]
CMD ["--help"]
EOF
Observe que estoy dando el comando dentro de la carpeta donde está el Dockerfile que estamos buildando, por eso usando "."
~vagrant@master:~/dockerfiles/echo-container$ docker image build -t echo-container . # buildando una imagen
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM alpine # primera capa
latest: Pulling from library/alpine
2408cc74d12b: Pull complete
Digest: sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c
Status: Downloaded newer image for alpine:latest
---> e66264b98777
Step 2/3 : ENTRYPOINT [ "echo" ] # segunda capa
---> Running in 7100d2e1505b
Removing intermediate container 7100d2e1505b
---> 0c771e92b47d
Step 3/3 : CMD ["--help"] # tercera capa
---> Running in 5ea99678316a
Removing intermediate container 5ea99678316a
---> 6da75d1ad4bb
Successfully built 6da75d1ad4bb
Successfully tagged echo-container:latest
Si usamos la imagen vamos a entender la diferencia entre entrypoint y cmd. Si no pasamos nada el cmd ya fue definido como --help. Si pasamos algún parámetro él escribirá lo que pasamos, pues el cmd puede sufrir overwrite por defecto.
vagrant@master:~/dockerfiles/echo-container$ docker container run echo-container
--help
vagrant@master:~/dockerfiles/echo-container$ docker container run echo-container estou aprendendo docker
estou aprendendo docker
Ahora vamos a mejorar nuestro webserver nginx vaya a la carpeta dockerfiles y cree una nueva carpeta y entre dentro de ella.
vagrant@master:~/dockerfiles/echo-container$ cd ..
vagrant@master:~/dockerfiles$ mkdir webserver
vagrant@master:~/dockerfiles$ cd webserver
cat << EOF > Dockerfile
FROM debian
RUN apt-get update; \
apt-get install git apache2 -yq
EXPOSE 80
ENTRYPOINT ["apachectl"]
CMD ["-D", "FOREGROUND"]
EOF
vagrant@master:~/dockerfiles/webserver$ docker image build -t webserver
...
...
...
Step 5/5 : CMD ["-D", "FOREGROUND"]
---> Running in 8d324204a768
Removing intermediate container 8d324204a768
---> 29c57f4397e7
Successfully built 29c57f4397e7
Successfully tagged webserver:latest
Fueron creadas 5 capas. Si dividiésemos el RUN en
RUN apt-get update
RUN apt-get install git apache2 -yq
en vez de colocar todo en una línea iría a crear 6 capas. Una buena práctica es mantener el menor número de capas.
Push de la imagen al registry
Necesitas etiquetar una imagen para tu usuario para poder subir al dockerhub. Si no pasamos una versión él va a adoptar la latest, pero vamos a pasar v1.
vagrant@master:~/dockerfiles/webserver$ docker login
Username: davidpuziol
Password:
Login Succeeded
vagrant@master:~/dockerfiles/webserver$ docker image tag echo-container:latest davidpuziol/echo-container:v1
vagrant@master:~/dockerfiles/webserver$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver latest 29c57f4397e7 9 minutes ago 300MB
echo-container latest 6da75d1ad4bb 19 minutes ago 5.53MB
davidpuziol/echo-container v1 6da75d1ad4bb 19 minutes ago 5.53MB
webserver-nginx latest 8e3f55a1d009 10 hours ago 211MB
debian latest 4eacea30377a 3 weeks ago 124MB
alpine latest e66264b98777 3 weeks ago 5.53MB
vagrant@master:~/dockerfiles/webserver$ docker image push davidpuziol/echo-container:v1
The push refers to repository [docker.io/davidpuziol/echo-container]
24302eb7d908: Mounted from library/alpine
v1: digest: sha256:880b12fea1dc826477b5cb0e6baab84c5c8927bb4dbb79ae00fcf9cfc5b7ede1 size: 528
vagrant@master:~/dockerfiles/webserver$
Podemos ahora hacer un pull directo de esa imagen desde cualquier lugar. Vamos a verificar y ver que ella está allí ya en dockerhub.

Dockerfile context
Cuando estamos haciendo un build de la imagen, el . (punto) representa el lugar donde está el contexto del dockerfile. Es el directorio que será enviado al contenedor. Cuando pasamos el contexto sin especificar dónde está el dockerfile él entiende que el archivo Dockerfile está en el mismo contexto.
Para crear la imagen como vimos anteriormente es necesario el comando
docker image build -t nombre:tag .
Podemos pasar el contexto y especificar el dockerfile con
docker image build -t nombre:tag -f dockerfilepath contextpath
Debemos tener cuidado con qué contexto vamos a pasar para el build pues podemos pasar archivos que contaminan y aumentan el contexto. Vamos a ver la diferencia.
Vamos a probar con nuestro echo container en la carpeta de nuestro propio Dockerfile. Fue utilizado el time delante del comando para capturar el tiempo de procesamiento de ese build.
vagrant@master:~/dockerfiles/echo-container$ tree
.
└── Dockerfile
0 directories, 1 file
vagrant@master:~/dockerfiles/echo-container$ time docker image build -t teste .
# Vea el tamaño 2.048kb
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM alpine
---> e66264b98777
Step 2/3 : ENTRYPOINT [ "echo" ]
---> Running in 58d981554e13
Removing intermediate container 58d981554e13
---> 92912c05d84c
Step 3/3 : CMD ["--help"]
---> Running in 4de69295ddb1
Removing intermediate container 4de69295ddb1
---> 64849f04738f
Successfully built 64849f04738f
Successfully tagged teste:latest
# Vea el tiempo de ejecución
real 0m0.369s
user 0m0.045s
sys 0m0.023s
Ahora vamos a crear un archivobasura con un poco de caracteres dentro y subir el directorio usar el contexto con un directorio arriba. Tamaño
vagrant@master:~/dockerfiles/echo-container$ cd ..
vagrant@master:~/dockerfiles$ vim arquivolixo
vagrant@master:~/dockerfiles$ tree
.
├── arquivolixo
└── echo-container
└── Dockerfile
1 directory, 2 files
vagrant@master:~/dockerfiles$ time docker image build -t teste:v2 -f echo-container/Dockerfile .
# Vea cómo el tamaño ya aumentó
Sending build context to Docker daemon 3.584kB
Step 1/3 : FROM alpine
---> e66264b98777
Step 2/3 : ENTRYPOINT [ "echo" ]
---> Using cache
---> 92912c05d84c
Step 3/3 : CMD ["--help"]
---> Using cache
---> 64849f04738f
Successfully built 64849f04738f
Successfully tagged teste:v2
# y el tiempo también
real 0m0.107s
user 0m0.020s
sys 0m0.020s
Ahora vamos a copiar más basura aún, un montón de log de /var/log a la carpeta para aumentar bastante.
vagrant@master:~/dockerfiles$ sudo cp -r /var/log/ .
vagrant@master:~/dockerfiles$ tree
.
├── arquivolixo
├── echo-container
│ └── Dockerfile
└── log
├── apt
│ ├── eipp.log.xz
│ ├── history.log
│ └── term.log
├── auth.log
├── btmp
├── cloud-init-output.log
├── cloud-init.log
├── dist-upgrade
├── dmesg
├── dpkg.log
├── journal
│ └── 925f882e39344f0db2ae9ae1bd831c5e
│ ├── system.journal
│ └── user-1000.journal
├── kern.log
├── landscape
│ └── sysinfo.log
├── lastlog
├── private
├── syslog
├── unattended-upgrades
│ └── unattended-upgrades-shutdown.log
└── wtmp
vagrant@master:~/dockerfiles$ sudo chown vagrant:vagrant * -R # Necesitamos cambiar el permiso sino el build no consigue leer los archivos.
vagrant@master:~/dockerfiles$ time docker image build -t teste:v3 -f echo-container/Dockerfile .
# Mira cómo ya aumentó a 17.62MB
Sending build context to Docker daemon 17.62MB
Step 1/3 : FROM alpine
---> e66264b98777
Step 2/3 : ENTRYPOINT [ "echo" ]
---> Using cache
---> 92912c05d84c
Step 3/3 : CMD ["--help"]
---> Using cache
---> 64849f04738f
Successfully built 64849f04738f
Successfully tagged teste:v3
# El tiempo también
real 0m0.237s
user 0m0.046s
sys 0m0.017s
Ahora vamos a verificar el tamaño de las imágenes
9 directories, 19 files
vagrant@master:~/dockerfiles$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
teste latest 64849f04738f 4 minutes ago 5.53MB
teste v2 64849f04738f 4 minutes ago 5.53MB
teste v3 64849f04738f 4 minutes ago 5.53MB
alpine latest e66264b98777 4 weeks ago 5.53MB
A pesar de que el contexto sea pasado para la creación de la imagen, él no utilizó el contexto, por lo tanto todas las imágenes permanecieron del mismo tamaño, pero el tiempo de build fue empeorando para procesar los archivos del contexto.
Voy a hacer la descarga de la iso de ubuntu y pasar el contexto en el home del usuario y vemos medir el tiempo. Vamos a generar la v4.
# Recordando que estamos en /home/vagrant o sea, el home del usuario
vagrant@master:~$ tree
├── dockerfiles
│ ├── arquivolixo
│ ├── echo-container
│ │ └── Dockerfile
│ └── log
│ ├── apt
│ │ ├── eipp.log.xz
│ │ ├── history.log
│ │ └── term.log
│ ├── auth.log
│ ├── btmp
│ ├── cloud-init-output.log
│ ├── cloud-init.log
│ ├── dist-upgrade
│ ├── dmesg
│ ├── dpkg.log
│ ├── journal
│ │ └── 925f882e39344f0db2ae9ae1bd831c5e
│ │ ├── system.journal
│ │ └── user-1000.journal
│ ├── kern.log
│ ├── landscape
│ │ └── sysinfo.log
│ ├── lastlog
│ ├── private
│ ├── syslog
│ ├── unattended-upgrades
│ │ └── unattended-upgrades-shutdown.log
│ └── wtmp
# la imagen aquí...
├── ubuntu-22.04-desktop-amd64.iso
├── wget-log
└── wget-log.1
vagrant@master:~$ time docker image build -t teste:v4 -f dockerfiles/echo-container/Dockerfile .
# Mira cuánto cargó
Sending build context to Docker daemon 3.673GB
Step 1/3 : FROM alpine
---> e66264b98777
Step 2/3 : ENTRYPOINT [ "echo" ]
---> Using cache
---> 92912c05d84c
Step 3/3 : CMD ["--help"]
---> Using cache
---> 64849f04738f
Successfully built 64849f04738f
Successfully tagged teste:v4
# mira el tiempo 50 segundos
real 0m50.932s
user 0m1.878s
sys 0m5.957s
vagrant@master:~$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
teste v2 64849f04738f 55 minutes ago 5.53MB
teste v3 64849f04738f 55 minutes ago 5.53MB
# pero la imagen está del mismo tamaño
teste v4 64849f04738f 55 minutes ago 5.53MB
alpine latest e66264b98777 4 weeks ago 5.53MB
Dockerfile mejores prácticas
Una buena práctica es crear el siguiente esquema de directorios.
vagrant@master:~/dockerfiles$ tree -a exemplo-squeme/
exemplo-squeme/
├── context
│ ├── .dockerignore
│ ├── arquivo1
│ └── arquivo2
└── image
└── Dockerfile
2 directories, 4 files
vagrant@master:~/dockerfiles$
Dentro de la carpeta image tenemos nuestro Dockerfile que puede servir tanto para ambiente de producción como desarrollo, pero cambiará el context.
Dentro de context tendremos los archivos útiles a nuestro contenedor y el .dockerignore que irá a ignorar algunos archivos que allí pueden estar.
Aprendiendo cómo crear las imágenes
Vamos a un nuevo ejemplo montando el mismo esquema definido arriba
Vamos a copiar la carpeta log que tenemos para dentro de nuestro context solo para tener basura junto y usamos el git ignore
vagrant@master:~/dockerfiles$ mkdir -p exemplo1/image
vagrant@master:~/dockerfiles$ mkdir -p exemplo1/context
vagrant@master:~/dockerfiles$ echo "Aprendendo sobre images" > exemplo1/context/arquivo.txt
# colocando para ser ignorado log en .dockerignore
vagrant@master:~/dockerfiles$ echo "log" > exemplo1/context/.dockerignore
# el comando COPY va a copiar todo del context en el caso el . para dentro de /files en el contenedor y después vamos a imprimir lo que está en el archivo
vagrant@master:~/dockerfiles$ cat << EOF > exemplo1/image/Dockerfile
FROM busybox
COPY . /files
RUN cat /files/arquivo.txt
EOF
vagrant@master:~/dockerfiles$ tree -a -du -h -L 3 exemplo1
exemplo1
├── [vagrant 4.0K] context
│ └── [vagrant 4.0K] log
│ ├── [vagrant 4.0K] apt
│ ├── [vagrant 4.0K] dist-upgrade
│ ├── [vagrant 4.0K] journal
│ ├── [vagrant 4.0K] landscape
│ ├── [vagrant 4.0K] private
│ └── [vagrant 4.0K] unattended-upgrades
└── [vagrant 4.0K] image
9 directories
vagrant@master:~/dockerfiles$
Vamos a ejecutar el contenedor y entrar dentro de él para verificar lo que fue copiado y si el .dockerignore funcionó
vagrant@master:~/dockerfiles$ docker container run --rm -it --rm teste1:v1 sh
/ $ ls -lha
total 48K
drwxr-xr-x 1 root root 4.0K Jun 22 20:00 .
drwxr-xr-x 1 root root 4.0K Jun 22 20:00 ..
-rwxr-xr-x 1 root root 0 Jun 22 20:00 .dockerenv
drwxr-xr-x 2 root root 12.0K Jun 6 22:13 bin
drwxr-xr-x 5 root root 360 Jun 22 20:00 dev
drwxr-xr-x 1 root root 4.0K Jun 22 20:00 etc
# CARPETA FILES
drwxr-xr-x 2 root root 4.0K Jun 22 19:55 files
drwxr-xr-x 2 nobody nobody 4.0K Jun 6 22:13 home
dr-xr-xr-x 190 root root 0 Jun 22 20:00 proc
drwx------ 1 root root 4.0K Jun 22 20:00 root
dr-xr-xr-x 13 root root 0 Jun 22 20:00 sys
drwxrwxrwt 2 root root 4.0K Jun 6 22:13 tmp
drwxr-xr-x 3 root root 4.0K Jun 6 22:13 usr
drwxr-xr-x 4 root root 4.0K Jun 6 22:13 var
/ # cd files/
/files $ ls -lha
total 16K
drwxr-xr-x 2 root root 4.0K Jun 22 19:55 .
drwxr-xr-x 1 root root 4.0K Jun 22 20:00 ..
-rw-rw-r-- 1 root root 38 Jun 22 19:40 .dockerignore
-rw-rw-r-- 1 root root 24 Jun 22 19:39 arquivo.txt
/files $ cat arquivo.txt
Aprendendo sobre images
Copió todo hasta el .dockerfile que no es necesario, pero lo importante es que el log no está aquí.
Velocidades en el build
Cuando ejecutamos un build el docker crea una caché entre las capas, por lo tanto cuando ejecutamos un build nuevo él hace más rápido. Si es necesario no utilizar la caché solamente pasar en el build el parámetro --no-cache.
Creé un ejemplo 2 para mostrar con la siguiente imagen
FROM debian
COPY . .
RUN apt-get update; apt-get install -y wget ssh vim
ENTRYPOINT bash
Si cambiamos el archivo de copy él irá a invalidar toda la caché de allí en adelante, o sea, va a tener que hacer nuevamente el apt-get update e install
# creando ejemplo 2
vagrant@master:~/dockerfiles/exemplo2/image$ docker image build -t exemplo2 .
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM debian
---> 4eacea30377a
Step 2/4 : COPY . .
---> 6c69c907ce60
Step 3/4 : RUN apt-get update; apt-get install -y wget ssh vim
---> Running in e9f30c474256
#.... PARTE REMOVIDA MUY GRANDE....#
..
done.
Removing intermediate container e9f30c474256
---> 9e568a952863
Step 4/4 : ENTRYPOINT bash
---> Running in dad4f2f64329
Removing intermediate container dad4f2f64329
---> 6ba55366c298
Successfully built 6ba55366c298
Successfully tagged exemplo2:latest
# Creando otra imagen ejemplo3
vagrant@master:~/dockerfiles/exemplo2/image$ docker image build -t exemplo3 .
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM debian
---> 4eacea30377a
Step 2/4 : COPY
# vea el uso de la caché que aprovechó del build anterior del ejemplo 2 . .
---> Using cache
---> 6c69c907ce60
Step 3/4 : RUN apt-get update; apt-get install -y wget ssh vim
# más un uso de caché
---> Using cache
---> 9e568a952863
Step 4/4 : ENTRYPOINT bash
# otro...
---> Using cache
---> 6ba55366c298
Successfully built 6ba55366c298
Successfully tagged exemplo3:latest
El primer build demoró cerca de 20 segundos mientras que el segundo aprovechó la caché y fue instantáneo y no generó ninguna salida grande.
Consejos
A continuación varios consejos para mejorar la creación de las imágenes.
Consejo 1 - El orden importa para la caché
El orden con que los comandos son colocados en el Dockerfile que vamos a montar IMPORTA. Recordando que cada comando crea una capa nueva, por lo tanto si una capa arriba es modificada él invalida toda la caché que fue hecha hacia abajo.
Para el ejemplo arriba, si tuviese algún cambio en el copy él irá a invalidar todas las cachés de los pasos siguientes. Lo ideal sería así:
FROM debian
RUN apt-get update
RUM apt-get install -y wget ssh vim
COPY . .
ENTRYPOINT bash
Consejo 2 - Copy más específico para limitar la ruptura de caché
Un consejo sería separar el copy en varios copy generando capas entre ellos. Archivos que no sufren modificaciones pueden venir al frente. Lo ideal es evitar el copy, pero sabemos que no es tan fácil así. Evite copiar cualquier cosa que tenga modificación y archivos innecesarios, pues él siempre romperá la caché.
Consejo 3 - Identifique las instrucciones que pueden ser agrupadas
Cada instrucción genera una capa diferente, por lo tanto reducir la cantidad de capas es extremadamente importante.
Si observa el uso del punto y coma entre los comandos hace que si el primer comando falla ejecuta el segundo. No es buena práctica, pues puede aún dar algún tipo de problema.
FROM debian
RUN apt-get update; apt-get install -y wget ssh vim
COPY . .
ENTRYPOINT bash
Haga con && que el próximo comando solamente se ejecutará si el primero es exitoso.
FROM debian
RUN apt-get update \
&& apt-get install -y \
wget \
ssh \
vim
COPY . .
ENTRYPOINT bash
Consejo 4 - Remover dependencias innecesarias
No instale paquetes que no necesitan ser instalados.
-
Ejemplo1: Si fuera java, no instale el paquete jdk (desarrollo) sino el jre (solamente el runtime).
-
Ejemplo2: pase el parámetro --no-install-recommends para el apt-get para no instalar los recomendados, solamente irá a instalar los obligatorios.
FROM debian
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
wget \
ssh \
vim
COPY . .
ENTRYPOINT bash
Consejo 5 - Remover la caché del gestor de paquetes
Cuando hace un update del sistema se crean archivos en /var/lib/apt/list y en /var/cache/apt vamos a hacer un análisis de esto. Solo ahí tenemos muchos megas, además de que existen algunos paquetes .deb que no deberían estar allí generando vulnerabilidades.
Analice otros gestores caso no esté utilizando una distro basada en debian.
vagrant@master:/var/cache/apt$ sudo du -hs /var/cache/apt
190M /var/cache/apt
vagrant@master:/var/cache/apt$ sudo du -hs /var/lib/apt/lists/
148M /var/lib/apt/lists/
vagrant@master:/var/cache/apt$
vagrant@master:/var/cache/apt$ tree /var/cache/apt
/var/cache/apt
├── archives
│ ├── apt-transport-https_2.0.9_all.deb
│ ├── containerd.io_1.6.6-1_amd64.deb
│ ├── docker-ce-cli_5%3a20.10.17~3-0~ubuntu-focal_amd64.deb
│ ├── docker-ce-rootless-extras_5%3a20.10.17~3-0~ubuntu-focal_amd64.deb
│ ├── docker-ce_5%3a20.10.17~3-0~ubuntu-focal_amd64.deb
│ ├── docker-compose-plugin_2.6.0~ubuntu-focal_amd64.deb
│ ├── docker-scan-plugin_0.17.0~ubuntu-focal_amd64.deb
│ ├── libssl1.1_1.1.1f-1ubuntu2.15_amd64.deb
│ ├── lock
│ ├── openssl_1.1.1f-1ubuntu2.15_amd64.deb
│ ├── partial [error opening dir]
│ ├── slirp4netns_0.4.3-1_amd64.deb
│ └── tree_1.8.0-1_amd64.deb
├── pkgcache.bin
└── srcpkgcache.bin
2 directories, 14 files
Mejorando el dockerfile... Vamos a imaginar que el vim no fuera necesario... ya vamos a retirarlo.
FROM debian
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
wget \
ssh \
&& rm -rf /var/lib/apt/lists \
&& rm -rf /var/cache/apt
COPY . .
ENTRYPOINT bash
El apt-get clean limpia parcialmente, por lo tanto es mejor borrar el directorio en la raíz.
Buildando la imagen como ejemplo 4 donde borramos las cosas y comparando con el ejemplo 2 y 3 ya vemos una diferencia.
vagrant@master:~/dockerfiles/exemplo2/image$ docker image list | grep exemplo
exemplo4 latest dddd96c9c835 37 seconds ago 139MB
exemplo2 latest 6ba55366c298 5 hours ago 223MB
exemplo3 latest 6ba55366c298 5 hours ago 223MB
Consejo 6 - Utilicen imágenes oficiales cuando sea posible
Procure utilizar imágenes oficiales si ellas ya existen. Pero recordando tiene que ser las oficiales por docker o del propio fabricante. Esto garantiza que la instalación sea hecha de acuerdo y generalmente son las más limpias.
Consejo 7 - Utilizar tag más específica
Procure garantizar una versión específica de la imagen base que está utilizando. Esto evita que use la tag latest y caso venga a buildar una nueva imagen puede ser que existan modificaciones que generen incompatibilidad con algo.
Consejo 8 - Busque por flavors mínimos
Dentro de un mismo proveedor existen varias versiones de la misma imagen. Si buscamos una imagen con openjdk en dockerhub vemos que tenemos diferentes tags. Vale la pena una investigación siempre y probar el build con versiones más reducidas.
Descargue varias imágenes diferentes para analizar
docker image pull openjdk:8
docker image pull openjdk:8-jre
docker image pull openjdk:8-jre-slim
docker image pull openjdk:8-jre-alpine
vagrant@master:~/dockerfiles/exemplo2/image$ docker image ls | grep openjdk
openjdk 8-jre 155efed40fd4 3 weeks ago 274MB
openjdk 8 5bf086edab5e 3 weeks ago 526MB
openjdk 8-jre-slim 1211f482e707 3 weeks ago 194MB
openjdk 8-jre-alpine f7a292bbb70c 3 years ago 84.9MB
Vea cómo la imagen de alpine es muy reducida.
slim = debian = GNU Libc alpine = Alpine = musl bbc
Consejo 9 - Utilizar multi-stage build
El multi-stage es la capacidad de tener varios FROMS dentro de un dockerfile. Usted crea una imagen por ejemplo para buildar el código y retira de esa imagen solamente los binarios y pasa para otra imagen reducida para ejecutar.
Voy a mostrar un ejemplo solamente de cómo funciona a través de un proyecto .net 6.0 imaginario.
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
# Directorio de entrada en el contenedor
WORKDIR /sources
# Copiando todo el supuesto código para dentro
COPY . .
# Comandos para generar los dlls en la carpeta /src
RUN dotnet restore \
&& dotnet publish meuapp/meuapp.csproj -c release -o /src --no-restore --no-cache
# final stage/image
# Observe que cambié la imagen de sdk para aspnet que funciona solo como runtime y es mucho menor
FROM mcr.microsoft.com/dotnet/aspnet:6.0
# Directorio de entrada. En el caso el copy va a mandar para dentro de ese directorio
WORKDIR /app
COPY --from=build /src .
CMD ["meuapp.dll"]
Es posible utilizar el COPY --from viniendo de otra imagen no declarada en el mismo archivo
COPY --from=imageteste:v1 /src .
Prune
Durante el desarrollo de una imagen creamos varias veces la misma tag. Algunas versiones acaban perdiendo la referencia y quedan con <none> para remover esas imágenes basta ejecutar el prune.
docker image prune
docker image prune -a va a remover todas las imágenes que no tienen contenedor usando