En este post hablaremos sobre la teoría y los conceptos más importantes de Docker. Ten en cuenta que comprender esta base es esencial y, como consecuencia, si tuviera que elegir el tema más importante de todos los que hay en este curso de Docker sería éste.
Te contaré mi caso: He intentado estudiar muchas veces Docker y ninguna de ellas ha tenido éxito… ¿Por qué? Básicamente porque estudiaba la teoría y los conceptos de Docker por encima y después pasaba directamente a la práctica con comandos y ficheros… Como te puedes imaginar entendía muy poco de lo que hacía y me resultaba muy difícil.
Es por ello que te recomiendo que te asegures de comprender muy bien todos los conceptos que explico a continuación, de esta manera tendrás la mitad del camino hecho y todo lo que hagamos en la parte práctica te resultará tremendamente sencillo.
¿Qué es Docker?
Docker es un sistema de virtualización que nos permite construir, ejecutar y desplegar aplicaciones de manera sencilla, portable, rápida y fiable:
- Portable, quizás la característica más importante puesto que permite que una aplicación junto con sus dependencias funcione siempre bien en cualquier ordenador sin necesidad de tener que configurarlo o seguir complejas guías.
- Rápido, apenas toma unos segundos desplegar un contenedor y hacer que la aplicación que éste contiene comience a funcionar.
- Sencillo, solo será necesario crear, como mínimo, un pequeño fichero en el proyecto para indicarle sus dependencias y cómo debe arrancar para que la aplicación comience a funcionar dentro de un contenedor.
- Fiable, una aplicación siempre se ejecutará correctamente, sin errores, en cualquier lugar, siempre y cuando hayamos hecho las pruebas iniciales correspondientes.
Algunos ejemplos y ventajas de Docker son:
- Equipos de desarrollo. Imaginemos un equipo de desarrollo que tiene 5 miembros y todos ellos tienen correctamente configurado su ordenador y la aplicación corriendo en su local, sin embargo, el proceso de configuración del proyecto u onboarding lleva como mínimo 2 días puesto que hay que instalar muchas cosas para que todo funcione correctamente.
Con contenedores conseguimos reducir el proceso de onboarding de un nuevo desarrollador a 10 minutos, simplemente le enviaríamos los proyectos con sus archivos de Docker y éste sólo tendría que ejecutarlos y crear contenedores con ellos para tener la aplicación corriendo en local.
Además con Docker nos aseguramos que al nuevo desarrollador le funcionará la aplicación puesto que, como hemos comentado, los contenedores son fiables ya que siempre se ejecutan de la misma forma. Si a un desarrollador le funciona la aplicación corriendo en un contenedor, también le funcionará al resto y a los nuevos miembros dado que no depende de la configuración de su máquina. - Distintos entornos. Supongamos que tenemos un entorno de desarrollo y otro de producción con configuraciones distintas donde, cada vez que tenemos que subir algo a producción, tenemos que pasarle al equipo de sistemas complejas instrucciones sobre cómo deben de desplegar la aplicación y las configuraciones de la misma. Este proceso, además de requerir tiempo, es muy propenso a errores humanos.
Con Docker y contenedores conseguimos eliminar posibles errores en los despliegues y reducir tiempo de publicación porque lo único que debemos hacer es enviarle al equipo de sistemas el fichero de configuración de Docker para que ejecuten y creen el contenedor. - Distintas versiones. Si tuviésemos 3 proyectos corriendo en distintas versiones de .NET (por ejemplo .NET 5, .NET 7 y .NET Core 2) sin contenedores sería necesario tener todas estas versiones instaladas en nuestro ordenador y en el entorno de producción. Esto no solo consume recursos sino que, tanto en .NET como en otros frameworks, esto puede dar lugar a incompatibilidades entre ellas.
Si utilizamos distintos contenedores para cada versión reducimos espacio en disco puesto que no es necesario tener siempre todas las versiones de .NET instaladas y funcionando en nuestro ordenador. Además evitamos posibles incompatibilidades por tener instalado a la vez distintas versiones de un mismo framework.
Imágenes y contenedores en Docker
Diferencias contenedores vs máquinas virtuales
La mejor manera de comprender qué es un contenedor es compararlo con las máquinas virtuales.
Una máquina virtual es un ordenador virtualizado donde ejecutar de manera aislada aplicaciones, podemos definirla a través de las siguientes características:
- Se virtualiza todo, incluido el hardware. Ejemplos de elementos que se virtualizan son el procesador, la memoria ram, el sistema operativo completo y la propia aplicación.
- Es necesario instalar el sistema operativo completo, ya sea manualmente o de manera automatizada. ¿Recuerdas aquellos días donde era necesario instalar a mano Windows y configurarlo completamente antes de tener lista tu máquina virtual?.
- Son lentas en el arranque puesto que tienen que simular e iniciar absolutamente todos los componentes de un ordenador.
- Ocupan mucho espacio ya que necesitan almacenar información de todo lo que simulan como, por ejemplo, el archivo de paginación, la memoria ram, el sistema operativo completo y el código de las aplicaciones.
- Consumen muchos recursos del ordenador en el que se alojan, por ejemplo, si tenemos una máquina virtual con 4GB de ram dentro de un ordenador con 12GB, el ordenador anfitrión únicamente podrá utilizar como máximo 8GB puesto que reserva 4GB para la máquina virtual aunque ésta no utilice toda esa ram.

