En la primera parte aprendimos a manejar contenedores, los datos y volúmenes, las imágenes, a crear aplicaciones con Docker y a usar GPUs. En este segundo capítulo damos el salto a **Docker Compose** para orquestar varios contenedores y a una serie de **temas avanzados** de Docker. 🐳
📚 **Esta entrada es parte de la serie _Guía de Docker_**, dividida en dos capítulos que se leen en orden:
> * Parte 1: Contenedores, imágenes y aplicaciones
* 👉 **Parte 2: Docker Compose y temas avanzados**
Docker compose
Docker compose vs docker-compose
docker-compose fue una herramienta que se creó para ayudar al mantenimiento de imágenes y contenedores y había que instalarla aparte de docker. Sin embargo, docker lo incorporó en sus últimas versiones y ya no es necesario instalarla; sin embargo, para usarla, en vez de usar el comando docker-compose hay que usar el comando docker compose. En muchos sitios encontrarás información con docker-compose, pero al instalar docker ya te vendrá instalado docker compose, por lo que todo lo que se podía hacer con docker-compose es compatible con docker compose
Docker compose
Docker Compose es una herramienta de Docker que hace todo lo que hemos visto hasta ahora, pero ahorrándonos tiempo y esfuerzo. Editando un archivo .yml podemos decirle a Docker Compose que cree todos los contenedores que queramos.
Para usarlo una vez no habrá mucha diferencia de escribir todos los comandos que vimos antes o escribir el archivo .yml, pero cuando quieras volver a tener la misma configuración de contenedores trabajando, solo con llamar al archivo .yml volverá a crear toda la configuración
Vamos a crear una carpeta donde guardaremos los archivos de Docker Compose
InputPython!mkdir dockerComposeFilesCopied
Creamos el archivo .yml dentro
InputPython!touch dockerComposeFiles/docker-compose.ymlCopied
Un archivo Docker Compose tiene que empezar por la versión
version: "<v.v>"En el momento de escribir esto, la última versión es la 3.8 así que escribimos esa
*docker-compose.yml*:
version: "3.8"A continuación se indican los servicios, que son los contenedores. En cada servicio hay que especificar la imagen y, además, se pueden añadir otros parámetros como puertos, variables de entorno, etc.
services:
container1:
image: ubuntu
container2:
image: ubuntuEl docker-compose.yml quedaría así:
version: "3.8"
services:
container1:
image: ubuntu
container2:
image: ubuntuUna vez que hemos creado el archivo, en su path, podemos ejecutar todo mediante el comando docker compose up, pero además, añadiendo la opción -d, haremos que corra en segundo plano
InputPython!cd dockerComposeFiles && docker compose up -dCopied
[+] Running 1/0⠿ Network dockercomposefiles_default Created 0.1s⠋ Container dockercomposefiles-container2-1 Creating 0.0s⠋ Container dockercomposefiles-container1-1 Creating 0.0s[+] Running 1/3⠿ Network dockercomposefiles_default Created 0.1s⠙ Container dockercomposefiles-container2-1 Creating 0.1s⠙ Container dockercomposefiles-container1-1 Creating 0.1s[+] Running 1/3⠿ Network dockercomposefiles_default Created 0.1s⠿ Container dockercomposefiles-container2-1 Starting 0.2s⠿ Container dockercomposefiles-container1-1 Starting 0.2s[+] Running 1/3⠿ Network dockercomposefiles_default Created 0.1s⠿ Container dockercomposefiles-container2-1 Starting 0.3s⠿ Container dockercomposefiles-container1-1 Starting 0.3s[+] Running 1/3⠿ Network dockercomposefiles_default Created 0.1s⠿ Container dockercomposefiles-container2-1 Starting 0.4s⠿ Container dockercomposefiles-container1-1 Starting 0.4s[+] Running 1/3⠿ Network dockercomposefiles_default Created 0.1s⠿ Container dockercomposefiles-container2-1 Starting 0.5s⠿ Container dockercomposefiles-container1-1 Starting 0.5s[+] Running 2/3⠿ Network dockercomposefiles_default Created 0.1s⠿ Container dockercomposefiles-container2-1 Started 0.5s⠿ Container dockercomposefiles-container1-1 Starting 0.6s[+] Running 3/3⠿ Network dockercomposefiles_default Created 0.1s⠿ Container dockercomposefiles-container2-1 Started 0.5s⠿ Container dockercomposefiles-container1-1 Started 0.7s
Si nos fijamos, ha creado dos contenedores dockercomposefiles-container1-1 y dockercomposefiles-container2-1 y la red que los une dockercomposefiles_default
Vamos a borrar los dos contenedores
InputPython!docker rm -f dockercomposefiles-container1-1 dockercomposefiles-container2-1Copied
dockercomposefiles-container1-1dockercomposefiles-container2-1
Y borramos la red que ha creado
InputPython!docker network rm dockercomposefiles_defaultCopied
dockercomposefiles_default
Vamos a intentar hacer lo que hicimos antes con lo que sabemos hasta ahora. Creamos una nueva imagen que venga con ping instalada
*Dockerfile*:
FROM ubuntu:20.04
RUN apt update
RUN apt install iputils-ping -yY la compilamos
InputPython!docker build -t ubuntu:ping ./dockerImagesCopied
Sending build context to Docker daemon 2.048kBStep 1/3 : FROM ubuntu:20.04---> a0ce5a295b63Step 2/3 : RUN apt update---> Running in 3bd5278d39b4WARNING: apt does not have a stable CLI interface. Use with caution in scripts.Get:1 http://security.ubuntu.com/ubuntu focal-security InRelease [114 kB]Get:2 http://archive.ubuntu.com/ubuntu focal InRelease [265 kB]Get:3 http://security.ubuntu.com/ubuntu focal-security/universe amd64 Packages [898 kB]Get:4 http://archive.ubuntu.com/ubuntu focal-updates InRelease [114 kB]Get:5 http://archive.ubuntu.com/ubuntu focal-backports InRelease [108 kB]Get:6 http://archive.ubuntu.com/ubuntu focal/universe amd64 Packages [11.3 MB]Get:7 http://security.ubuntu.com/ubuntu focal-security/main amd64 Packages [2133 kB]Get:8 http://security.ubuntu.com/ubuntu focal-security/multiverse amd64 Packages [27.5 kB]Get:9 http://security.ubuntu.com/ubuntu focal-security/restricted amd64 Packages [1501 kB]Get:10 http://archive.ubuntu.com/ubuntu focal/main amd64 Packages [1275 kB]Get:11 http://archive.ubuntu.com/ubuntu focal/restricted amd64 Packages [33.4 kB]Get:12 http://archive.ubuntu.com/ubuntu focal/multiverse amd64 Packages [177 kB]Get:13 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages [2594 kB]Get:14 http://archive.ubuntu.com/ubuntu focal-updates/restricted amd64 Packages [1613 kB]Get:15 http://archive.ubuntu.com/ubuntu focal-updates/multiverse amd64 Packages [30.2 kB]Get:16 http://archive.ubuntu.com/ubuntu focal-updates/universe amd64 Packages [1200 kB]Get:17 http://archive.ubuntu.com/ubuntu focal-backports/universe amd64 Packages [27.4 kB]...Successfully built c3d32aa9de02Successfully tagged ubuntu:pingUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Comprobamos que se ha creado
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEubuntu ping c3d32aa9de02 About a minute ago 112MBmaximofn/ubuntu test a78cf3ea16d8 25 hours ago 77.8MBnginx latest 2d389e545974 33 hours ago 142MBubuntu latest 2dc39ba059dc 12 days ago 77.8MBubuntu 20.04 a0ce5a295b63 12 days ago 72.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
Le cambiamos el tag
InputPython!docker tag ubuntu:ping maximofn/ubuntu:pingCopied
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEubuntu ping c3d32aa9de02 About a minute ago 112MBmaximofn/ubuntu ping c3d32aa9de02 About a minute ago 112MBmaximofn/ubuntu test c3d32aa9de02 About a minute ago 112MBnginx latest 2d389e545974 33 hours ago 142MBubuntu latest 2dc39ba059dc 12 days ago 77.8MBubuntu 20.04 a0ce5a295b63 12 days ago 72.8MBhello-world latest feb5d9fea6a5 11 months ago 13.3kB
Editamos el archivo Docker Compose para que coja las imágenes que acabamos de crear
*docker-compose.yml*:
version: "3.8"
services:
container1:
image: maximofn/ubuntu:ping
container2:
image: maximofn/ubuntu:pingY además le decimos que ejecute una no operación
El docker-compose.yml quedaría así:
version: "3.8"
services:
container1:
image: ubuntu
command: tail -f /dev/null
container2:
image: ubuntu
command: tail -f /dev/nullLo levantamos
InputPython!cd dockerComposeFiles && docker compose up -dCopied
[+] Running 0/0⠋ Container dockercomposefiles-container1-1 Recreate 0.1s⠋ Container dockercomposefiles-container2-1 Recreate 0.1s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s⠙ Container dockercomposefiles-container2-1 Recreate 0.2s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s⠹ Container dockercomposefiles-container2-1 Recreate 0.3s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s⠸ Container dockercomposefiles-container2-1 Recreate 0.4s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s⠼ Container dockercomposefiles-container2-1 Recreate 0.5s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s⠴ Container dockercomposefiles-container2-1 Recreate 0.6s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s...⠸ Container dockercomposefiles-container2-1 Recreate 1.4s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Recreated 0.1s...[+] Running 2/2⠿ Container dockercomposefiles-container1-1 Started 10.8s⠿ Container dockercomposefiles-container2-1 Started 10.9s[+] Running 2/2⠿ Container dockercomposefiles-container1-1 Started 10.8s⠿ Container dockercomposefiles-container2-1 Started 10.9s
Vemos los contenedores que están corriendo
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES935939e5a75d maximofn/ubuntu:ping "tail -f /dev/null" 15 seconds ago Up 13 seconds dockercomposefiles-container2-1f9138d7064dd maximofn/ubuntu:ping "tail -f /dev/null" 25 seconds ago Up 13 seconds dockercomposefiles-container1-1
Están los dos contenedores corriendo, ahora nos metemos en uno e intentamos hacer ping al otro
$ docker exec -it dockercomposefiles-container1-1 bash
root@f9138d7064dd:/# ping dockercomposefiles-container2-1
PING dockercomposefiles-container2-1 (172.21.0.3) 56(84) bytes of data.
64 bytes from dockercomposefiles-container2-1.dockercomposefiles_default (172.21.0.3): icmp_seq=1 ttl=64 time=0.110 ms
64 bytes from dockercomposefiles-container2-1.dockercomposefiles_default (172.21.0.3): icmp_seq=2 ttl=64 time=0.049 ms
64 bytes from dockercomposefiles-container2-1.dockercomposefiles_default (172.21.0.3): icmp_seq=3 ttl=64 time=0.049 ms
64 bytes from dockercomposefiles-container2-1.dockercomposefiles_default (172.21.0.3): icmp_seq=4 ttl=64 time=0.075 ms
^C
--- dockercomposefiles-container2-1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3068ms
rtt min/avg/max/mdev = 0.049/0.070/0.110/0.025 msComo vemos, podemos hacer ping, hemos creado bien la imagen con ping instalado. Además, en el docker-compose hemos hecho que se ejecute una no operación para que los contenedores estén corriendo
Borramos los dos contenedores y la red que hemos creado
InputPython!docker rm -f dockercomposefiles-container1-1 dockercomposefiles-container2-1Copied
dockercomposefiles-container1-1dockercomposefiles-container2-1
InputPython!docker network rm dockercomposefiles_defaultCopied
dockercomposefiles_default
Cómo nombra Docker Compose los contenedores
Si nos fijamos, los contenedores que crea Docker se llaman dockercomposefiles-container1-1 y dockercomposefiles-container2-1. Esto es porque la carpeta en la que está el archivo de Docker Compose está en una carpeta llamada dockerComposeFiles, por eso la primera parte del nombre de los contenedores es dockercomposefiles, a continuación aparece el nombre del servicio que le hemos dado en el archivo Docker Compose (container1 y container2) y por último un número para poder crear más si es necesario
Similarmente ocurre con el nombre de la red que ha creado dockercomposefiles_default
Logs en docker compose
Vamos ahora a cambiar el archivo Docker Compose, en las líneas en las que teníamos command: tail -f /dev/null, vamos a poner command: ping 0.0.0.0
Y además le decimos que ejecute una no operación
El docker-compose.yml quedaría así:
version: "3.8"
services:
container1:
image: ubuntu
command: ping 0.0.0.0
container2:
image: ubuntu
command: ping 0.0.0.0Esto lo hacemos para que cada contenedor esté escupiendo el ping constantemente, así simulamos algunos logs
Si ejecutamos de nuevo el docker compose
InputPython!cd dockerComposeFiles && docker compose up -dCopied
[+] Running 0/0⠋ Container dockercomposefiles-container1-1 Recreate 0.1s⠋ Container dockercomposefiles-container2-1 Recreate 0.1s[+] Running 0/2⠙ Container dockercomposefiles-container1-1 Recreate 0.2s⠙ Container dockercomposefiles-container2-1 Recreate 0.2s[+] Running 0/2⠹ Container dockercomposefiles-container1-1 Recreate 0.3s⠹ Container dockercomposefiles-container2-1 Recreate 0.3s[+] Running 0/2⠸ Container dockercomposefiles-container1-1 Recreate 0.4s⠸ Container dockercomposefiles-container2-1 Recreate 0.4s[+] Running 0/2⠼ Container dockercomposefiles-container1-1 Recreate 0.5s⠼ Container dockercomposefiles-container2-1 Recreate 0.5s[+] Running 0/2⠴ Container dockercomposefiles-container1-1 Recreate 0.6s⠴ Container dockercomposefiles-container2-1 Recreate 0.6s[+] Running 0/2⠦ Container dockercomposefiles-container1-1 Recreate 0.7s⠦ Container dockercomposefiles-container2-1 Recreate 0.7s[+] Running 0/2⠧ Container dockercomposefiles-container1-1 Recreate 0.8s⠧ Container dockercomposefiles-container2-1 Recreate 0.8s[+] Running 0/2...⠿ Container dockercomposefiles-container1-1 Starting 11.0s⠿ Container dockercomposefiles-container2-1 Started 11.0s[+] Running 2/2⠿ Container dockercomposefiles-container1-1 Started 11.1s⠿ Container dockercomposefiles-container2-1 Started 11.0s
Ahora podemos ver los logs de los dos contenedores mediante el comando docker compose logs
InputPython!cd dockerComposeFiles && docker compose logsCopied
dockercomposefiles-container2-1 | PING 0.0.0.0 (127.0.0.1) 56(84) bytes of data.dockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.042 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.025 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.022 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.030 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.021 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=6 ttl=64 time=0.021 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=7 ttl=64 time=0.030 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=8 ttl=64 time=0.028 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=9 ttl=64 time=0.028 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=10 ttl=64 time=0.026 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=11 ttl=64 time=0.028 msdockercomposefiles-container1-1 | PING 0.0.0.0 (127.0.0.1) 56(84) bytes of data.dockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=12 ttl=64 time=0.027 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=13 ttl=64 time=0.039 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=14 ttl=64 time=0.035 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=15 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.036 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.036 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.032 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.032 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=21 ttl=64 time=0.033 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=22 ttl=64 time=0.034 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.037 ms...dockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=214 ttl=64 time=0.015 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=215 ttl=64 time=0.021 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=216 ttl=64 time=0.020 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=217 ttl=64 time=0.049 ms
Como vemos, podemos ver los logs de los dos contenedores, pero en el caso de querer ver solo los de uno, podemos especificar el **nombre del servicio**
InputPython!cd dockerComposeFiles && docker compose logs container1Copied
dockercomposefiles-container1-1 | PING 0.0.0.0 (127.0.0.1) 56(84) bytes of data.dockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.037 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.025 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.023 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.031 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.034 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=6 ttl=64 time=0.033 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=7 ttl=64 time=0.034 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=8 ttl=64 time=0.022 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=9 ttl=64 time=0.032 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=10 ttl=64 time=0.029 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=11 ttl=64 time=0.031 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=12 ttl=64 time=0.024 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=13 ttl=64 time=0.029 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=14 ttl=64 time=0.032 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=15 ttl=64 time=0.033 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.034 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.028 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.034 ms...dockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=332 ttl=64 time=0.027 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=333 ttl=64 time=0.030 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=334 ttl=64 time=0.033 msdockercomposefiles-container1-1 | 64 bytes from 127.0.0.1: icmp_seq=335 ttl=64 time=0.036 ms
InputPython!cd dockerComposeFiles && docker compose logs container2Copied
dockercomposefiles-container2-1 | PING 0.0.0.0 (127.0.0.1) 56(84) bytes of data.dockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.042 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.025 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.022 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.030 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.021 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=6 ttl=64 time=0.021 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=7 ttl=64 time=0.030 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=8 ttl=64 time=0.028 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=9 ttl=64 time=0.028 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=10 ttl=64 time=0.026 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=11 ttl=64 time=0.028 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=12 ttl=64 time=0.027 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=13 ttl=64 time=0.039 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=14 ttl=64 time=0.035 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=15 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.036 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.036 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.032 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.032 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=21 ttl=64 time=0.033 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=22 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=23 ttl=64 time=0.035 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=24 ttl=64 time=0.037 ms...dockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=340 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=341 ttl=64 time=0.033 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=342 ttl=64 time=0.034 msdockercomposefiles-container2-1 | 64 bytes from 127.0.0.1: icmp_seq=343 ttl=64 time=0.036 ms
Si queremos ver los logs continuamente, podemos añadir la opción -f: docker compose logs -f <service name>
Si he hecho un docker compose con más de dos servicios, cuando se quiera ver los logs de varios servicios solo hay que añadir más nombres al comando, docker compose logs <name service 1> <name service 2> ...
Exec servicios
Como hemos visto, mediante el comando exec podemos entrar a un contenedor indicando el nombre del contenedor, el comando que se quiere ejecutar y la opción -it. Con Docker Compose esto es más sencillo, ya que solamente es necesario el nombre del servicio y el comando, pero no es necesaria la opción -it ya que Docker Compose lo da por sentado
$ docker compose exec container1 bash
root@a7cf282fe66c:/#Deteniendo docker compose
Cuando hemos terminado de trabajar, con un solo comando (stop), Docker Compose para todo, no hace falta ir parando uno a uno cada contenedor
InputPython!cd dockerComposeFiles && docker compose stopCopied
[+] Running 0/0⠋ Container dockercomposefiles-container2-1 Stopping 0.1s⠋ Container dockercomposefiles-container1-1 Stopping 0.1s[+] Running 0/2⠙ Container dockercomposefiles-container2-1 Stopping 0.2s⠙ Container dockercomposefiles-container1-1 Stopping 0.2s[+] Running 0/2⠹ Container dockercomposefiles-container2-1 Stopping 0.3s⠹ Container dockercomposefiles-container1-1 Stopping 0.3s[+] Running 0/2⠸ Container dockercomposefiles-container2-1 Stopping 0.4s⠸ Container dockercomposefiles-container1-1 Stopping 0.4s[+] Running 0/2⠼ Container dockercomposefiles-container2-1 Stopping 0.5s⠼ Container dockercomposefiles-container1-1 Stopping 0.5s[+] Running 0/2⠴ Container dockercomposefiles-container2-1 Stopping 0.6s⠴ Container dockercomposefiles-container1-1 Stopping 0.6s[+] Running 0/2⠦ Container dockercomposefiles-container2-1 Stopping 0.7s⠦ Container dockercomposefiles-container1-1 Stopping 0.7s[+] Running 0/2⠧ Container dockercomposefiles-container2-1 Stopping 0.8s⠧ Container dockercomposefiles-container1-1 Stopping 0.8s...[+] Running 1/2⠿ Container dockercomposefiles-container2-1 Stopped 10.4s⠸ Container dockercomposefiles-container1-1 Stopping 10.4s[+] Running 2/2⠿ Container dockercomposefiles-container2-1 Stopped 10.4s⠿ Container dockercomposefiles-container1-1 Stopped 10.4s
Como se puede ver, docker compose ha parado los dos contenedores, pero no los ha borrado, ni ha borrado la red
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1e6c1dd9adb2 maximofn/ubuntu:ping "ping 0.0.0.0" 16 minutes ago Exited (137) 25 seconds ago dockercomposefiles-container2-1a7cf282fe66c maximofn/ubuntu:ping "ping 0.0.0.0" 16 minutes ago Exited (137) 25 seconds ago dockercomposefiles-container1-1
InputPython!docker network lsCopied
NETWORK ID NAME DRIVER SCOPE13cc632147f3 bridge bridge locald4a2f718cd83 dockercomposefiles_default bridge localda1f5f6fccc0 host host locald3b0d93993c0 none null local
Docker compose como herramienta de desarrollo
Al igual que vimos antes, para poder desarrollar, lo ideal sería compartir la carpeta que tiene el código con el servicio. Esto con docker compose se hace añadiendo la etiqueta volumes al archivo docker compose. Primero tenemos que añadirle la ruta de la carpeta donde está el código en el host y luego la ruta en el contenedor.
*docker-compose.yml*:
version: "3.8"
services:
container1:
image: ubuntu
command: ping 0.0.0.0
volumes:
- ../dockerHostFolder/:/dockerContainerFolder
container2:
image: ubuntu
command: ping 0.0.0.0Cómo se puede ver, la ruta de la carpeta del host la he puesto relativa
Si levantamos el Docker Compose
InputPython!cd dockerComposeFiles && docker compose up -dCopied
[+] Running 1/0⠋ Container dockercomposefiles-container1-1 Recreate 0.1s⠿ Container dockercomposefiles-container2-1 Created 0.0s[+] Running 0/2⠿ Container dockercomposefiles-container1-1 Starting 0.2s⠿ Container dockercomposefiles-container2-1 Starting 0.2s[+] Running 0/2⠿ Container dockercomposefiles-container1-1 Starting 0.3s⠿ Container dockercomposefiles-container2-1 Starting 0.3s[+] Running 0/2⠿ Container dockercomposefiles-container1-1 Starting 0.4s⠿ Container dockercomposefiles-container2-1 Starting 0.4s[+] Running 1/2⠿ Container dockercomposefiles-container1-1 Started 0.5s⠿ Container dockercomposefiles-container2-1 Starting 0.5s[+] Running 2/2⠿ Container dockercomposefiles-container1-1 Started 0.5s⠿ Container dockercomposefiles-container2-1 Started 0.6s
Si entramos en el contenedor, podemos ver qué hay dentro del archivo text.txt
$ docker compose exec container1 bash
root@c8aae9d619d3:/# ls dockerContainerFolder/
bindFile.txt fileExtract.txt text.txt
root@c8aae9d619d3:/# cat dockerContainerFolder/text.txt
hola contenedorSi ahora lo abrimos en el host, escribimos hola host y volvemos a ver en el contenedor
root@c8aae9d619d3:/# cat dockerContainerFolder/text.txt
hola hostY ahora al revés, si lo modificamos en el contenedor
root@c8aae9d619d3:/# echo hola compose > dockerContainerFolder/text.txt
root@c8aae9d619d3:/# exit
exitSi lo vemos desde el host, debemos obtener hola compose
InputPython!cat dockerHostFolder/text.txtCopied
hola compose
Exposición de puertos en docker compose
También podemos configurar los puertos en el archivo de Docker Compose, mediante la etiqueta ports, indicando el puerto del host y a continuación la IP del servicio
ports:
- <host port>:<service port>Docker compose en equipo - docker override
Si somos un grupo de personas desarrollando sobre Docker con Docker Compose, es probable que muchas personas anden cambiando el archivo Docker Compose, lo cual puede hacer que no se sincronicen bien y haya conflictos.
Para solucionar esto Docker ofrece una herramienta llamada Docker Override. De esta manera puede haber un archivo Docker Compose base y que cada uno lo modifique mediante Docker Override.
Para hacer esto, ahora tenemos que crear un archivo llamado docker-compose.override.yml que será el que podremos editar
InputPython!touch dockerComposeFiles/docker-compose.override.ymlCopied
Si ahora intentamos levantar el Docker Compose, vamos a recibir un error
InputPython!cd dockerComposeFiles && docker compose up -dCopied
Top-level object must be a mapping
Y esto es porque Docker Compose ha detectado que hay un archivo llamado docker-compose.override.yml y que está vacío, por lo que vamos a editarlo. El archivo docker-compose.override.yml lo que hace es editar el archivo docker-compose.yml, por lo que si por ejemplo queremos hacer un cambio en el servicio container2 para añadirle un volumen escribiríamos así el archivo docker-compose.override.yml
*docker-compose.override.yml*:
version: "3.8"
services:
container2:
volumes:
- ../dockerHostFolder/:/dockerOverrideFolderDate cuenta de que la carpeta compartida en el servicio la he llamado dockerOverrideFolder, por lo que vamos a levantar el docker compose y ver si vemos esa carpeta en el contenedor container2
InputPython!cd dockerComposeFiles && docker compose up -dCopied
[+] Running 1/0⠋ Container dockercomposefiles-container2-1 Recreate 0.1s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠙ Container dockercomposefiles-container2-1 Recreate 0.2s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠹ Container dockercomposefiles-container2-1 Recreate 0.3s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠸ Container dockercomposefiles-container2-1 Recreate 0.4s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠼ Container dockercomposefiles-container2-1 Recreate 0.5s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠴ Container dockercomposefiles-container2-1 Recreate 0.6s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠦ Container dockercomposefiles-container2-1 Recreate 0.7s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 1/2⠧ Container dockercomposefiles-container2-1 Recreate 0.8s⠿ Container dockercomposefiles-container1-1 Running 0.0s...[+] Running 1/2⠿ Container dockercomposefiles-container2-1 Starting 10.8s⠿ Container dockercomposefiles-container1-1 Running 0.0s[+] Running 2/2⠿ Container dockercomposefiles-container2-1 Started 10.8s⠿ Container dockercomposefiles-container1-1 Running 0.0s
Vemos que ha tardado 10 segundos en montar el servicio container2, eso es porque ha estado aplicando los cambios
$ docker compose exec container2 bash
root@d8777a4e611a:/# ls dockerOverrideFolder/
bindFile.txt fileExtract.txt text.txt
root@d8777a4e611a:/# cat dockerOverrideFolder/text.txt
hola compose
root@d8777a4e611a:/# exitBajamos el Compose y borramos los contenedores y la red creada
InputPython!cd dockerComposeFiles && docker compose downCopied
[+] Running 0/0⠋ Container dockercomposefiles-container2-1 Stopping 0.1s⠋ Container dockercomposefiles-container1-1 Stopping 0.1s[+] Running 0/2⠙ Container dockercomposefiles-container2-1 Stopping 0.2s⠙ Container dockercomposefiles-container1-1 Stopping 0.2s[+] Running 0/2⠹ Container dockercomposefiles-container2-1 Stopping 0.3s⠹ Container dockercomposefiles-container1-1 Stopping 0.3s[+] Running 0/2⠸ Container dockercomposefiles-container2-1 Stopping 0.4s⠸ Container dockercomposefiles-container1-1 Stopping 0.4s[+] Running 0/2⠼ Container dockercomposefiles-container2-1 Stopping 0.5s⠼ Container dockercomposefiles-container1-1 Stopping 0.5s[+] Running 0/2⠴ Container dockercomposefiles-container2-1 Stopping 0.6s⠴ Container dockercomposefiles-container1-1 Stopping 0.6s[+] Running 0/2⠦ Container dockercomposefiles-container2-1 Stopping 0.7s⠦ Container dockercomposefiles-container1-1 Stopping 0.7s[+] Running 0/2⠧ Container dockercomposefiles-container2-1 Stopping 0.8s⠧ Container dockercomposefiles-container1-1 Stopping 0.8s...⠸ Container dockercomposefiles-container2-1 Stopping 10.4s⠸ Container dockercomposefiles-container1-1 Stopping 10.4s[+] Running 1/2⠿ Container dockercomposefiles-container2-1 Removed 10.4s⠿ Container dockercomposefiles-container1-1 Removing 10.5s[+] Running 2/2⠿ Container dockercomposefiles-container2-1 Removed 10.4s⠿ Container dockercomposefiles-container1-1 Removed 10.5s⠋ Network dockercomposefiles_default Removing 0.1s[+] Running 3/3⠿ Container dockercomposefiles-container2-1 Removed 10.4s⠿ Container dockercomposefiles-container1-1 Removed 10.5s⠿ Network dockercomposefiles_default Removed 0.2s
En este caso, solo con down Docker Compose ha parado y borrado todo, ya que, como vemos en los contenedores y en la red pone Removed
Docker compose restart
A la hora de escribir un docker compose, podemos añadir la etiqueta restart para que si el contenedor se cae, se reinicie automáticamente
restart: alwaysDe esta manera, si el contenedor se cae, se reiniciará automáticamente. Si queremos que se reinicie solo un número de veces, podemos añadirle la opción on-failure
restart: on-failure:<number>Ahora el contenedor se reiniciará un número de veces, pero si se cae más veces, no se reiniciará. Si queremos que se reinicie siempre, podemos añadirle la opción unless-stopped
restart: unless-stoppedAhora el contenedor se reiniciará siempre, a menos que se pare manualmente
Docker avanzado
Administrar ambiente de trabajo
Borrado de contenedores apagados
Después de estar un tiempo desarrollando, podemos tener varios contenedores apagados, pero guardados en el ordenador. Esto al final ocupa memoria, así que con docker container prune podemos eliminar todos los que están parados
InputPython!docker run ubuntuCopied
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESeffcee24f54a ubuntu "bash" 37 seconds ago Exited (0) 36 seconds ago musing_rosalind
$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
effcee24f54aab22e34fdea2465b3b7af132d8c627e5432ba3e915a370876977
Total reclaimed space: 0BEn este caso hemos ahorrado 0 bytes, pero en el caso de dejar contenedores apagados después de mucho desarrollo, seguro que el ahorro de memoria será mayor
Borrado de todos los contenedores
En caso de tener contenedores corriendo, podemos eliminar todos los contenedores mediante otro comando
El comando docker ps -q nos devuelve la ID de todos los contenedores, por lo que con el comando docker rm -f $(docker ps -aq) pararemos y borraremos todos
InputPython!docker run -d ubuntu tail -f /dev/nullCopied
c22516186ef7e3561fb1ad0d508a914857dbc61274a218f297c4d80b1fc33863
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESc22516186ef7 ubuntu "tail -f /dev/null" About a minute ago Up About a minute agitated_knuth
InputPython!docker rm -f $(docker ps -aq)Copied
c22516186ef7
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Borrado de todo
Como hemos visto Docker también crea redes, imágenes, volúmenes, etc, así que con el comando docker system prune podemos borrar todos los contenedores parados, todas las redes que no estén usadas por al menos un contenedor, las imágenes repetidas y lo que haya repetido en la caché de compilación
$ docker system prune
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
- all dangling build cache
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0BAl igual que antes, no se ha ahorrado mucho espacio, pero después de mucho tiempo desarrollando, el ahorro será considerable
Uso de recursos del host por parte de contenedores
Por ejemplo, a la hora de crear un contenedor, podemos limitar la RAM que el host puede usar mediante la opción --memory
InputPython!docker run -d --memory 1g ubuntu tail -f /dev/nullCopied
d84888eafe531831ef8915d2270422365adec02678122bf59580e2da782e6972
Pero con docker ps no tenemos acceso a los recursos que está consumiendo el contenedor
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESd84888eafe53 ubuntu "tail -f /dev/null" 35 seconds ago Up 34 seconds musing_ritchie
Para ello tenemos el comando docker stats
$ docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
d84888eafe53 musing_ritchie 0.00% 540KiB / 1GiB 0.05% 5.62kB / 0B 0B / 0B 1Esto es muy útil si queremos simular un entorno con un límite de RAM
Deteniendo contenedores correctamente: SHELL vs EXEC
Como hemos explicado, cuando asignamos un proceso a un contenedor, cuando ese proceso termina, el contenedor se para, pero a veces podemos encontrarnos con problemas con esto. Vamos a crear una nueva carpeta llamada Dockerfile_loop
InputPython!mkdir Dockerfile_loopCopied
Ahora vamos a crear un archivo llamado loop.sh dentro de Dockerfile_loop
InputPython!touch Dockerfile_loop/loop.shCopied
Y vamos a escribir lo siguiente dentro de loop.sh
#!/usr/bin/env bash
trap "exit 0" SIGTERM
while true; do :; doneSi yo ejecuto este script en el host se ejecuta hasta que introduzca CTRL+C
``` bash
./loop
^C
```
Ahora vamos a crear un archivo Dockerfile dentro de Dockerfile_loop
InputPython!touch Dockerfile_loop/DockerfileCopied
*Dockerfile*:
FROM ubuntu:trusty
COPY ["loop.sh", "/"]
CMD /loop.shVamos a crear una imagen basada en Ubuntu que copia el script dentro y lo ejecuta, y el script se ejecuta hasta que recibe la señal SIGTERM del sistema operativo. Compilamos la imagen
InputPython!docker build -t ubuntu:loop ./Dockerfile_loopCopied
Sending build context to Docker daemon 3.072kBStep 1/3 : FROM ubuntu:trusty---> 13b66b487594Step 2/3 : COPY ["loop.sh", "/"]---> 89f2bbd25a88Step 3/3 : CMD /loop.sh---> Running in ff52569c35fdRemoving intermediate container ff52569c35fd---> feb091e4efa3Successfully built feb091e4efa3Successfully tagged ubuntu:loopUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Ejecutamos el contenedor
docker run -d --name looper ubuntu:loop bashInputPython!docker run -d --name looper ubuntu:loopCopied
8a28f8cc9892213c4e0603dfdde320edf52c091b82c60510083549a391cd6645
Comprobamos y vemos que el contenedor está corriendo
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES8a28f8cc9892 ubuntu:loop "/bin/sh -c /loop.sh" 4 seconds ago Up 3 seconds looper
Intentamos parar el contenedor con docker stop looper. Docker stop trata de parar el contenedor enviándole la señal SIGTERM.
InputPython%%time!docker stop looperCopied
looperCPU times: user 89.2 ms, sys: 21.7 ms, total: 111 msWall time: 10.6 s
Esto ha tardado unos 10 segundos en detenerse, cuando tendría que ser inmediato. Esto es porque stop ha mandado la orden SIGTERM para que se parara el contenedor, pero como no paraba, al rato le ha mandado un SIGKILL para forzar que se detenga. Veamos a ver qué pasa, si listamos los contenedores
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES8a28f8cc9892 ubuntu:loop "/bin/sh -c /loop.sh" 23 seconds ago Exited (137) 2 seconds ago looper
Podemos ver que la señal de Exited es 137, eso equivale a SIGKILL, es decir, Docker tuvo que forzar el apagado.
Vamos a borrar el contenedor y a volver a ejecutarlo
InputPython!docker rm looperCopied
looper
InputPython!docker run -d --name looper ubuntu:loopCopied
84bc37f944d270be5f84a952968db2b8cf5372c61146d29383468198ceed18fd
Si ahora intentamos detener el contenedor con docker kill looper
InputPython%%time!docker kill looperCopied
looperCPU times: user 9.1 ms, sys: 857 µs, total: 9.96 msWall time: 545 ms
Vemos que el tiempo son unos 500 ms, es decir, docker lo ha parado en un momento enviándole la orden SIGKILL. Porque kill no manda SIGTERM y si en un tiempo no se ha parado el contenedor manda SIGKILL, lo que hace es mandar SIGKILL desde el inicio.
Si vemos los contenedores, vemos que la señal de salida es la misma, 137
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES84bc37f944d2 ubuntu:loop "/bin/sh -c /loop.sh" 6 seconds ago Exited (137) 2 seconds ago looper
Esta no es la manera correcta de apagar un contenedor, porque cuando queramos apagar el contenedor habría que hacerlo mediante la señal SIGTERM, para que este termine de procesar lo que estuviera haciendo y luego se apague
Si borramos el contenedor y lo volvemos a ejecutar
InputPython!docker rm looperCopied
looper
InputPython!docker run -d --name looper ubuntu:loopCopied
b9d9f370cc0de7569eb09d0a85cd67e8ea6babc0754a517ccba5c5057f5cc50e
Si ahora vemos los procesos que se están ejecutando dentro del contenedor
InputPython!docker exec looper ps -efCopied
UID PID PPID C STIME TTY TIME CMDroot 1 0 0 14:05 ? 00:00:00 /bin/sh -c /loop.shroot 7 1 93 14:05 ? 00:00:02 bash /loop.shroot 8 0 0 14:05 ? 00:00:00 ps -ef
En realidad el proceso principal, el 1, no es /loop.sh sino que es /bin/sh -c /loop.sh, es decir, es un proceso hijo del shell. Por lo que cuando llegaba la señal SIGTERM le llegaba al shell, pero este no se la manda a sus procesos hijos, por eso no le llegaba a loop.sh
Para que no pase esto hay que cambiar el Dockerfile a lo siguiente
*Dockerfile*:
FROM ubuntu:trusty
COPY ["loop.sh", "/"]
CMD ["/loop.sh"] # antes era CMD /loop.shEsta forma se llama exec form, mientras que la anterior se llama shell form, de manera que de la anterior forma corre el proceso como un hijo del shell, mientras que de la forma exec form ejecuta el proceso que le digamos. Así que borramos el contenedor, volvemos a compilar y volvemos a hacer correr el contenedor con la imagen
InputPython!docker rm -f looperCopied
looper
InputPython!docker build -t ubuntu:loop ./Dockerfile_loopCopied
Sending build context to Docker daemon 3.072kBStep 1/3 : FROM ubuntu:trusty---> 13b66b487594Step 2/3 : COPY ["loop.sh", "/"]---> Using cache---> 89f2bbd25a88Step 3/3 : CMD ["/loop.sh"]---> Running in 6b8d92fcd57cRemoving intermediate container 6b8d92fcd57c---> 35a7bb2b1892Successfully built 35a7bb2b1892Successfully tagged ubuntu:loopUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
InputPython!docker run -d --name looper ubuntu:loopCopied
850ae70c071426850b28428ac60dcbf875c6d35d9b7cc66c17cf391a23392965
Sí, ahora veo los procesos dentro del contenedor
InputPython!docker exec looper ps -efCopied
UID PID PPID C STIME TTY TIME CMDroot 1 0 88 14:14 ? 00:00:02 bash /loop.shroot 7 0 0 14:14 ? 00:00:00 ps -ef
Ahora sí veo que el proceso principal, el 1, es /loop.sh
Si ahora pruebo a parar el contenedor
InputPython%%time!docker stop looperCopied
looperCPU times: user 989 µs, sys: 7.55 ms, total: 8.54 msWall time: 529 ms
Vemos que tarda más. Veamos el código con el que paró
InputPython!docker ps -aCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES850ae70c0714 ubuntu:loop "/loop.sh" About a minute ago Exited (0) 33 seconds ago looper
Contenedores ejecutables
Si queremos un binario que corra como un ejecutable, en el dockerfile hay que especificar el comando en ENTRYPOINT y los parámetros del comando en CMD, vamos a verlo
Vamos a crear una nueva carpeta donde guardaremos el Dockerfile
InputPython!mkdir dockerfile_pingCopied
Ahora creamos un Dockerfile dentro
InputPython!touch dockerfile_ping/DockerfileCopied
Escribimos dentro del Dockerfile lo siguiente
FROM ubuntu:trusty
ENTRYPOINT [ "/bin/ping", "-c", "3" ]
CMD [ "localhost" ]Compilamos la imagen
InputPython!docker build -t ubuntu:ping ./dockerfile_pingCopied
Sending build context to Docker daemon 3.072kBStep 1/3 : FROM ubuntu:trusty---> 13b66b487594Step 2/3 : ENTRYPOINT [ "/bin/ping", "-c", "3" ]---> Using cache---> 1cebcfb542b1Step 3/3 : CMD [ "localhost" ]---> Using cache---> 04ddc3de52a2Successfully built 04ddc3de52a2Successfully tagged ubuntu:pingUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Si ahora corremos la imagen sin pasarle un parámetro, el contenedor se hará un ping a sí mismo
InputPython!docker run --name ping_localhost ubuntu:pingCopied
PING localhost (127.0.0.1) 56(84) bytes of data.64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.041 ms64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.058 ms64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.054 ms--- localhost ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 2027msrtt min/avg/max/mdev = 0.041/0.051/0.058/0.007 ms
Pero si ahora le pasamos un parámetro, hará ping a la dirección que le digamos
InputPython!docker run --name ping_google ubuntu:ping google.comCopied
PING google.com (216.58.209.78) 56(84) bytes of data.64 bytes from waw02s06-in-f14.1e100.net (216.58.209.78): icmp_seq=1 ttl=111 time=3.93 ms64 bytes from waw02s06-in-f14.1e100.net (216.58.209.78): icmp_seq=2 ttl=111 time=6.80 ms64 bytes from waw02s06-in-f14.1e100.net (216.58.209.78): icmp_seq=3 ttl=111 time=6.92 ms--- google.com ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 2002msrtt min/avg/max/mdev = 3.930/5.886/6.920/1.383 ms
Borramos los contenedores
InputPython!docker rm ping_localhost ping_googleCopied
ping_localhostping_google
El contexto de build
Vamos a crear una carpeta llamada dockerfile_contexto
InputPython!mkdir dokerfile_contextoCopied
Ahora creamos en ella dos archivos: un test.txt y el Dockerfile
InputPython!touch dokerfile_contexto/Dockerfile dokerfile_contexto/text.txtCopied
Modificamos el Dockerfile y ponemos lo siguiente
FROM ubuntu:trusty
COPY [".", "/"]Esto lo que va a hacer es que va a copiar dentro de la imagen todo lo que tenga en la carpeta en la que se encuentra el Dockerfile. Compilamos la imagen
InputPython!docker build -t ubuntu:contexto ./dokerfile_contextoCopied
Sending build context to Docker daemon 2.56kBStep 1/2 : FROM ubuntu:trusty---> 13b66b487594Step 2/2 : COPY [".", "/"]---> 3ab79fdce389Successfully built 3ab79fdce389Successfully tagged ubuntu:contextoUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Vamos a ver qué hay dentro del contenedor
InputPython!docker run --name ls ubuntu:contexto lsCopied
Dockerfilebinbootdevetchomeliblib64mediamntoptprocrootrunsbinsrvsystext.txttmpusrvar
Como vemos, está el archivo text.txt. Pero es posible que dentro de la carpeta que está en el mismo directorio que el Dockerfile haya archivos o carpetas que no queremos que se copien en la imagen, por la razón que sea, así que, al igual que en git tenemos el .gitignore, en docker tenemos el .dockerignore, donde metemos los archivos o carpetas que no queremos que, a la hora de compilar, se tengan en cuenta
Así que creamos un archivo .dockerignore
InputPython!touch dokerfile_contexto/.dockerignoreCopied
Y dentro añadimos el text.txt, y de paso el Dockerfile, que no lo necesitamos dentro de la imagen
*.dockerignore*:
Dockerfile
text.txtBorramos el contenedor que habíamos creado, volvemos a compilar y vemos qué hay dentro del contenedor
InputPython!docker rm lsCopied
ls
InputPython!docker build -t ubuntu:contexto ./dokerfile_contextoCopied
Sending build context to Docker daemon 3.072kBStep 1/2 : FROM ubuntu:trusty---> 13b66b487594Step 2/2 : COPY [".", "/"]---> 7a6689546da4Successfully built 7a6689546da4Successfully tagged ubuntu:contextoUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
InputPython!docker run --name ls ubuntu:contexto lsCopied
binbootdevetchomeliblib64mediamntoptprocrootrunsbinsrvsystmpusrvar
Vemos que ahora no están ni Dockerfile ni text.txt. Borramos el contenedor
InputPython!docker rm lsCopied
ls
Multi-stage build
Al final de un desarrollo no queremos que todo el código esté en la imagen que se va a mandar a producción.
Podemos dividir el dockerfile en dos, por ejemplo, el developer.Dockerfile y el production.Dockerfile, donde en desarrollo habrá más cosas que en el de producción. A la hora de compilarlos, mediante la opción -f elegimos el dockerfile que queremos usar
docker build -t <tag> -f developer.Dockerfile
docker build -t <tag> -f production.DockerfilePero para no tener que crear dos archivos Dockerfile, Docker creó el multi-stage builds. Con un solo Dockerfile vamos a solucionar el problema
Creamos la carpeta donde vamos a guardar el Dockerfile
InputPython!mkdir docker_multi_stageCopied
Y dentro creamos el archivo Dockerfile
InputPython!cd docker_multi_stage && touch DockerfileCopied
Editamos el archivo, metiendo lo siguiente
# Etapa 1: Generar el ejecutable con Python basado en Alpine
FROM python:3.9-alpine as build-stage
WORKDIR /app
# Instalar dependencias para PyInstaller
RUN apk add --no-cache gcc musl-dev libc-dev
# Generar hello.py
RUN echo 'print("Hello from Alpine!")' > hello.py
# Instalar PyInstaller
RUN pip install pyinstaller
# Usar PyInstaller para crear un ejecutable independiente
RUN pyinstaller --onefile hello.py
# Etapa 2: Ejecutar el ejecutable en una imagen de Alpine
FROM alpine:latest
WORKDIR /app
# Copiar el ejecutable desde la etapa de build
COPY --from=build-stage /app/dist/hello .
# Comando por defecto para ejecutar el ejecutable
CMD ["./hello"]Como se puede ver, el Dockerfile está dividido en dos. Por un lado, se trabaja sobre la imagen python:3.9-alpine, que se llama build-stage. Y, por otro lado, trabajamos sobre la imagen alpine:latest, que es una imagen de Linux muy ligera y se utiliza mucho en producción
Lo compilamos
InputPython!docker build -t maximofn/multistagebuild:latest ./docker_multi_stageCopied
[+] Building 0.0s (0/2) docker:default
[+] Building 0.2s (4/6) docker:default=> [internal] load build definition from Dockerfile 0.0s=> => transferring dockerfile: 722B 0.0s=> [internal] load .dockerignore 0.0s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/alpine:latest 0.1s=> [internal] load metadata for docker.io/library/python:3.9-alpine 0.1s...=> CACHED [stage-1 3/3] COPY --from=build-stage /app/dist/hello . 0.0s=> exporting to image 0.0s=> => exporting layers 0.0s=> => writing image sha256:7fb090d1495d00e892118b6bc3c03400b63a435fd4703 0.0s=> => naming to docker.io/maximofn/multistagebuild:latest 0.0s
Si ahora vemos las imágenes que tenemos
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEmaximofn/multistagebuild latest 7fb090d1495d 8 minutes ago 13.6MB
Vamos a bajarnos la imagen de Python para ver cuánto pesa
InputPython!docker pull python:3.9-alpineCopied
3.9-alpine: Pulling from library/pythona8db6415: Already existsd5e70e42: Already exists3fe96417: Already existsaa4dddbb: Already exists518be9f7: Already exists Digest: sha256:6e508b43604ff9a81907ec17405c9ad5c13664e45a5affa2206af128818c7486Status: Downloaded newer image for python:3.9-alpinedocker.io/library/python:3.9-alpine
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEmaximofn/multistagebuild latest 7fb090d1495d 9 minutes ago 13.6MBpython 3.9-alpine 6946662f018b 9 days ago 47.8MB
Podemos ver que mientras nuestra imagen pesa solo 13.6 MB, la de Python con la que ha construido la aplicación pesa 47.8 MB. Por lo que podemos sacar dos conclusiones: con la primera imagen, la de Python, ha construido la aplicación, ha generado el ejecutable y ese ejecutable es el que usamos en la segunda imagen, la de Alpine. Además podemos ver que aunque la primera imagen que usa es la de Python no se descarga en nuestro sistema, ya que la hemos tenido que descargar nosotros
Pues ya sólo queda probarlo
InputPython!docker run --rm --name multi_stage_build maximofn/multistagebuildCopied
Hello from Alpine!
Funciona!
Multi arch builds
Supongamos que queremos hacer una imagen que se pueda ejecutar en un ordenador y en una Raspberry. El ordenador probablemente tenga un micro con arquitectura AMD64, mientras que la Raspberry tiene un micro con arquitectura ARM. Por lo tanto, no podemos crear la misma imagen para los dos. Es decir, cuando creamos una imagen, la creamos con un Dockerfile que suele empezar así
FROM ...Por lo tanto, el Dockerfile de la imagen del ordenador podría empezar así
FROM ubuntu:latestMientras que el de la Raspberry podría empezar así
FROM arm64v8/ubuntu:latestTendríamos que crear dos archivos Dockerfile, compilarlos y en el ordenador usar una imagen y en la Raspberry usar otra
Para evitar tener que ver la arquitectura del ordenador y ver qué imagen tenemos que usar, Docker crea los manifest, que, como su nombre indica, es un manifiesto que indica en función de qué arquitectura de micro tengamos, usa una imagen u otra
Así que vamos a ver cómo hacer esto
En primer lugar, creamos una carpeta donde vamos a crear nuestros archivos Dockerfile
InputPython!mkdir docker_multi_archCopied
Ahora creamos los dos Dockerfiles
InputPython!cd docker_multi_arch && touch Dockerfile_arm64 Dockerfile_amd64Copied
Escribimos el Dockerfile para AMD64
InputPython!cd docker_multi_arch && echo "FROM ubuntu:20.04" >> Dockerfile_amd64 && echo "CMD echo 'Hello from amd64'" >> Dockerfile_amd64Copied
InputPython!cd docker_multi_arch && echo "FROM arm64v8/ubuntu:latest" >> Dockerfile_arm && echo "CMD echo 'Hello from ARM'" >> Dockerfile_armCopied
Ahora combinamos las dos imágenes
InputPython!cd docker_multi_arch && docker build -t maximofn/multiarch:arm -f Dockerfile_arm .Copied
[+] Building 0.0s (0/1) docker:default[+] Building 0.2s (2/3) docker:default=> [internal] load build definition from Dockerfile_amd64 0.1s=> => transferring dockerfile: 89B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.1s[+] Building 0.3s (2/3) docker:default=> [internal] load build definition from Dockerfile_amd64 0.1s=> => transferring dockerfile: 89B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.2s[+] Building 0.5s (2/3) docker:default=> [internal] load build definition from Dockerfile_amd64 0.1s=> => transferring dockerfile: 89B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.4s[+] Building 0.6s (2/3) docker:default=> [internal] load build definition from Dockerfile_amd64 0.1s=> => transferring dockerfile: 89B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/ubuntu:20.04 0.5s...=> => transferring context: 2B 0.0s=> [internal] load build definition from Dockerfile_arm 0.0s=> => transferring dockerfile: 94B 0.0s=> [internal] load metadata for docker.io/arm64v8/ubuntu:latest 1.8s=> [auth] arm64v8/ubuntu:pull token for registry-1.docker.io 0.0s=> CACHED [1/1] FROM docker.io/arm64v8/ubuntu:latest@sha256:94d12db896d0 0.0s=> exporting to image 0.0s=> => exporting layers 0.0s=> => writing image sha256:a9732c1988756dc8e836fd96e5c9512e349c97ea5af46 0.0s=> => naming to docker.io/maximofn/multiarch:arm 0.0s
Vamos a ver qué tenemos las dos imágenes compiladas
InputPython!docker image lsCopied
REPOSITORY TAG IMAGE ID CREATED SIZEmaximofn/multiarch arm a9732c198875 4 weeks ago 69.2MBmaximofn/multiarch amd64 5b612c83025f 6 weeks ago 72.8MB
Vemos que hemos compilado las dos imágenes. Para poder crear un manifest, primero tenemos que subir las imágenes a Docker Hub, así que las subimos
InputPython!docker push maximofn/multiarch:amd64Copied
The push refers to repository [docker.io/maximofn/multiarch]82bdeb5f: Mounted from library/ubuntu amd64: digest: sha256:30e820f2a11a24ad4d8fb624ae485f7c1bcc299e8cfc72c88adce1acd0447e1d size: 529
InputPython!docker push maximofn/multiarch:armCopied
The push refers to repository [docker.io/maximofn/multiarch]
eda53374: Layer already exists arm: digest: sha256:6ec5a0752d49d3805061314147761bf25b5ff7430ce143adf34b70d4eda15fb8 size: 529
Si me voy a mi Docker Hub puedo ver que mi imagen maximofn/multiarch tiene los tags amd64 y arm
Ahora vamos a crear el manifest basado en estas dos imágenes
InputPython!docker manifest create maximofn/multiarch:latest maximofn/multiarch:amd64 maximofn/multiarch:armCopied
Created manifest list docker.io/maximofn/multiarch:latest
Una vez creado, tenemos que indicar las arquitecturas de las CPU a las que corresponde cada una
InputPython!docker manifest annotate maximofn/multiarch:latest maximofn/multiarch:amd64 --os linux --arch amd64Copied
InputPython!docker manifest annotate maximofn/multiarch:latest maximofn/multiarch:arm64 --os linux --arch arm64Copied
manifest for image maximofn/multiarch:arm64 does not exist in maximofn/multiarch:latest
Una vez creado y anotado, podemos subir el manifest a Docker Hub
InputPython!docker manifest push maximofn/multiarch:latestCopied
sha256:1ea28e9a04867fe0e0d8b0efa455ce8e4e29e7d9fd4531412b75dbd0325e9304
Si ahora vuelvo a mirar los tags que tiene mi imagen maximofn/multiarch, veo también el de latest
Ahora, tanto si quiero usar mi imagen desde una máquina con CPU AMD64 o CPU ARM al hacer FROM maximofn/multiarch:latest, docker comprobará la arquitectura de la CPU y bajará el tag amd64 o el tag arm. Vamos a verlo, si desde mi ordenador ejecuto la imagen obtengo
InputPython!docker run maximofn/multiarch:latestCopied
Unable to find image 'maximofn/multiarch:latest' locally
latest: Pulling from maximofn/multiarchDigest: sha256:7cef0de10f7fa2b3b0dca0fbf398d1f48af17a0bbc5b9beca701d7c427c9fd84Status: Downloaded newer image for maximofn/multiarch:latestHello from amd64
Como no la tiene, se la baja
Si ahora me conecto por ssh a una Raspberry Pi y pruebo lo mismo, obtengo
raspberry@raspberrypi:~ $ docker run maximofn/multiarch:latest Unable to find image 'maximofn/multiarch:latest' locally
latest: Pulling from maximofn/multiarch
Digest: sha256:1ea28e9a04867fe0e0d8b0efa455ce8e4e29e7d9fd4531412b75dbd0325e9304
Status: Downloaded newer image for maximofn/multiarch:latest
Hello from ARMAparece Hello from ARM ya que la Raspberry tiene un micro con arquitectura ARM
Como se puede ver, cada máquina se ha bajado la imagen que necesitaba
Escritura correcta de Dockerfiles avanzado
Ya vimos la manera de escribir correctamente Dockerfiles, pero hay una cosa más que podemos hacer ahora que conocemos el multi-stage build y es crear un contenedor para crear el ejecutable y otro más pequeño para ejecutarlo
Llegamos a la conclusión de que un buen Dockerfile podría ser este
FROM python:3.9.18-alpine
WORKDIR /sourceCode/sourceApp
COPY ./sourceCode/sourceApp .
CMD ["python3", "app.py"]Vamos a crear ahora un ejecutable en un contenedor builder y en otro más pequeño lo ejecutamos
FROM python:3.9.18-alpine as builder
WORKDIR /sourceCode/sourceApp
RUN apk add --no-cache gcc musl-dev libc-dev && pip install pyinstaller
COPY ./sourceCode/sourceApp .
RUN pyinstaller --onefile app.py
FROM alpine:3.18.3
WORKDIR /sourceCode/sourceApp
COPY --from=builder /sourceCode/sourceApp/dist/app .
CMD ["./app"]Creamos el código de Python en la ruta necesaria
InputPython!mkdir multistagebuild/sourceCode!mkdir multistagebuild/sourceCode/sourceApp!touch multistagebuild/sourceCode/sourceApp/app.py!echo 'print("Hello from Alpine!")' > multistagebuild/sourceCode/sourceApp/app.pyCopied
Ahora compilando la imagen
InputPython!docker build -t maximofn/multistagebuild:alpine-3.18.3 ./multistagebuildCopied
[+] Building 0.0s (0/0) docker:default[+] Building 0.0s (0/1) docker:default[+] Building 0.2s (3/5) docker:default=> [internal] load build definition from Dockerfile 0.1s=> => transferring dockerfile: 357B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/alpine:3.18.3 0.1s=> [internal] load metadata for docker.io/library/python:3.9.18-alpine 0.1s=> [auth] library/alpine:pull token for registry-1.docker.io 0.0s[+] Building 0.3s (3/5) docker:default=> [internal] load build definition from Dockerfile 0.1s=> => transferring dockerfile: 357B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/alpine:3.18.3 0.2s=> [internal] load metadata for docker.io/library/python:3.9.18-alpine 0.2s=> [auth] library/alpine:pull token for registry-1.docker.io 0.0s[+] Building 0.5s (4/6) docker:default=> [internal] load build definition from Dockerfile 0.1s=> => transferring dockerfile: 357B 0.0s=> [internal] load .dockerignore 0.1s=> => transferring context: 2B 0.0s=> [internal] load metadata for docker.io/library/alpine:3.18.3 0.4s...=> exporting to image 0.1s=> => exporting layers 0.1s=> => writing image sha256:8a22819145c6fee17e138e818610ccf46d7e13c786825 0.0s=> => naming to docker.io/maximofn/multistagebuild:alpine-3.18.3 0.0s
La ejecutamos
InputPython!docker run --rm --name multi_stage_build maximofn/multistagebuild:alpine-3.18.3Copied
Hello from Alpine!
La imagen maximofn/multistagebuild:alpine-3.18.3 solo pesa 13.6 MB
Diferencia entre RUN, CMD y ENTRYPOINT
RUN
El comando RUN es el más sencillo, simplemente ejecuta un comando en el momento de la compilación de la imagen. Por ejemplo, si queremos instalar un paquete en la imagen, lo hacemos mediante RUN.
Por tanto, es importante: RUN se ejecuta en el momento de la compilación de la imagen, no cuando se ejecuta el contenedor
CMD
El comando CMD es el comando que se ejecuta cuando se ejecuta el contenedor. Por ejemplo, si queremos que el contenedor ejecute un comando cuando se ejecute, lo hacemos mediante CMD. Por ejemplo, si tenemos una aplicación de Python en un contenedor, con CMD podemos indicarle que cuando se ejecute el contenedor ejecute la aplicación de Python.
De esta manera, cuando se levante el contenedor, se ejecutará la aplicación de Python. Es decir, si hacemos docker run <image> se ejecutará la aplicación de Python. Pero CMD nos permite sobreescribir el comando que se ejecuta cuando se levanta el contenedor, por ejemplo, si hacemos docker run <image> bash se ejecutará bash en vez de la aplicación de Python.
ENTRYPOINT
El comando ENTRYPOINT es similar al comando CMD, pero con una diferencia: y es que ENTRYPOINT no está pensado para sobreescribirse. Es decir, si tenemos una aplicación de Python en un contenedor, con ENTRYPOINT podemos indicarle que cuando se ejecute el contenedor ejecute la aplicación de Python. Pero si hacemos docker run <image> bash se ejecutará la aplicación de Python, no bash.
Un uso muy común de ENTRYPOINT es cuando queremos que el contenedor sea un ejecutable, por ejemplo, si queremos que el contenedor sea un ejecutable de una versión de Python que no tenemos en nuestro host, porque por ejemplo queremos probar la nueva versión de Python que ha salido, podemos hacer
FROM python:3.9.18-alpine
ENTRYPOINT ["python3"]De esta manera, cuando se levante el contenedor, se ejecutará Python. Es decir, si hacemos docker run <image> se ejecutará Python. Pero ENTRYPOINT nos permite sobrescribir el comando que se ejecuta cuando se levanta el contenedor, por ejemplo, si hacemos docker run <image> myapp.py se ejecutará python3 myapp.py dentro del contenedor. Así podemos probar nuestra aplicación de Python en la nueva versión de Python
Cambios en un contenedor
Con docker diff podemos ver las diferencias que hay entre el contenedor y la imagen, lo que es lo mismo, la diferencia en el contenedor desde que se creó hasta ahora
Vamos a correr un contenedor y dentro creamos un archivo
InputPython!docker run --rm -it --name ubuntu-20.04 ubuntu:20.04 bashCopied
root@895a19aef124:/# touch file.txt
Ahora podemos ver la diferencia
InputPython!docker diff ubuntu-20.04Copied
C /rootA /root/.bash_historyA /file.txt
A significa que se ha añadido, C significa que se ha cambiado y D significa que se ha borrado
Docker en Docker
Supongamos que tenemos contenedores que necesitan levantar o apagar otros contenedores. Esto se logra de la siguiente manera
Dado que en Linux todo es un archivo y el host se comunica con Docker mediante un socket. Por lo que para Linux, ese socket es un archivo. Así que si le montamos ese socket como un archivo al contenedor, podrá hablar con Docker
Primero vamos a montar un contenedor con Ubuntu
InputPython!docker run -d --name ubuntu ubuntu:latest tail -f /dev/nullCopied
144091e4a3325c9068064ff438f8865b40f944af5ce649c7156ca55a3453e423
Vamos a montar el contenedor que va a poder hablar con Docker montando la carpeta /var/run/docker.sock
$ docker run -it --rm --name main -v /var/run/docker.sock:/var/run/docker.sock docker:19.03.12
/ # Nos hemos metido dentro de un contenedor, y si dentro ejecutamos docker ps
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9afb778d6c20 docker:19.03.12 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds main
144091e4a332 ubuntu:latest "tail -f /dev/null" 19 seconds ago Up 18 seconds ubuntuComo podemos ver, dentro de Docker podemos ver los contenedores del host
Podemos correr un nuevo contenedor
# docker run -d --name ubuntu_from_main ubuntu:latest tail -f /dev/null
362654a72bb0fb047c13968707a6f16b87fed7ce051eb5c1a146b15828589a1a
/ #Y si volvemos a ver los contenedores
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
362654a72bb0 ubuntu:latest "tail -f /dev/null" 3 seconds ago Up 3 seconds ubuntu_from_main
9afb778d6c20 docker:19.03.12 "docker-entrypoint.s…" About a minute ago Up About a minute main
144091e4a332 ubuntu:latest "tail -f /dev/null" 2 minutes ago Up About a minute ubuntuPero si ahora ejecutamos una nueva terminal del host, veremos el contenedor creado desde dentro del contenedor
InputPython!docker psCopied
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES362654a72bb0 ubuntu:latest "tail -f /dev/null" About a minute ago Up About a minute ubuntu_from_main9afb778d6c20 docker:19.03.12 "docker-entrypoint.s…" 3 minutes ago Up 3 minutes main144091e4a332 ubuntu:latest "tail -f /dev/null" 3 minutes ago Up 3 minutes ubuntu
Todo lo que hagamos desde el contenedor main se verá reflejado en el host
Esto tiene la ventaja de que podemos instalar programas en un contenedor que tiene acceso al host para no tener que instalarlos en el host. Por ejemplo dive es una herramienta para explorar contenedores, pero si no la quieres instalar en el host la puedes instalar en un contenedor con acceso al host, así desde ese contenedor main puedes explorar el resto de contenedores sin tener que instalarla en el host