🐳 Creando un contenedor desde cero
En esta clase damos el primer paso para crear nuestros propios contenedores.
Hasta ahora habíamos utilizado imágenes ya existentes, pero ahora aprenderemos a escribir nuestro propio Dockerfile, entendiendo qué hace cada instrucción y cómo Docker construye una imagen paso a paso.
📄 ¿Qué es un Dockerfile?
Un Dockerfile es una receta que indica a Docker cómo construir una imagen.
Cada instrucción genera una nueva capa que Docker puede reutilizar posteriormente gracias a su sistema de caché.
La idea es ir construyendo el entorno paso a paso.
🏔️ Elegir una imagen base
Todo Dockerfile comienza con una imagen base utilizando la instrucción FROM.
En el ejemplo más sencillo se utiliza:
FROM alpine
Alpine Linux es una de las imágenes más utilizadas porque:
- ocupa apenas unos pocos megabytes
- es extremadamente ligera
- sirve como base para instalar cualquier otra herramienta
- permite construir imágenes muy rápidas
También es posible fijar una versión concreta:
FROM alpine:3.22.4
o utilizar imágenes como Node especificando exactamente la versión deseada.
▶️ Definir el comando de inicio
Después de elegir la imagen base podemos indicar qué comando debe ejecutarse cuando el contenedor arranque.
Esto se hace mediante CMD.
CMD ["echo", "Hola Docker"]
Es importante recordar que:
- solo puede existir un comando de inicio efectivo
- si se definen varios
CMD, únicamente tendrá efecto el último
Docker sobrescribe los anteriores.
🔨 Construyendo la imagen
Una vez creado el Dockerfile, la imagen se construye mediante:
docker build -t hola-docker .
El punto (.) indica el contexto de construcción, es decir, la carpeta actual donde se encuentra el Dockerfile.
Después podremos ejecutarla utilizando:
docker run hola-docker
Con ello obtendremos el resultado del comando definido dentro del contenedor.
🚀 Un ejemplo real con Node.js
Tras el ejemplo mínimo, la clase pasa a construir un contenedor para una pequeña API creada con Express.
La aplicación:
- escucha por defecto en el puerto 3000
- permite configurar el puerto mediante variables de entorno
- permite modificar el mensaje de saludo
- expone un endpoint principal y otro de health check
- registra información en consola para facilitar el seguimiento de la aplicación.
📁 Establecer un directorio de trabajo
Antes de copiar archivos al contenedor se define un directorio de trabajo.
WORKDIR /app
Esto evita trabajar directamente sobre la raíz del sistema operativo del contenedor y reduce posibles conflictos con carpetas existentes.
Es una buena práctica prácticamente en cualquier Dockerfile.
📦 Copiar primero los manifiestos
Una decisión muy importante consiste en copiar primero únicamente:
package.jsonpackage-lock.json
Después se instalan las dependencias:
RUN npm ci
Solo una vez instaladas se copia el resto del proyecto.
COPY . .
Este orden no es casual.
Está pensado para aprovechar el sistema de caché de Docker y evitar reinstalar todas las dependencias cuando únicamente cambia el código fuente.
⚡ Instalar solo lo necesario
Durante la instalación también se comenta que, en proyectos reales, suele ser recomendable instalar únicamente las dependencias necesarias para producción.
Así:
- la imagen pesa menos
- el proceso de construcción es más rápido
- se reducen herramientas innecesarias dentro del contenedor.
🌐 Documentar el puerto utilizado
La aplicación utiliza el puerto 3000.
Para indicárselo a Docker se utiliza:
EXPOSE 3000
Esta instrucción documenta el puerto que utilizará la aplicación dentro del contenedor, aunque por sí sola no publica dicho puerto hacia el exterior.
👤 Ejecutar la aplicación con un usuario sin privilegios
Otra buena práctica consiste en evitar ejecutar la aplicación como usuario root.
En este caso la imagen oficial de Node ya incorpora un usuario llamado node.
Por ello se utiliza:
USER node
Con esto el contenedor funciona con menos privilegios, mejorando la seguridad.
▶️ Arrancar la aplicación
Finalmente se define el comando de inicio:
CMD ["node", "server.js"]
Con ello el contenedor iniciará automáticamente el servidor cuando sea ejecutado.
⚡ Cómo funciona la caché de Docker
Una vez construida la imagen por primera vez, al volver a ejecutar el mismo docker build Docker detecta que muchas capas no han cambiado.
En consecuencia:
- no vuelve a descargar la imagen base
- reutiliza las capas anteriores
- el proceso de construcción es muchísimo más rápido
Este comportamiento es una de las claves para construir imágenes eficientes.
🔌 Exponer un puerto no significa publicarlo
Uno de los errores más habituales es pensar que:
EXPOSE 3000
permite acceder automáticamente desde el navegador.
No es así.
Ese puerto existe únicamente dentro del contenedor.
Para hacerlo accesible desde la máquina anfitriona hay que publicar el puerto durante la ejecución:
docker run -p 5002:3000 nombre-imagen
En este ejemplo:
5002corresponde al puerto de nuestra máquina3000corresponde al puerto interno del contenedor
Gracias a este mapeo podremos acceder a la aplicación desde:
http://localhost:5002
aunque dentro del contenedor siga escuchando en el puerto 3000.
📌 Ideas clave de esta clase
Quédate con estos conceptos:
- Un Dockerfile describe cómo construir una imagen.
- Toda imagen parte de una instrucción
FROM. - Alpine es una imagen base ligera y muy utilizada.
CMDdefine el comando de inicio del contenedor.WORKDIRevita trabajar sobre la raíz del sistema.- Copiar primero los manifiestos mejora el aprovechamiento de la caché.
RUN npm ciinstala las dependencias durante la construcción.COPYincorpora el código fuente al contenedor.EXPOSEdocumenta el puerto interno de la aplicación.USER nodemejora la seguridad del contenedor.- Docker reutiliza automáticamente las capas cacheadas.
- Para acceder desde el host es necesario publicar los puertos mediante
-p.
🚀 Lo siguiente: optimizar el Dockerfile
Ahora que ya sabemos construir un contenedor completamente desde cero, el siguiente paso será comprender en profundidad cómo funciona la caché de Docker y cómo organizar las instrucciones del Dockerfile para conseguir construcciones mucho más rápidas y eficientes.
💡 Tip: El orden de las instrucciones dentro de un Dockerfile tiene un impacto enorme en el rendimiento. Una buena organización puede ahorrar minutos en cada construcción durante el desarrollo.