Un contenedor es un cajón aislado donde ejecutar aplicaciones, algunas de sus características son:
- Utilizan el hardware y el kernel del sistema operativo anfitrión. Esto no significa que los contenedores no tengan sistema operativo, lo que no tienen es el kernel del sistema operativo porque utilizan el del sistema anfitrión.
Como recuerdo, el kernel del sistema operativo es la parte que se encarga de comunicar las aplicaciones con el hardware como, por ejemplo, reservar y consumir memoria ram, utilizar el procesador para realizar cálculos… etc. - Como consecuencia de lo anterior solo se virtualiza una parte pequeña de recursos que son la parte del sistema operativo que no forma parte del kernel, las dependencias de la aplicación y la propia aplicación.
Un ejemplo de ello sería un contenedor donde instalamos el sistema operativo Alpine, encima de éste instalamos .Net Core 2 y sobre éste hacemos funcionar nuestra aplicación. - No hay que instalar el sistema operativo completo y, la parte que se instala, se hace de manera totalmente automática.
- Son rápidos en el arranque porque no tienen que simular y arrancar muchos componentes, solo los estrictamente necesarios.
- No ocupan mucho espacio en comparación con las máquinas virtuales, mientras que un contenedor puede ocupar 200 MB, una máquina virtual de características similares puede llegar fácilmente a los 8GB.
- Consumen menos recursos del sistema anfitrión. Siguiendo el ejemplo de las máquinas virtuales, en un contenedor no indicamos cuánta memoria ram tiene y ésta no queda totalmente bloqueada en el sistema anfitrión, simplemente utiliza en cada momento la que necesita.

