Cuando estás empezando con Docker hay 4 comandos que son muy parecidos y que pueden llevarte a confusión: run
, start
, create
y exec o lo que es lo mismo: "correr", iniciar, crear y ejecutar.
Los nombres se parecen mucho y sus funciones parecen similares. Por ejemplo, ¿no es lo mismo iniciar un contenedor que ejecutarlo?
Pues no exactamente. Así que vamos a verlo para que quede claro:
docker create
: este comando crea un nuevo contenedor a partir de una imagen. Pero no lo ejecuta.
docker start
: pone en funcionamiento un contenedor que esté parado. Es decir, cuando has creado un contenedor con el comando anterior (docker create
) puedes ejecutarlo con docker start
.
docker run
: es una combinación de los dos anteriores, ya que este comando crea el contenedor a partir de la imagen que le indiquemos y, acto seguido, lo pone en funcionamiento. Es más, puede sustituir también al comando docker pull
para descargar la imagen si no está en local todavía.
docker exec
: ejecuta dentro del contenedor un comando que se le indique. Algo totalmente diferente a lo anterior.
Un ejemplo de Docker paso a paso
Para ver las diferencias en la práctica, vamos a hacer un ejercicio paso a paso.
Evidentemente, para poder seguirlo tienes que tener instalado Docker. Si estás en Windows o en macOS, Docker Desktop.
En primer lugar, vamos a descargarnos una imagen con la que trabajar. Usaremos la mítica hello-world, que es una imagen oficial y contiene un pequeño ejemplo que se limita a saludarnos, pero que es perfecta para hacer pruebas y ver que todo funciona como es debido. Para ello usaremos el siguiente comando:
docker pull hello-world
que se traerá a nuestra máquina una copia de la imagen.
Podemos ver la imagen en el listado de imágenes locales escribiendo:
docker images
La siguiente figura muestra el resultado de ejecutar estos dos comandos:
Ahora que ya tenemos la imagen, vamos a crear un contenedor a partir de ella, para lo cual usaremos el siguiente comando:
docker create --name hw-01 hello-world
Esto crea el contendor, pero no lo ejecuta. De hecho, si vamos a listar los contenedores en ejecución:
docker ps
no lo verás por ningún lado. Sólo puedes verlo si indicas que se muestren todos los contenedores, independientemente de su estado de ejecución:
docker ps -a
En la siguiente captura puedes ver la ejecución de estos 3 comandos:
Vale. Ahora vamos a ejecutar la imagen con docker run
, especificando un nombre diferente al anterior, en este caso hw-02
:
docker run --name hw-02 hello-world
Ahora, recibimos por consola el resultado de ejecutar el contenedor, con el mensaje de "Hola Mundo" y una explicación de cómo se ha ejecutado para poder hacerlo:
O sea, a diferencia del comando anterior, que solo creó el contenedor, docker run
ha creado un nuevo contenedor (hw-02
) y además lo ha iniciado (y por lo tanto ejecutado), cosa que podemos ver por el resultado pero también si listamos los contenedores que tenemos:
Y no está en ejecución porque este contenedor se detiene automáticamente cuando termina de hacer lo suyo, ya que no tiene ningún proceso de tipo "daemon" o servicio que se ejecute todo el tiempo.
Vamos a lanzarlo de nuevo, pero esta vez con docker start
. Como hemos visto, esto permite iniciar (y ejecutar, por tanto, lo que tenga dentro) un contenedor que ya exista y esté parado:
docker start hw-02
Fíjate en una cosa importante de la imagen anterior: cuando ejecutamos el contenedor con docker start
, lo ejecuta, pero no captura la salida (técnicamente el STDOUT o salida estándar). Por lo tanto no se ve el resultado de la ejecución como antes, solo el nombre del contenedor. Si hacemos un listado de los contenedores, vemos que no hay ninguno en ejecución, pero que se acaba de ejecutar el que queríamos, solo que no hemos visto nada.
Si el contenedor fuese uno que queda en ejecución (por ejemplo, el de la imagen ubuntu
, que ejecuta ese sistema operativo), entonces sí que lo veríamos en ejecución con docker ps
, pero no en este caso.
Podemos recuperar la salida estándar con docker start
si le pasamos el parámetro -a
(o --attach
):
Otra cosa a tener en cuenta es que el contenedor hw-01
, que creamos antes y está sin utilizar, lo tenemos que ejecutar con docker start
, ya que docker run
crea siempre uno nuevo, y luego lo inicia. Esta es una diferencia crucial, aparte del hecho de que docker run
tiene muchos más modificadores para variar su comportamiento.
docker start hw-01
Fíjate en cómo el contenedor se ha ejecutado y se ha detenido tras la ejecución.
En cuanto a docker exec
, es capaz de ejecutar los comandos que necesitemos en cualquier contenedor que esté en ejecución. Esto último es muy importante, ya que no se encarga de lanzarlos y luego ejecutar el comando.
Por ejemplo, si descargamos una imagen de alguna distribución Linux, por ejemplo la de Alpine Linux, y la ejecutamos:
docker run alpine
Vemos que descarga la imagen (puesto que no la teníamos en local), acto seguido crea un contenedor y lo ejecuta, pero se detiene inmediatamente, ya que es una imagen de propósito general, pensada para tener una base del sistema operativo para nuevas imágenes, pero no para ejecutar un comando concreto. Si intentásemos ejecutar algo con docker exec
no funcionaría porque no está el contenedor en ejecución.
Así que, para probar exec,
tendremos que ejecutar el contenedor y mantener a Alpine Linux en funcionamiento. Para ello podemos usar el comando run
así:
docker run -it --rm alpine
Esto ejecutará un nuevo contenedor a partir de la imagen alpine
y abrirá una sesión interactiva con él y un terminal (-it
). De este modo lo tendremos funcionando en segundo plano y podremos probar el lanzamiento de comandos con exec
. La opción --rm
sirve para eliminar automáticamente el contenedor una vez se deje de ejecutar
Al hacer esto tendremos una consola abierta con el contenedor, pero lo importante aquí es que lo dejamos en ejecución y podremos usar exec
contra él.
Si abres otra terminal y listas los contenedores en ejecución, verás que el nuevo contenedor está ahí esperando:
Nota: en un contenedor que contenga algún servicio o proceso, que será lo habitual si es para una de nuestras aplicaciones, no sería necesario esto, ya que estará en ejecución mientras no lo paremos. En este ejemplo que estamos haciendo el uso de docker exec
no tiene mucho sentido porque ya tenemos una sesión bash abierta contra el contenedor tras hacer el docker run
(podríamos haberlo mantenido en ejecución de muchas otras maneras, por ejemplo con tail -f /dev/null
), pero es solo a efectos de mostrarte cómo funciona docker exec
. En un contenedor real, siempre en funcionamiento, usaremos exec
cuando queramos.
Vale, ahora que ya tenemos el contenedor en funcionamiento, vamos a usar docker exec
para ejecutar algo en él. Por ejemplo, empecemos por algo sencillo: averiguar la ruta actual en la que estamos posicionados en el contenedor al lanzar los comandos:
docker exec -it c8b pwd
En este caso devuelve la raíz (/
).
Nota: fíjate que en lugar del nombre del contenedor he utilizado su ID, el cual no hace falta introducirlo entero y llega con los 2 o 3 primeros números mientras no coincidan con los de otro contenedor (es harto difícil). El parámetro -it
es la combinación de 2 y sirve para que nos devuelva la salida del comando en nuestro terminal.
Podemos especificar el directorio en el que queremos ejecutar el comando con el parámetro -w
(o su versión larga: --workdir
), así:
docker exec -it -w /root c8b pwd
En este caso devuelve, como es lógico, /root
, que es el que le hemos indicado como directorio base.
Pues de la misma manera podemos ejecutar cualquier comando (por ejemplo, la lista de subdirectorios y archivos del directorio actual):
docker exec -it c8b ls
o incluso instrucciones que se le pasen a un comando:
docker exec -it c8b sh -c "echo Hola desde Alpine Linux"
Con exec
podremos modificar cosas en el sistema operativo subyacente a nuestro contenedor, administrar ciertos parámetros, etc... aunque debemos tener en cuenta que nada que modifiquemos en el contenedor se conservará cuando lo detengamos (las imágenes son inmutables).
Este es el resultado de ejecutar todos estos ejemplos:
Cuando hayas terminado sal del contenedor en ejecución, en el primer terminal, escribiendo simplemente el comando exit
. Esto lo detendrá y además lo eliminará porque especificamos el modificador --rm
en docker run
.
En resumen
Aunque estos cuatro comandos de Docker tienen nombres parecidos y algunos se pueden usar para cosas parecidas, son muy diferentes entre sí y cada uno tiene su utilidad. Espero que te hayan quedado claros.
Si quieres aprender Docker y Kubernetes a fondo, sin recetas mágicas y a prueba de futuro, nuestro curso Docker y Kubernetes: desarrollo y despliegue de aplicaciones basadas en contenedores es justo lo que estabas buscando. Y su tutor es mucho mejor que yo en esto 😉
Fecha de publicación: