Empezando con Docker: imágenes y contenedores

En el vertiginoso mundo del desarrollo de software, la promesa de que una aplicación "funcione en cualquier lugar" es a menudo más una quimera que una realidad. ¿Cuántas veces nos hemos topado con el frustrante escenario de una aplicación que corre perfectamente en nuestro entorno de desarrollo, solo para fallar miserablemente al desplegarla en producción o en la máquina de un compañero? Este problema, conocido coloquialmente como "funciona en mi máquina", ha sido una constante fuente de dolores de cabeza para equipos de desarrollo y operaciones durante décadas. Afortunadamente, ha surgido una tecnología que no solo aborda este desafío de frente, sino que también ha revolucionado la forma en que construimos, distribuimos y ejecutamos nuestras aplicaciones: Docker.

Docker ha transformado el panorama tecnológico al introducir un nuevo paradigma para la contenerización, ofreciendo una solución robusta y eficiente para garantizar la consistencia en todos los entornos. Si eres nuevo en este ecosistema o simplemente buscas consolidar tus conocimientos, este post te servirá como una guía introductoria a sus conceptos fundamentales: las imágenes y los contenedores. Comprender estos dos pilares es el primer paso indispensable para dominar Docker y aprovechar todo su potencial.

Qué es Docker y por qué es relevante

Bright purple neon sign with Spanish phrase 'Yo te quiero con limón y sal...' on dark background.

Docker es una plataforma de código abierto que automatiza el despliegue de aplicaciones dentro de contenedores de software. Un contenedor es una unidad estandarizada de software que empaqueta todo lo necesario para que una pieza de software funcione, incluyendo el código, un tiempo de ejecución, bibliotecas, variables de entorno y archivos de configuración. La magia de Docker reside en su capacidad para aislar las aplicaciones del entorno subyacente, garantizando que funcionen de manera consistente, independientemente de dónde se ejecuten.

La relevancia de Docker en la industria actual es innegable. Sus ventajas clave lo han posicionado como una herramienta esencial para desarrolladores, equipos de DevOps y arquitectos de sistemas:

  • Aislamiento: Cada contenedor es un proceso aislado que no interfiere con otros contenedores o con el sistema operativo anfitrión. Esto previene conflictos entre dependencias de distintas aplicaciones.
  • Portabilidad: Una imagen Docker construida una vez puede ejecutarse en cualquier sistema que tenga instalado Docker, ya sea un portátil de desarrollo, un servidor local o un proveedor de nube. Esto soluciona de raíz el problema del "funciona en mi máquina".
  • Consistencia: El entorno de ejecución de una aplicación es idéntico en desarrollo, pruebas y producción, lo que reduce drásticamente los errores relacionados con diferencias ambientales.
  • Eficiencia: Los contenedores son mucho más ligeros y rápidos de iniciar que las máquinas virtuales (VMs). Mientras que una VM virtualiza el hardware completo y ejecuta un sistema operativo invitado completo, un contenedor comparte el kernel del sistema operativo del host y solo empaqueta la aplicación y sus dependencias. Esto se traduce en un menor consumo de recursos y tiempos de arranque casi instantáneos.
  • Desarrollo ágil: Permite a los desarrolladores trabajar en entornos que replican fielmente la producción, facilitando la integración continua y la entrega continua (CI/CD).
  • Escalabilidad: Las aplicaciones contenerizadas son más fáciles de escalar horizontalmente, ya que se pueden lanzar nuevas instancias de contenedores rápidamente para manejar picos de demanda.

Desde mi perspectiva, la aparición de Docker no fue solo una evolución, sino una auténtica revolución en la forma en que abordamos el ciclo de vida del software. Ha democratizado el acceso a técnicas de despliegue que antes eran complejas y ha simplificado la colaboración entre equipos, permitiendo que la energía se concentre más en la innovación y menos en la infraestructura.

Comprendiendo los pilares: imágenes y contenedores

Para sumergirse en Docker, es fundamental tener una comprensión clara de sus dos conceptos centrales: las imágenes y los contenedores. Aunque están intrínsecamente relacionados, cada uno cumple una función distinta y complementaria.

Las imágenes: el plano inmutable