Finalmente cabe mencionar que, dado que los contenedores utilizan el kernel del sistema operativo anfitrión, éstos pueden ser de dos tipos, contenedores Linux y contenedores Windows. Obviamente un contenedor Linux solo podremos ejecutarlo en Linux y uno Windows en Windows.
Imagen vs contenedor Docker
Antes de definir una imagen en Docker es necesario saber las diferencias entre una aplicación y un proceso:
- Una aplicación no es más que el código de un programa almacenado en el disco duro de nuestro ordenador.
- Un proceso es la aplicación ejecutándose en nuestro ordenador. Además un proceso puede estar en estado activo, pausado o parado completamente.
Una vez que conocemos estos conceptos es sencillo definir qué es una imagen en Docker, es el código de nuestra aplicación junto con el código de sus dependencias almacenado en algún sitio. Siguiendo el ejemplo anterior, las diferencias entre una imagen y un contenedor en Docker son:
- Una imagen es el código de un programa junto con sus dependencias almacenado en cualquier sitio, por ejemplo, en la nube o en el disco duro de nuestro ordenador.
- Un contenedor es la imagen ejecutándose en un entorno aislado o cajón. De la misma manera que un proceso, un contenedor puede estar en estado activo, pausado o parado.
¿Qué son las capas de una imagen?
¿Recuerdas que hemos dicho que una imagen es el código de una aplicación junto con sus dependencias?. Pues bien, en esa definición va explícito que una imagen es un conjunto de capas, es decir, está formada por varias partes.
Supongamos que tenemos un API escrita en .NET, podemos decir que un ejemplo de capas de imagen en Docker es:
- Sistema operativo, necesitamos una base sobre la que nuestra aplicación .NET funcione, por ejemplo, Ubuntu.
Recuerda que el núcleo del sistema operativo no pertenece a la imagen ni al contenedor, sino que se utiliza el del sistema operativo anfitrión. - Entorno de .NET, también necesitamos el ecosistema necesario para que nuestra aplicación se ejecute, en este caso podría ser .NET 8 para Linux porque la API la tenemos en .NET Core.
- Dependencias de la aplicación, supongamos que nuestra API tiene un método que accede a base de datos y genera un Excel que envía un email. Pues bien, necesitaremos instalar las librerías para acceder a la base de datos (Entity Framework Core), para crear el Excel (Epplus) y para mandar el email (System.Net.Mail).
- Código de la aplicación, finalmente, después de todo lo necesario, tendremos el código de nuestro programa escrito, por ejemplo, en C#.

Repositorios de imágenes
De la misma manera que el código de una aplicación lo podemos almacenar en un repositorio como Github, una imagen también podemos almacenarla en repositorios de imágenes como Docker Hub. Al igual que en GitHub, los repositorios pueden ser públicos o privados.
Algunos ejemplos útiles de repositorios públicos en Docker Hub pueden ser la imagen de WordPress y la imagen de SQL Server.
Ahora bien, te estarás preguntando, ¿Para qué necesito un repositorio de imágenes como Docker Hub?. Pues bien, supongamos que quieres instalar WordPress en local, tienes dos opciones:
- Instalación manual. Esto requiere, entre otras cosas, instalar PHP en tu ordenador, instalar y configurar un servidor web como Apache, alojar el código de WordPress en Apache, realizar las mil configuraciones necesarias para que funcione y finalmente ejecutarlo.
Algunos problemas que puedes encontrar es que la versión de PHP de tu ordenador no es la correcta para la versión de WordPress que quieres o que, tras configurar muchas cosas en tu equipo, no consigas que tu instalación de WordPress funcione. - Instalación a través de un contenedor Docker. Lo único que necesitarás serán un par de comandos para descargar la imagen de WordPress y ejecutarla en un contenedor, todas las dependencias como Apache o PHP ya vendrán incluidas y no tendrás conflictos entre versiones.
Contenedores y volúmenes en Docker
Una de las características que definen a los contenedores es que no tienen un almacenamiento persistente, es decir, cuando un contenedor se elimina y es creado de nuevo toda la información que se haya generado en el antiguo desaparece.
Supongamos que tenemos un contenedor donde tenemos un API que almacena datos en una base de datos local, pues bien, en el momento en que eliminamos y creamos de nuevo el contenedor del API la base de datos se borrará y, con ella, todos los datos que hayamos introducido.
Es por este motivo que surgen los volúmenes en Docker, un concepto que permite que uno o varios contenedores compartan el almacenamiento del sistema anfitrión para hacer que los datos sean persistentes entre ellos.
Siguiendo el ejemplo anterior, si hacemos que la base de datos del API se almacene en un volumen no tendremos el problema de que se borren los datos cuando recreamos el contenedor, teniéndolos, de esta manera, disponibles para cualquier contenedor de nuestro sistema.

