Docker para Desarrolladores: Guia Practica desde Cero
Todo lo que necesitas saber sobre Docker como desarrollador: desde los conceptos básicos hasta Docker Compose y mejores prácticas.
¿Qué es Docker y por qué deberías usarlo?
Docker es una plataforma de contenedorización que permite empaquetar una aplicación con todas sus dependencias (runtime, librerías, variables de entorno, archivos de configuración) en una unidad estándar llamada contenedor. A diferencia de las máquinas virtuales, los contenedores comparten el kernel del sistema operativo anfitrión, lo que los hace extremadamente ligeros y rápidos de iniciar.
El problema que Docker resuelve es universal: "en mi máquina funciona". Cuando empaquetas tu aplicación en un contenedor Docker, garantizas que se ejecutará exactamente igual en cualquier entorno: tu portátil, el servidor de staging, la máquina de tu compañero de equipo o la nube. El contenedor incluye todo lo necesario para funcionar, eliminando las inconsistencias entre entornos que causan tantos bugs y pérdidas de tiempo.
Para un desarrollador, Docker se ha convertido en una herramienta esencial del día a día, comparable a Git o a tu editor de código. No necesitas ser experto en DevOps para beneficiarte de Docker; basta con entender los conceptos fundamentales y saber escribir un Dockerfile decente.
Conceptos fundamentales
Imágenes vs Contenedores
Una imagen es una plantilla de solo lectura que contiene el sistema de archivos, las dependencias y las instrucciones necesarias para ejecutar una aplicación. Piensa en ella como una "foto" o un "molde" de tu aplicación en un momento dado. Las imágenes son inmutables: una vez construidas, no cambian.
Un contenedor es una instancia ejecutable de una imagen. Es como una máquina virtual ligera que corre tu aplicación de forma aislada. Puedes crear múltiples contenedores a partir de la misma imagen, cada uno con su propio estado y ciclo de vida. Los contenedores son efímeros: se crean, ejecutan y destruyen fácilmente.
La analogía clásica es: la imagen es la clase (en programación orientada a objetos) y el contenedor es la instancia de esa clase. O la imagen es la receta y el contenedor es el plato preparado.
Registros y Docker Hub
Un registro es un almacén de imágenes Docker. Docker Hub es el registro público por defecto, donde puedes encontrar imágenes oficiales de prácticamente cualquier software: Node.js, Python, PostgreSQL, Redis, Nginx, etc. También existen registros privados como GitHub Container Registry, AWS ECR y Google Artifact Registry para imágenes de tu propia organización.
Volúmenes y bind mounts
Los contenedores son efímeros: cuando se destruyen, todos los datos dentro se pierden. Los volúmenes son la solución de Docker para persistencia de datos: son directorios gestionados por Docker que sobreviven al ciclo de vida de los contenedores. Los bind mounts mapean un directorio del host directamente al contenedor, ideal para desarrollo donde quieres que los cambios en tu código se reflejen instantáneamente.
Tu primer Dockerfile
Un Dockerfile es un archivo de texto con instrucciones que Docker usa para construir una imagen. Cada instrucción crea una capa en la imagen, y Docker cachea estas capas para acelerar construcciones posteriores.
Veamos un Dockerfile para una aplicación Node.js:
FROM node:20-alpine - Usa la imagen oficial de Node.js 20 basada en Alpine Linux, una distribución minimalista que produce imágenes más pequeñas y con menor superficie de ataque.
WORKDIR /app - Establece el directorio de trabajo dentro del contenedor. Todas las instrucciones posteriores se ejecutarán desde este directorio.
COPY package*.json ./ - Copia los archivos de dependencias primero. Esto aprovecha el cache de Docker: si las dependencias no cambian, esta capa se reutiliza y no necesitas reinstalarlas.
RUN npm ci --only=production - Instala las dependencias de producción. npm ci es más rápido y determinista que npm install, ideal para CI/CD.
COPY . . - Copia el resto del código fuente. Esta capa se invalida con cada cambio en el código, pero como las dependencias ya están instaladas en una capa anterior, la reconstrucción es rápida.
EXPOSE 3000 - Documenta el puerto que la aplicación usa. Nota: esto es solo documentación; el mapeo real de puertos se hace al ejecutar el contenedor.
CMD ["node", "server.js"] - El comando que se ejecuta cuando el contenedor arranca. Usa el formato exec (array JSON) para que el proceso reciba señales del sistema correctamente.
Docker Compose: Orquestando múltiples servicios
La mayoría de aplicaciones modernas no son un solo servicio. Tu stack típico incluye una API, una base de datos, una caché Redis, quizás un servicio de colas y un servidor web. Docker Compose permite definir y gestionar todos estos servicios en un único archivo YAML.
Un archivo docker-compose.yml para un stack Node.js + PostgreSQL + Redis incluiría tres servicios, cada uno con su imagen, puertos, variables de entorno y volúmenes. Con un solo comando docker compose up, todos los servicios arrancan, se conectan entre sí mediante una red interna de Docker, y están listos para desarrollo.
Docker Compose es especialmente valioso para onboarding de nuevos desarrolladores. En lugar de seguir una guía de setup de 20 pasos para instalar PostgreSQL, Redis, configurar variables de entorno y seed de datos, el nuevo desarrollador solo necesita Docker instalado y ejecutar docker compose up. En minutos tiene todo el stack funcionando localmente.
Los conceptos clave de Docker Compose son: services (los contenedores que componen tu aplicación), networks (redes virtuales que conectan servicios), volumes (almacenamiento persistente) y depends_on (orden de arranque entre servicios).
Mejores prácticas para Dockerfiles
Usa imágenes base pequeñas
Las imágenes Alpine (basadas en Alpine Linux, ~5MB) son significativamente más pequeñas que las imágenes Debian/Ubuntu completas. Imágenes más pequeñas significan descargas más rápidas, menos espacio en disco y menor superficie de ataque. Para Node.js, Python y Go, las variantes Alpine son la opción por defecto recomendada.
Para Go, puedes ir aún más lejos con imágenes scratch (vacías) o distroless de Google, que contienen solo el binario compilado sin sistema operativo. El resultado son imágenes de 10-20MB que contienen únicamente tu aplicación.
Aprovecha el cache de capas
Docker cachea cada capa del Dockerfile. Cuando reconstruyes, solo las capas que han cambiado (y las posteriores) se reconstruyen. Ordena las instrucciones de menos a más cambiante: primero las dependencias del sistema, luego las dependencias de la aplicación (package.json), y finalmente el código fuente.
El patrón más importante es copiar package.json e instalar dependencias antes de copiar el código fuente. Así, cambios en el código no invalidan la capa de dependencias, reduciendo los tiempos de build de minutos a segundos.
Usa multi-stage builds
Los multi-stage builds permiten usar una imagen con todas las herramientas de compilación en una etapa y copiar solo los artefactos necesarios a una imagen final minimalista. Para una aplicación React, la primera etapa compila con Node.js y la segunda etapa sirve los archivos estáticos con Nginx, resultando en una imagen final de ~25MB en lugar de ~500MB.
Para Go, el multi-stage build es esencial: compilas en una imagen con el SDK de Go y copias el binario estático a una imagen scratch. El resultado es una imagen que contiene solo tu ejecutable, sin compilador, sin sistema operativo, sin nada superfluo.
No ejecutes como root
Por defecto, los procesos dentro de un contenedor se ejecutan como root. Esto es un riesgo de seguridad: si un atacante compromete tu aplicación, tiene acceso root dentro del contenedor y potencialmente al host. Crea un usuario no privilegiado en tu Dockerfile y úsalo para ejecutar la aplicación con las instrucciones RUN adduser y USER.
Usa .dockerignore
El archivo .dockerignore funciona como .gitignore: especifica qué archivos no deben copiarse a la imagen. Excluye node_modules, .git, .env, logs, archivos de test y cualquier cosa que no sea necesaria en producción. Esto reduce el tamaño de la imagen y evita filtrar información sensible.
Casos de uso comunes para desarrolladores
Entorno de desarrollo local
En lugar de instalar PostgreSQL, Redis, Elasticsearch y otros servicios directamente en tu máquina, ejecútalos como contenedores Docker. Esto mantiene tu sistema operativo limpio, permite ejecutar múltiples versiones simultáneamente, y facilita resetear el estado completamente eliminando y recreando los contenedores.
Testing con dependencias
Los tests de integración que requieren bases de datos, servicios externos o colas de mensajes son perfectos para Docker. En tu pipeline de CI, levanta los servicios necesarios como contenedores, ejecuta los tests contra ellos, y destruye todo al finalizar. Esto garantiza un entorno limpio y reproducible para cada ejecución de tests.
Microservicios locales
En arquitecturas de microservicios, desarrollar un servicio que depende de otros cinco servicios puede ser un nightmare. Docker Compose permite levantar todo el ecosistema de microservicios localmente, facilitando el desarrollo y debugging de interacciones entre servicios.
Despliegue en producción
Aunque para producción a gran escala se usa Kubernetes, Docker es la base de todo el ecosistema de contenedores. Para proyectos pequeños y medianos, Docker Swarm o simplemente docker compose en un servidor pueden ser suficientes. La imagen que probaste localmente es exactamente la misma que se despliega en producción.
Comandos esenciales que todo desarrollador debe conocer
docker build -t mi-app . - Construye una imagen desde el Dockerfile en el directorio actual y la etiqueta como "mi-app".
docker run -p 3000:3000 -d mi-app - Ejecuta un contenedor en segundo plano mapeando el puerto 3000 del host al 3000 del contenedor.
docker compose up -d - Levanta todos los servicios definidos en docker-compose.yml en segundo plano.
docker logs -f contenedor - Muestra los logs de un contenedor en tiempo real (follow mode).
docker exec -it contenedor sh - Abre una shell interactiva dentro de un contenedor en ejecución, útil para debugging.
docker system prune -a - Limpia imágenes, contenedores y volúmenes no utilizados. Úsalo periódicamente para liberar espacio en disco.
Conclusión
Docker no es solo para equipos de DevOps. Como desarrollador, Docker te permite crear entornos de desarrollo consistentes, ejecutar tests con dependencias complejas, y garantizar que tu aplicación funciona igual en cualquier entorno. La curva de aprendizaje es corta: en un par de horas puedes tener tu primer Dockerfile funcional y en un día dominar Docker Compose para tu stack completo.
Empieza containerizando tus dependencias de desarrollo (bases de datos, Redis, etc.), luego containeriza tu propia aplicación, y finalmente configura Docker Compose para orquestar todo el stack. Este enfoque incremental te permite adoptar Docker gradualmente sin sentirte abrumado.