Una imagen Docker es una plantilla de solo lectura que contiene un conjunto de instrucciones para crear un contenedor. Piensa en ella como un "plano" o una "receta" completa que define un entorno de aplicación. Una imagen incluye el sistema de archivos de la aplicación, el tiempo de ejecución, las variables de entorno, las bibliotecas y el código binario.

Las características clave de las imágenes son:

  • Inmutabilidad: Una vez creada, una imagen no puede ser modificada. Cualquier cambio resulta en la creación de una nueva imagen. Esta propiedad garantiza la consistencia: si todos usan la misma imagen, todos obtendrán el mismo entorno.
  • Capas: Las imágenes se construyen en capas, lo que permite una increíble eficiencia en el almacenamiento y la transferencia. Cuando construyes una imagen, cada instrucción en tu Dockerfile (el archivo que define cómo construir una imagen) crea una nueva capa. Si varias imágenes comparten capas base (por ejemplo, la capa de Ubuntu), Docker solo necesita almacenar esa capa una vez. Esto acelera las descargas y reduce el espacio en disco.
  • Solo lectura: Como mencionamos, una imagen es inmutable y de solo lectura. Cuando un contenedor se inicia a partir de una imagen, se añade una capa de escritura "modificable" sobre esta, permitiendo que el contenedor realice cambios sin afectar la imagen base.

Las imágenes se suelen almacenar en registros (registries), siendo el más conocido Docker Hub. Este es un repositorio público donde puedes encontrar imágenes preconfiguradas para casi cualquier software que necesites: sistemas operativos (Ubuntu, Alpine), servidores web (Nginx, Apache), bases de datos (PostgreSQL, MySQL), runtimes de lenguajes (Node.js, Python), y mucho más. También puedes construir tus propias imágenes personalizadas y subirlas a Docker Hub (público o privado) o a otros registros como AWS ECR o Google Container Registry.

Los contenedores: instancias ejecutables

Un contenedor Docker es una instancia en ejecución de una imagen. Es la unidad donde tu aplicación cobra vida. Cuando ejecutas una imagen, Docker crea un contenedor que es un entorno aislado, ligero y efímero donde tu aplicación puede operar.

Las propiedades fundamentales de los contenedores son:

  • Aislado: Cada contenedor se ejecuta de forma independiente, con su propio sistema de archivos, procesos de red y espacio de nombres, aislado del host y de otros contenedores.
  • Efímero: Los contenedores están diseñados para ser desechables. Si un contenedor se detiene o se elimina, los cambios que se realizaron dentro de su capa de escritura (a menos que se hayan configurado volúmenes para persistencia de datos) se pierden. Esto fomenta una arquitectura de "servicios sin estado", donde los datos importantes se almacenan externamente.
  • Ligero: Al compartir el kernel del sistema operativo del host, los contenedores arrancan en cuestión de segundos y consumen significativamente menos recursos que una máquina virtual equivalente.
  • Basado en imagen: Un contenedor es siempre una instancia de una imagen. La imagen proporciona el estado inicial y el sistema de archivos base del contenedor. Puedes iniciar múltiples contenedores desde la misma imagen, y cada uno será una instancia separada y aislada.

La interacción entre imágenes y contenedores es fundamental: la imagen es el molde, el contenedor es el objeto creado a partir de ese molde. Una única imagen puede dar lugar a un número ilimitado de contenedores en ejecución, cada uno con su propio estado. Puedes explorar más sobre el ciclo de vida de los contenedores y su funcionamiento en la documentación oficial de Docker.

Trabajando con Docker: comandos esenciales

Dominar Docker implica familiarizarse con su línea de comandos (CLI). Aunque hay interfaces gráficas y herramientas de orquestación, la CLI sigue siendo la forma más potente y directa de interactuar con Docker. Aquí te presento algunos de los comandos más utilizados.

Gestionando imágenes