Un apunte importante a tener en cuenta es que el almacenamiento de un contenedor no se elimina cuando éste se reinicia, solo cuando lo eliminamos y creamos uno nuevo exactamente igual. Comprobaremos en la práctica este punto para que veáis las diferencias.
Contenedores y variables de configuración
Es importante tener claro que una imagen se ejecuta en un contenedor y, para ello, es necesario proporcionarle, en la mayor parte de los casos, variables de configuración.
Seguiremos el ejemplo anterior donde tenemos una imagen de un API en .NET que se conecta a una base de datos, pero esta vez no es local sino externa. Además queremos ejecutarla en dos contenedores distintos, uno para desarrollo y otro para producción. Obviamente la cadena de conexión de base de datos será distinta para ambos entornos.
Pues bien, las variables de configuración se utilizan para el escenario anterior, es decir, permiten que una misma imagen tenga comportamientos distintos al ejecutarse en contenedores diferentes.
En el ejemplo anterior, a la hora de ejecutar cada contenedor, a través de las variables de configuración, le pasaremos distintas cadenas de conexión para que cada API apunte a una base de datos distinta.
Conjuntos de contenedores o paquetes en Docker
¿Qué es un conjunto de contenedores o paquetes?
Un conjunto de contenedores o paquetes en Docker no es más que un grupo de contenedores que tienen algo en común entre sí, que de una manera u otra están relacionados entre ellos.
Ten en cuenta que este concepto no existe realmente en Docker pero creo que es necesario para entender otros conceptos más avanzados.
Siguiendo el ejemplo anterior donde teníamos un API en .NET, podemos ampliarlo de tal manera que ahora tengamos tres contenedores:
- Aplicación cliente con MVC .NET que accede al API.
- API en .NET Core 3.
- Base de datos SQL Server a la que accede el API para consultar y modificar datos.
Lo anterior podemos verlo como un paquete de contenedores porque cumple con las siguientes características:
- Forman parte del mismo proyecto o solución.
- Son dependientes entre sí ya que, si uno de ellos faltase, los otros no funcionarían o no tendrían mucho sentido.
- Necesitan comunicarse entre ellos, como mínimo la aplicación debe comunicarse con el API y, a su vez, el API con la base de datos.
- Se despliegan en un orden determinado, primero deberíamos desplegar la base de datos, después el API y, finalmente, la aplicación cliente.
Si alteramos este orden es posible que, por ejemplo, la aplicación llame al API sin que ésta esté levantada y, como consecuencia, se lance un error al usuario.

Redes y puertos en Docker
Una de las características de los paquetes de contenedores es que los contenedores necesitan comunicarse entre sí pero… ¿Cómo lo hacen?, pues bien, la respuesta es sencilla, a través de las redes y puertos de Docker.
Por defecto, un contenedor es un espacio aislado donde ejecutar una imagen, esto significa que, por defecto, un contenedor no es accesible ni se puede comunicar con nadie.
Por una parte tenemos las redes Docker que, simplemente, son un grupo de contenedores que, de manera interna, se pueden comunicar entre sí.
Esto significa que una red Docker no permite el acceso desde fuera, simplemente permite la comunicación interna de los contenedores que están dentro de ella.
Siguiendo el ejemplo anterior, tenemos los siguientes requisitos:
- La aplicación cliente debe poder comunicarse con el API.
- El API debe poder comunicarse con la base de datos.
- Ninguna otra comunicación está permitida, por ejemplo, la aplicación cliente no puede comunicarse con la base de datos directamente.
La forma de satisfacer estos requisitos es creando dos redes de Docker:
- Red 1, donde estará la aplicación cliente y el API.
- Red 2, donde estará el API y la base de datos.

