Tabla de Contenidos
Gestión de imágenes y contenedores
Notas pertenecientes al curso Docker a fondo e Introducción a Kubernetes: aplicaciones basadas en contenedores
Hasta ahora hemos aprendido los conceptos fundamentales de Docker: qué es, para qué sirve, cómo ponerlo en marcha en nuestro sistema operativo, qué son las imágenes… E incluso hemos lanzado ya nuestros primeros contenedores.
En este módulo continuamos viendo cuestiones fundamentales y en concreto aprenderemos a:
- Gestionar las imágenes que tenemos en nuestro sistema
- Crear contenedores a partir de dichas imágenes y a ponerlos en marcha
- Ver el estado de nuestros contenedores
- Qué es el mapeo de puertos y para qué se utiliza
Al finalizarlo sabrás cómo usar la CLI de Docker para realizar las tareas básicas de administración tanto de contenedores como de imágenes.
Gestionar las imágenes de Docker
Hay tres comandos principales que debes conocer para empezar a gestionar las imágenes Docker que tengas en tu máquina:
pullpara descargar imágenes de un repositorioimagespara listar las imágenes que tengas descargadasrmipara borrar imágenes
Descargar imágenes
Como ya hemos visto en el módulo anterior, el comando docker pull permite descargar una imagen de un repositorio. Por el momento trabajaremos solo con imágenes públicas en el repositorio de Docker Hub. Más adelante veremos cómo crear y descargar imágenes de repositorios privados.
El uso de este comando es muy simple:
docker pull <nombre_imagen>.
Ver las imágenes descargadas
Para ver las imágenes descargadas basta con ejecutar el comando docker images. Con este comando, Docker nos listará todas las imágenes que tenemos descargadas en nuestra máquina. Por cada imagen Docker nos da la siguiente información:
- El nombre de la imagen (campo REPOSITORY)
- La etiqueta (TAG) de la imagen (hablaremos sobre las etiquetas posteriormente)
- El identificador único de la imagen
- Cuándo se creó la imagen (¡no cuándo se descargó!) en el repositorio
- El tamaño que ocupa
Este comando tiene dos modificadores interesantes. El primero es -q (o --quiet) que hace que tan solo nos muestre los ids de las imágenes, sin ninguna otra información y el segundo es -f (o --filter) que nos permite filtrar las imágenes a listar mediante algún criterio soportado (ver documentación para conocer cuáles son).
Borrar imágenes
Para borrar una o varias imágenes debemos usar el comando docker rmi cuya sintaxis es muy sencilla:
docker rmi <imagenes>
Podemos borrar una imagen usando su id (el obtenido mediante docker images) o bien por nombre. Hay una sutil diferencia entre borrar una imagen por id o por nombre, pero ya lo veremos más adelante ya que necesitamos comprender cómo funciona el sistema de capas en Docker para ello. De momento quédate con que docker rmi sirve para borrar imágenes y debes especificar sus ids o sus nombres.
Siempre que en un comando de Docker debamos pasar un id (sea de lo que sea dicho id), nos basta con pasar el mínimo número de dígitos que identifiquen unívocamente dicho id. No es necesario pasar el id completo. Así, si tenemos dos imágenes descargadas y una tiene el id f7f1caf5064e y la otra el id fbf5d242965f y queremos borrar la primera, basta con teclear docker rmi f7, ya que f7 identifica unívocamente a la primera imagen (ningún otro id empieza por f7). Eso es muy interesante ya que permite ahorrar muchas pulsaciones.
Al comando rmi le podemos pasar uno o varios ids (o nombres). Así docker rmi f7 fb es un comando válido (asumiendo que f7 y fb identifican a ids de imágenes).
Existe una diferencia entre usar IDs o nombres en el comando rmi. En la siguiente lección se comenta esa diferencia.
Si usas bash o Powershell hay un truco para borrar rápidamente todas las imágenes que tengamos instaladas en nuestro sistema y es teclear: docker rmi $(docker images -q). Observa que esto no funciona en la línea de comandos tradicional de Windows (cmd).
Gestionar los contendores
Iniciar contenedores
Una vez tenemos una imagen descargada podemos crear un contenedor mediante el comando docker run, pasándole el nombre de la imagen. Debemos tener presente que este comando es uno de los más complejos de Docker y tiene multitud de opciones. Las más importantes las iremos viendo a lo largo del curso.
docker run hello-world
Si no tienes la imagen que le indiques al comando run, este se la descargará.
Por defecto, cuando ejecutamos un contenedor, este se ejecuta en primer plano (lo que Docker llama foreground). Cuando ejecutamos un contenedor en primer plano:
- El comando
runse espera a que el contenedor finalice. - Docker enlaza la salida estándar (stdout) y la salida de error (stderr) del contenedor al terminal. Es decir, veremos por la ventana de línea de comandos la salida generada por el contenedor.
Si no queremos que el comando run espere al contenedor, podemos usar el modificador -d. Con ese modificador Docker inicia el contenedor, imprime su id por la consola y nos devuelve el control. El contenedor continúa ejecutándose, pero no veremos nada de él en la línea de comandos.
Listar los contenedores
Para listar los contenedores que tenemos en el sistema podemos usar el comando docker ps. Dicho comando muestra todos los contenedores que se están ejecutando. Recuerda que cuando un contenedor finaliza su proceso inicial, termina su ejecución y se para, pero sigue existiendo. Dichos contenedores parados no se muestran por defecto con el comando ps, aunque podemos verlos si usamos el modificador -a (o --all).
Hagamos una prueba, ejecuta tres veces la imagen hello-world (es decir, teclea tres veces docker run hello-world). Después de cada ejecución verás un mensaje por pantalla y el comando run te devolverá el control, ya que el contenedor ha finalizado su ejecución. Ahora consulta los contenedores que tienes mediante docker ps y verás una salida parecida a la siguiente:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Es decir, no aparece ningún contenedor porque no hay ninguno ejecutándose en este momento. Ahora usa el modificador -a (docker ps -a) y observa la salida, que será parecida a:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES bee553f522b6 hello-world "/hello" 2 minutes ago Exited (0) 2 minutes ago happy_lumiere 65839cd1422c hello-world "/hello" 2 minutes ago Exited (0) 2 minutes ago awesome_wing 57f700469b93 hello-world "/hello" 2 minutes ago Exited (0) 2 minutes ago reverent_sammet
Observa que ahora aparecen los tres contenedores que has creado mediante los tres comandos docker run ejecutados antes. En la siguiente imagen se muestra un ejemplo de la salida:
La información que se muestra para cada contenedor es la siguiente:
- Id del contenedor
- Nombre de la imagen a partir de la cual se ha creado el contenedor
- El comando (proceso) que ha ejecutado el contenedor
- Cuándo se ha creado el contenedor
- El estado del contenedor (si está corriendo o está parado)
- Los puertos usados por el contenedor (hablaremos más adelante sobre los puertos)
- El nombre del contenedor. No te preocupes mucho ahora por el nombre, si tú no especificas nada, Docker asigna uno.
Si quieres especificar un nombre usa el modificador --name con el comando run, e indica el nombre que quieras darle al contenedor:
docker run --name HolaMundo hello-world
Creará un contenedor con el nombre “HolaMundo”.
Al igual que el comando images, el comando ps también tiene el modificador -q (o --quiet) que solo muestra los ids por pantalla. Si usamos bash o Powershell podemos usar la salida de docker ps -q como entrada de otros comandos que aceptan ids de contenedores.
Borrar contenedores
El comando rm borra contenedores. Dicho comando funciona de forma muy parecida al comando rmi (que borra imágenes): hay que pasarle uno o más ids de contenedores a borrar. Por defecto, dicho comando no borra contenedores que se están ejecutando (hay que pararlos primero).
Si queremos borrar todos los contenedores parados a la vez:
docker rm $(docker ps -q -a)
Como se ha comentado, el comando rm no borrará contenedores en marcha. Si queremos borrar un contenedor que se está ejecutando, podemos hacerlo usando el modificador -f (o --force).
Parar contenedores
Hay dos comandos para parar contenedores: stop y kill. El primero intenta detener un contenedor de “forma amable”: intenta parar el proceso de forma correcta y al cabo de un periodo de tiempo (10 segundos por defecto) mata al proceso del contenedor (si este no ha terminado). Por su parte, kill se limita a matar el proceso del contenedor.
Para parar un contenedor de forma amable podemos usar:
docker stop mi_contenedor
Donde mi_contenedor es el nombre del contenedor. Podemos usar el modificador -t (o --time) para indicar cuál es el periodo de gracia entre que Docker da orden al proceso para que finalice correctamente y decide “matarlo” por las malas.
Por su parte, la sintaxis del comando kill es muy parecida:
docker kill mi_contenedor
En contenedores Linux, el comando docker stop manda primero un SIGTERM y al cabo del periodo de gracia un SIGKILL al proceso del contenedor. Esto permite al proceso pararse correctamente respondiendo a la señal SIGTERM. En contenedores Windows las cosas no son tan sencillas y parece ser que, en según qué casos, el proceso del contenedor puede no recibir la notificación de que debe pararse. No entraremos en más detalles en este curso, pero ten presente que (por ahora) en algunos casos, un contenedor Windows si se para con docker stop puede no darse cuenta de que se debe parar (y, por lo tanto, al final del periodo de gracia Docker mata el proceso).
Reiniciar contenedores
Cuando tenemos un contenedor parado puedes reiniciarlo mediante el comando start. Dicho comando reinicia uno o varios contenedores parados. Es decir, básicamente vuelve a ejecutar el proceso de inicio del contenedor otra vez. En muchos casos, no hay diferencia entre realizar un start sobre un contenedor parado o bien usar run para crear un contenedor nuevo con la misma imagen. Más adelante veremos qué diferencia hay entre usar start y run.
Su sintaxis es muy sencilla:
docker start mi_contenedor
Si queremos que el contenedor se ejecute interactivamente, es decir, enviando la salida estándar a la consola y los errores, podemos aplicarle el modificador -a o --attach.
Borrar imágenes con contenedores existentes
Si hay un contenedor creado a partir de una imagen no vas a poder borrarla:
El modificador -n de docker ps muestra los últimos “n” contenedores creados. En este caso -n 1 muestra el último contenedor creado
En la imagen se ve cómo se crea un contenedor (usando docker run) y luego se intenta usar docker rmi para borrar la imagen dockercampusmvp/go-hello-world que se ha usado para crear el contenedor. Dado que existe un contenedor que está usando la imagen, docker rmi da error.
Si paramos el contenedor e intentamos borrar la imagen, el error es el mismo:
Una solución sería eliminar todos los contenedores que usan la imagen, pero otra opción es usar el modificador -f a docker rmi:
Observa que puedes usar docker rmi -f ¡incluso si hay un contenedor en marcha!
Ahora, eso plantea una duda: ¿cómo puede borrar Docker la imagen si hay contenedores que la usan?
La realidad es que, en este caso, el comando rmi no borra la imagen sino que simplemente “le quita el nombre”. De hecho, si miras la imagen anterior verás que en la salida del comando rmi, Docker responde con Untagged que significa “desetiquetado”. En este caso Docker nos dice que ha eliminado el nombre dockercampusmvp/go-hello-world. Pero la imagen en nuestro disco continua estando:
Observa como ahora en la salida de docker ps se nos muestran ambos contenedores pero la imagen no tiene nombre. En su lugar, Docker nos muestra el ID de la imagen (b03158891458 en este ejemplo). Realmente Docker referencia las imágenes por el ID, el nombre es solo “una etiqueta” que se coloca para que nosotros, los humanos, no tengamos que andar manejando esos IDs.
Si realmente quieres borrar la imagen de disco, debes usar el comando docker rmi pero pasándole el ID:
Observa como en este caso, al usar el ID, Docker se queja de que existen contenedores que usan la imagen y, ahora, ni el modificador -f nos puede ayudar: para eliminar realmente la imagen de disco, no puede haber ningún contenedor en marcha que la use.
En la imagen anterior puedes ver como tenemos dos contenedores que usan la imagen b03 (uno parado y otro en marcha). Eliminamos el contenedor que está en marcha, por lo que nos quedamos solamente con el contenedor parado. Entonces sí que podemos usar el modificador -f de docker rmi para borrar la imagen. Observa como ahora la salida de docker rmi es Deleted, porque, ahora sí, la imagen ha sido eliminada de disco.
Así que resumiendo:
docker rmi <nombre>: funciona incluso con contenedores en marcha (usando-f), pero en este caso no elimina la imagen físicamente, solo elimina el nombre para referirnos a ella. Si no hay contenedores en marcha entonces la imagen se elimina definitivamente (aunque en este caso debes usar-f).docker rmi <id>: si hay contenedores en marcha no va a funcionar, ni usando-f. Si hay contenedores parados, debemos usar-f. En este caso la imagen se elimina físicamente de disco.
Así pues, nunca podremos eliminar físicamente una imagen en disco si hay algún contenedor en marcha con esta imagen. Si hay contenedores parados, sí que podremos hacerlo.
Comandos básicos de Docker
A continuación te muestro una línea de los comandos básicos de Docker, a modo de repaso rápido.
| Comando | Acción |
|---|---|
docker ps | Lista los contenedores ejecutándose |
docker ps -a | Lista todos los contenedores |
docker run | Crea un contenedor a partir de una imagen |
docker images | Lista las imágenes de docker descargadas en mi ordenador |
docker start | Reinicializa un contenedor parado (en background) |
docker stop | Para (correctamente) un contenedor en marcha |
docker kill | Mata un contenedor en marcha |
docker rmi | Elimina una imagen de mi ordenador |
Mapeo de puertos
Hasta ahora hemos ejecutado contenedores sencillos como hello-world con los que no interactuábamos. Pero, es habitual que, más pronto que tarde, necesites comunicarte con un contenedor. Para ello debes saber dos cosas:
- Cuál es la IP del contenedor
- A través de qué puerto (o puertos) está escuchando
Responder a la primera pregunta es muy sencillo: la IP de un contenedor es la IP de la máquina que ejecuta dicho contenedor. Es decir, si los contenedores se están ejecutando en tu máquina, su IP es localhost (127.0.0.1). Para saber la segunda respuesta, debes primero consultar la documentación del que haya hecho el contenedor.
Por ejemplo, vamos a usar el contenedor dbergk/pingpong. En su página de información nos indica que, por defecto, los contenedores creados con dicha imagen esperan una petición a la url /ping en su puerto 8080. Y que responderán con pong.
Es decir, teniendo un contenedor en marcha, se debe poder navegar a la url http://localhost:8080/ping y el resultado debe ser pong. Pruébalo tú mismo: descarga la imagen, ejecuta un contenedor y navega a dicha URL.
Bien, si lo has probado habrás visto que no te funciona. La razón es que debes tener presente que los puertos que abre un contenedor son sus propios puertos, no los de la máquina que ejecuta el contenedor.
Para comprobarlo, pon en marcha a la vez dos contenedores de la imagen dbergk/pingpong. Si ahora haces un docker ps verás algo parecido a:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 5685b0f533fb dbergk/pingpong "java PingPong" 2 minutes ago Up 16 minutes 8080/tcp inspiring_wing c00ff9389dcc dbergk/pingpong "java PingPong" 2 minutes ago Up 18 minutes 8080/tcp clever_ardinghelli
Observa que tus dos contenedores están en marcha. Si cada uno de ellos hubiera intentado abrir el puerto 8080 de la máquina host esto debería haber dado error, ya que un puerto no se puede abrir dos veces. Observa la columna PORTS, en la que en ambos casos pone 8080/tcp. Eso nos indica que ambos contenedores exponen su puerto 8080.
Bien, ahora la pregunta es: ¿cómo puedo acceder a dichos contenedores?
La respuesta es que, ahora mismo, no puedes. Porque ningún puerto de la máquina host ha sido mapeado a los puertos de los contenedores. Cuando lanzas un contenedor que abra algún puerto, debes mapear un puerto de la máquina host al puerto del contenedor. Si no lo haces, no podrás comunicarte con el contenedor.
Para ello, usa el modificador -p del comando docker run. Dicho comando espera una cadena en formato:
puerto_host:puerto_contenedor
Por ejemplo, prueba con:
docker run -d -p 8080:8080 dbergk/pingpong`
Observa ahora la salida de docker ps:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 514850f9a930 dbergk/pingpong "java PingPong" 3 seconds ago Up 2 seconds 0.0.0.0:8080->8080/tcp elated_payne 5685b0f533fb dbergk/pingpong "java PingPong" 3 minutes ago Up 20 minutes 8080/tcp inspiring_wing c00ff9389dcc dbergk/pingpong "java PingPong" 3 minutes ago Up 22 minutes 8080/tcp clever_ardinghelli
¿Observas la diferencia en la columna PORTS? El último contenedor que hemos lanzado pone 0.0.0.0:8080→8080/tcp. Eso nos indica que el puerto 8080 de la máquina host ha sido mapeado al puerto 8080 del contenedor. Ahora, si navegas a http://localhost:8080/ping deberías recibir la respuesta del contenedor:
Por supuesto el puerto del host y del contenedor no tiene por qué coincidir. Así, si usaras -p 5000:8080 estarías mapeando el puerto 5000 del host al puerto 8080 del contenedor, por lo que entonces deberías ir a http://localhost:5000/ping para comunicarte con este contenedor. Esto te permite tener varios contenedores que escuchen por su mismo puerto y comunicarte con ellos usando puertos distintos en el host.
Comunicándose con un contenedor a través de su IP interna
Antes hemos comentado que no podías acceder al contenedor si no tenía su puerto mapeado a un puerto del host, pero eso no es estrictamente cierto.
La verdad es que cada contenedor recibe una IP propia dentro de la red privada que monta Docker. Siempre podemos acceder al contenedor usando dicha IP y el puerto que abre el contenedor (en el ejemplo de la imagen dbergk/pingpong era el 8080).
Para encontrar la IP propia de un contenedor debes ejecutar el comando:
docker inspect <id-contenedor> -f "{{ .NetworkSettings.IPAddress }}"
Recuerda que puedes usar docker ps para obtener el id:
Observa como en este caso usamos el puerto 8080, es decir el puerto que abre el contenedor: cuando accedemos a un contenedor usando la IP de la red virtual de Docker, debemos usar los puertos que el contenedor abre, no los mapeados al host (si los hubiera).
Si pruebas lo mismo en una máquina Windows con Docker for Windows seguramente tendrás problemas:
Recuerda que en Docker for Windows, los contenedores se ejecutan mediante una máquina virtual (MV) que se ejecuta en Hyper-V, o mediante WSL 2. Eso significa que son esos artilugios los que están conectados a la red privada de Docker, no el host que ejecuta Windows. Por ello, acceder a contenedores Linux ejecutándose en Docker for Windows, a través de la IP interna del contenedor no es un escenario soportado.
Si te interesan los detalles técnicos, en esta issue de Github se describen, así como diversos workarounds que han ido funcionando y dejado de funcionar en distintas versiones de Docker.
Por supuesto, si en lugar de contenedores Linux, lo que ejecutas son contenedores Windows, entonces sí que funciona (ya que entonces es directamente el host de Windows quien ejecuta los contenedores).
Nunca olvides que, en Windows, los contenedores Linux se ejecutan a través de una MV o de WSL2 y eso genera alguna que otra fricción.
Inmutabilidad de contenedores
Los contenedores, una vez creados son inmutables. Eso significa que no puedes modificar las características de un contenedor como, por ejemplo, los puertos mapeados, tras haberlo creado a partir de una imagen. Es por ello que el comando docker start (para reiniciar un contenedor existente) no permite los modificadores como (-p). Para ello debes usar docker run, que crea otro contenedor.
Que los contenedores sean inmutables no significa que no se puedan crear nuevos ficheros en ellos o ejecutar procesos adicionales (más adelante verás cómo). Lo que implica es que, una vez creados, no puedes modificar sus características: la imagen que ejecutan, sus variables de entorno, los puertos redirigidos y otras.
Ejercicios propuestos
En este módulo hemos visto cómo funcionan los comandos básicos de Docker. Es importante que te familiarices con ellos porque los usarás constantemente.
A continuación tienes algunos ejemplos para practicar:
- Borra la imagen
dbergk/pingpong - Crea dos contenedores de la imagen
dbergk/pingpong. Hazlo en modo background. Asegúrate de que puedes comunicarte con ambos contenedores usando dos puertos a tu elección - Detén uno de los contenedores:
- Verifica que no puedes conectarte con este contenedor
- Reinícialo de nuevo
- Verifica que puedes conectarte con él de nuevo
- Para ambos contenedores
- ¿Qué comando usarías para encontrar los ids de ambos contenedores?
- Borra la imagen
dbergk/pingpongde nuevo
Repasa lo necesario de este módulo para ser capaz de realizar todas esas tareas con fluidez.