Estos comandos te permiten descargar, listar y eliminar imágenes de tu máquina local.

  • docker pull <nombre_imagen>[:tag]: Descarga una imagen del Docker Hub (o de otro registro configurado) a tu máquina local. El :tag es opcional y se refiere a la versión de la imagen (por ejemplo, nginx:latest, ubuntu:22.04). Si no se especifica, se descarga la versión latest.
    docker pull nginx:latest
    
  • docker images: Lista todas las imágenes que tienes almacenadas localmente. Muestra el repositorio, la etiqueta, el ID de la imagen, su fecha de creación y su tamaño.
    docker images
    
  • docker rmi <id_imagen_o_nombre>[:tag]: Elimina una o varias imágenes de tu almacenamiento local.
    docker rmi nginx:latest
    docker rmi abc123def456 # usando el ID de la imagen
    
    Mi opinión: Estos tres comandos son la puerta de entrada a cualquier proyecto Docker. Son el "ABC" para gestionar los componentes base de tus aplicaciones, y tenerlos claros te ahorrará muchos dolores de cabeza cuando necesites limpiar espacio o asegurar que estás usando la versión correcta de una dependencia.

Gestionando contenedores

Estos comandos son el corazón de la interacción con Docker, permitiéndote ejecutar, controlar y monitorear tus aplicaciones.

  • docker run [opciones] <nombre_imagen>[:tag] [comando] [argumentos]: Este es, probablemente, el comando más importante. Crea un nuevo contenedor a partir de una imagen y lo ejecuta.
    • -d (detach): Ejecuta el contenedor en segundo plano.
    • -p <puerto_host>:<puerto_contenedor>: Mapea un puerto del host a un puerto del contenedor. Es crucial para acceder a servicios dentro del contenedor desde fuera.
    • --name <nombre_contenedor>: Asigna un nombre personalizado al contenedor para facilitar su identificación.
    • -e <VARIABLE=valor>: Establece variables de entorno dentro del contenedor.
    • -v <ruta_host>:<ruta_contenedor>: Monta un volumen del host en el contenedor para persistencia de datos o compartir archivos.
    docker run -d -p 8080:80 --name mi-servidor-web nginx
    
    Puedes encontrar una referencia completa de las opciones de docker run en la documentación oficial.
  • docker ps: Lista los contenedores que están actualmente en ejecución.
    docker ps
    
  • docker ps -a: Lista todos los contenedores, tanto los que están en ejecución como los que están detenidos.
    docker ps -a
    
  • docker stop <id_o_nombre_contenedor>: Detiene uno o varios contenedores en ejecución.
    docker stop mi-servidor-web
    
  • docker start <id_o_nombre_contenedor>: Inicia uno o varios contenedores detenidos.
    docker start mi-servidor-web
    
  • docker restart <id_o_nombre_contenedor>: Reinicia uno o varios contenedores.
    docker restart mi-servidor-web
    
  • docker rm <id_o_nombre_contenedor>: Elimina uno o varios contenedores. Solo se pueden eliminar contenedores detenidos.
    docker rm mi-servidor-web
    
  • docker exec -it <id_o_nombre_contenedor> <comando>: Ejecuta un comando dentro de un contenedor en ejecución. Útil para depurar o acceder a la shell del contenedor.
    • -i: Mantiene la entrada estándar abierta.
    • -t: Asigna una pseudo-TTY.
    docker exec -it mi-servidor-web bash
    
  • docker logs <id_o_nombre_contenedor>: Muestra los logs de un contenedor.
    docker logs -f mi-servidor-web # El flag -f permite seguir los logs en tiempo real
    

Ejemplos prácticos y flujo de trabajo común

Para consolidar estos conceptos, veamos un par de ejemplos prácticos que ilustran cómo se utilizan las imágenes y los contenedores en un flujo de trabajo típico.

Desplegando una aplicación web simple

Imaginemos que queremos poner en marcha un servidor web Nginx para servir contenido estático.

  1. Obtener la imagen Nginx:

    docker pull nginx:latest
    

    Esto descargará la imagen oficial de Nginx desde Docker Hub a tu máquina.

  2. Ejecutar Nginx y mapear puertos:

    docker run -d -p 8080:80 --name mi-web-server nginx
    

    Aquí estamos:

    • docker run: Ejecutando un nuevo contenedor.
    • -d: En modo "detached", es decir, en segundo plano.
    • -p 8080:80: Mapeando el puerto 8080 de nuestro host al puerto 80 del contenedor (donde Nginx escucha por defecto).
    • --name mi-web-server: Dándole un nombre fácil de recordar al contenedor.
    • nginx: Indicando que use la imagen Nginx.
  3. Verificar en el navegador: Ahora puedes abrir tu navegador y visitar http://localhost:8080. Deberías ver la página de bienvenida predeterminada de Nginx. ¡Acabas de desplegar una aplicación web en un contenedor en cuestión de segundos!

  4. Limpiar:

    docker stop mi-web-server
    docker rm mi-web-server
    

    Con estos comandos, detienes y luego eliminas el contenedor.