Llegados a este punto te preguntarás… Si los contenedores no son accesibles por defecto desde fuera… ¿Cómo puede el usuario acceder a la aplicación cliente?. Pues bien, para eso existen los puertos en Docker, para permitir la comunicación tanto desde fuera como desde dentro:
- Puertos privados, permiten la comunicación entre contenedores.
- Puertos públicos, permiten la comunicación entre un contenedor y el exterior.

Como podemos observar, siguiendo el ejemplo anterior, si quisiéramos permitir el acceso al contenedor de la aplicación deberíamos utilizar puertos y, más concretamente, mapearlos, de tal manera que le indiquemos a Docker “El puerto 8080 el contenedor de la aplicación se corresponde con el puerto 80 del sistema anfitrión”.
Esto nos permitirá que, si el sistema anfitrión es un servidor accesible desde Internet, cualquier usuario se pueda conectar a la aplicación a través del puerto 80 que, a su vez, apunta al contenedor que está ejecutando nuestra aplicación cliente.
Dependencias entre contenedores
Las dependencias entre contenedores permiten satisfacer el punto mencionado anteriormente, es decir, que a la hora de ejecutarse dentro de un grupo, deben seguir un orden determinado.
Siguiendo el ejemplo anterior podemos observar que:
- El contenedor de la aplicación cliente depende únicamente del contenedor del API.
- El contenedor del API depende únicamente del contenedor de base de datos.
- El contenedor de base de datos no depende de nadie.
- El contenedor de la aplicación no depende del contenedor de base de datos directamente y viceversa.

Dockerfile y docker-compose.yml
¿Para qué sirven Dockerfile y docker-compose.yml?
En Docker existen dos formas de crear imágenes y ejecutarlas en contenedores:
- A través de comandos, para lo cual necesitamos instalar un programa en nuestro equipo y, a través de su terminal, ejecutar los comandos necesarios.
- A través de ficheros que contengan las instrucciones necesarias para crear las imágenes y ejecutarlas dentro de contenedores.
Pues bien, Dockerfile y docker-compose.yml sirven para generar imágenes y ejecutarlas en contenedores a través de ficheros, es decir, la segunda opción.
Sin estos ficheros estaríamos obligados a almacenar en algún sitio todos los comandos para que, cada vez que creamos o ejecutamos una imagen en un contenedor, los escribamos a mano a través de la terminal.
Podemos verlo como un fichero .bat en Windows o un .sh en Linux, es decir, son ficheros que almacenan un conjunto de comandos y que podemos ejecutar de una sola vez con una única instrucción.
Finalmente cabe destacar que estos ficheros se encuentran, generalmente, en la raíz del proyecto correspondiente para poder crear sus imágenes y contenedores.
Diferencias Dockerfile vs docker-compose.yml
Quizás te preguntes, ¿Cuál es la diferencia entre Dockerfile y docker-compose.yml?:
- Dockerfile permite definir las instrucciones necesarias para crear una imagen.
Un ejemplo de Dockerfile sería indicar que queremos que nuestra api se ejecute sobre Ubuntu, utilice la versión .NET Core 3, que tiene X dependencias y las instrucciones necesarias para compilar y publicar. - docker-compose.yml permite definir las instrucciones necesarias para crear un conjunto de contenedores con todo lo que hemos visto anteriormente como, por ejemplo, redes, dependencias, puertos y variables de configuración.
Un ejemplo de docker-compose.yml sería indicar que tenemos tres contenedores donde la aplicación cliente depende del API y el API de la base de datos. También podríamos indicar el usuario y contraseña de la base de datos.
Cómo instalar y probar Docker en local
A lo largo de los siguientes tutoriales de este curso de Docker vamos a pasar a la práctica y, para ello, necesitamos un programa que nos permita crear imágenes y contenedores Docker.
Pues bien, el programa que nos permite instalar y probar Docker en local es Docker desktop, el cual puedes descargar desde el enlace anterior.
Ten en cuenta los dos siguientes puntos:
- Debes seleccionar la versión correspondiente a tu sistema operativo.
- Lee los requisitos e instrucciones del sistema operativo en cuestión porque, si no cumples con alguno de ellos, es posible que comiencen a suceder cosas extrañas cuando estés ejecutando comandos.
Una vez que tengas Docker desktop descargado e instalado en tu ordenador abre la terminal a través del icono que aparece en la parte inferior derecha y, en ella, introduce el comando docker version:

Si todo ha ido bien durante el proceso de instalación te aparecerá la información completa de la versión de Docker instalada en tu equipo tal y como muestra la imagen.
¿Qué es Kubernetes?
Quizás hayas escuchado este concepto cada vez que se habla de Docker… Pues bien, te adelanto que esto no forma parte de este curso de Docker y que lo veremos más adelante en el blog. No obstante creo que es necesario verlo y entenderlo superficialmente para tener una noción básica sobre él y evitar confusiones.
Kubernetes es un sistema que permite ejecutar, distribuir y replicar contenedores en distintos servidores conocidos como “Pods”.
Algunas de las ventajas que ofrece Kubernetes son:
- Alta disponibilidad ya que un contenedor puede estar replicado en N servidores.
- Escalabilidad, si un contenedor recibe muchas peticiones podemos escalarlo y replicarlo en distintos servidores para así disminuir la carga de cada uno de ellos.
- Balanceo de carga, derivado del punto anterior, al tener el mismo contenedor en múltiples servidores la carga entre cada una de esas copias está balanceada.
- Optimización de recursos, costes e infraestructura, de manera similar a Azure, Kubernetes permite ampliar o reducir de manera dinámica los recursos (servidores) asignados a cada contenedor, esta técnica es más conocida como escalado automático horizontal.
Ejemplo práctico de Docker y esquema general
Los siguientes tutoriales prácticos de Docker se basan en un repositorio compuesto por tres proyectos .NET:
- Aplicación MVC .NET que accede a un API para consultar e insertar empleados.
- API de .NET que realiza sus operaciones sobre una base de datos SQL Server.
- Base de datos SQL Server sobre la que el API realiza las operaciones.
En dicho repositorio existen dos ramas, “WithoutDocker” que contiene los tres proyectos sin Docker y “WithDocker” que contiene el resultado final de implementar Docker en la práctica.
El esquema general de Docker que vamos a implementar está compuesto por los siguientes puntos:
- Tres imágenes, una para cada proyecto. A su vez, cada imagen tendrá N capas.
- Tres contenedores, uno para cada una de las imágenes creadas anteriormente.
- Un volumen para permitir el almacenamiento persistente de la base de datos en nuestro equipo anfitrión.
- Las variables de configuración necesarias como, por ejemplo, el usuario y contraseña de la base de datos.
- Un conjunto de contenedores o paquete donde introduciremos nuestros tres contenedores.
- Dos redes que permitirán la comunicación, por una parte, entre la aplicación MVC y el API y, por otra parte, entre el API y la base de datos.
- Un mapeo de puertos que nos permitirá acceder a la aplicación cliente MVC de .NET desde el exterior.
- Dependencias que nos permitirán definir el orden de ejecución de los contenedores, es decir, el API depende de la base de datos y la aplicación cliente del API.
- Fichero Dockerfile en cada proyecto para crear las imágenes de cada uno de ellos.
- Fichero docker-compose.yml para configurar todo lo anterior y crear nuestro grupo de contenedores.

Quizás algo no te haya quedado del todo claro. Pese a que mi recomendación es que intentes comprender todo lo que hay en este tutorial, ten en cuenta que más adelante en el curso de Docker veremos en la práctica todos estos conceptos.