Usando una base de datos con Docker

Otro caso de uso muy común es ejecutar bases de datos en contenedores para el desarrollo local. Esto evita tener que instalar y configurar la base de datos directamente en tu sistema operativo.

  1. Obtener la imagen de PostgreSQL:

    docker pull postgres:14
    
  2. Ejecutar PostgreSQL con variables de entorno:

    docker run -d \
      --name mi-db-postgres \
      -e POSTGRES_PASSWORD=mysecretpassword \
      -p 5432:5432 \
      postgres:14
    

    Aquí, hemos introducido:

    • -e POSTGRES_PASSWORD=mysecretpassword: Estableciendo la variable de entorno necesaria para la contraseña del usuario postgres dentro del contenedor.
    • -p 5432:5432: Mapeando el puerto predeterminado de PostgreSQL del host al contenedor.
  3. Conectarse a la base de datos: Ahora puedes usar cualquier cliente de PostgreSQL (pgAdmin, DBeaver, o desde tu código) y conectarte a localhost:5432 con el usuario postgres y la contraseña mysecretpassword. La base de datos está completamente contenida y aislada.

Este flujo de trabajo es muy potente y flexible. Puedes encontrar más tutoriales que integran varias capas, como una aplicación Node.js con una base de datos MongoDB, para construir un stack completo en la guía de inicio rápido de Docker.

Más allá de lo básico: lo que sigue

Este post es solo el punto de partida en tu viaje con Docker. Una vez que te sientas cómodo con las imágenes y los contenedores, hay muchos otros conceptos clave que te permitirán explotar al máximo el potencial de esta tecnología.

Dockerfile: construyendo tus propias imágenes

Para pasar de usar imágenes preexistentes a crear tus propias aplicaciones contenerizadas, necesitarás aprender sobre los Dockerfiles. Un Dockerfile es un simple archivo de texto que contiene una serie de instrucciones para construir una imagen Docker personalizada. Es la clave para empaquetar tu propia aplicación y sus dependencias de manera reproducible. Dominar los Dockerfiles es esencial para cualquier desarrollador que trabaje con Docker.

Docker Compose: orquestación de múltiples servicios

Las aplicaciones modernas a menudo constan de varios servicios interconectados (un frontend, un backend, una base de datos, un caché, etc.). Gestionar estos servicios individualmente con docker run puede volverse tedioso y propenso a errores. Aquí es donde entra en juego Docker Compose. Docker Compose es una herramienta para definir y ejecutar aplicaciones Docker de varios contenedores. Con un único archivo YAML, puedes configurar todos los servicios de tu aplicación, sus redes y volúmenes, y luego lanzarlos y gestionarlos con un solo comando (docker compose up). Es invaluable para entornos de desarrollo y staging.

Volúmenes: persistencia de datos

Dado que los contenedores son efímeros, cualquier dato guardado dentro de la capa de escritura del contenedor se perderá si el contenedor se elimina. Para la persistencia de datos (como bases de datos, archivos subidos por usuarios, etc.), Docker ofrece los volúmenes. Los volúmenes son la forma preferida de persistir datos generados por y usados por contenedores Docker. Permiten que los datos sobrevivan a la vida de un contenedor y se pueden compartir entre contenedores.

Redes de Docker

Para que tus contenedores se comuniquen entre sí y con el mundo exterior, es crucial entender el concepto de las redes de Docker. Docker proporciona varios modos de red que permiten a los contenedores aislarse o interactuar de manera segura y eficiente. Comprender cómo funcionan las redes te permitirá construir arquitecturas de microservicios robustas.

Conclusión

Empezar con Docker puede parecer abrumador al principio, pero una vez que comprendes los conceptos fundamentales de imágenes y contenedores, todo comienza a encajar. Las imágenes son las plantillas inmutables que encapsulan tu aplicación y su entorno, mientras

Diario Tecnología