Hay una frase que todo desarrollador ha dicho o escuchado al menos una vez: "En mi máquina funciona". El código corre perfectamente en local, pero al subirlo al servidor falla. La versión de Node es diferente, falta una librería, el sistema operativo tiene otra configuración...
Docker nació para eliminar esa frase del vocabulario del desarrollo de software. Para siempre.
Es la herramienta más adoptada en DevOps y desarrollo moderno. Según la encuesta Stack Overflow 2025, Docker es la herramienta más usada en entornos profesionales de desarrollo, por delante de Kubernetes, Terraform y otras herramientas de infraestructura. Y una vez que entiendes qué hace y por qué, no querrás trabajar sin él.
¿Qué es Docker?
Docker es una plataforma de código abierto lanzada en 2013 por Docker, Inc. (anteriormente dotCloud) que permite empaquetar aplicaciones y todas sus dependencias en unidades estandarizadas llamadas contenedores, que pueden ejecutarse de forma consistente en cualquier máquina que tenga Docker instalado.
La definición más clara: Docker garantiza que si tu aplicación funciona en tu computadora, funcionará exactamente igual en el servidor de producción, en la máquina de tu compañero de trabajo y en cualquier servicio cloud.
SIN DOCKER — el problema clásico:
Tu máquina: Servidor de producción:
Python 3.11 Python 3.8
Node 20 Node 16
PostgreSQL 15 PostgreSQL 12
librería X v2.3 librería X v1.9
Ubuntu 22.04 CentOS 7
Resultado: "En mi máquina funciona" 😤
CON DOCKER — el contenedor lleva todo consigo:
Tu máquina: Servidor: Máquina de tu compañero:
Docker Docker Docker
↓ ↓ ↓
[Contenedor] [Contenedor] [Contenedor]
Python 3.11 Python 3.11 Python 3.11
Node 20 Node 20 Node 20
PostgreSQL 15 PostgreSQL 15 PostgreSQL 15
librería X v2.3 librería X v2.3 librería X v2.3
Resultado: funciona igual en todos lados ✅El concepto clave: el contenedor
Un contenedor es un proceso aislado que incluye todo lo que una aplicación necesita para ejecutarse: el código, las librerías, las dependencias, las variables de entorno y la configuración. Es como una caja sellada y autosuficiente.
Pero hay algo importante: un contenedor no es una máquina virtual. Y entender esa diferencia es lo que hace que Docker "haga clic" en tu cabeza.
Contenedores vs Máquinas Virtuales
MÁQUINA VIRTUAL: CONTENEDOR DOCKER:
┌─────────────────────┐ ┌─────────────────────┐
│ Tu aplicación │ │ Tu aplicación │
├─────────────────────┤ ├─────────────────────┤
│ Librerías / Deps │ │ Librerías / Deps │
├─────────────────────┤ ├─────────────────────┤
│ Sistema Operativo │ │ Docker Engine │
│ COMPLETO (4-8 GB) │ │ (no replica SO) │
├─────────────────────┤ ├─────────────────────┤
│ Hipervisor │ │ Sistema Operativo │
├─────────────────────┤ │ del HOST (real) │
│ Hardware │ ├─────────────────────┤
└─────────────────────┘ │ Hardware │
└─────────────────────┘
VM simula hardware completo. Docker comparte el kernel del SO host.
Cada VM lleva su propio SO. El contenedor solo lleva lo necesario.
Arranque: minutos. Arranque: segundos o milisegundos.
Tamaño: gigabytes. Tamaño: megabytes.La clave técnica: Docker usa dos características del kernel de Linux — namespaces (aislamiento de procesos, red y filesystem) y cgroups (límites de CPU y memoria) — para crear entornos aislados sin necesitar un sistema operativo completo por contenedor.
El resultado práctico: un contenedor arranca en segundos, ocupa megabytes en lugar de gigabytes y consume muchos menos recursos que una VM haciendo lo mismo.
Los 4 conceptos que necesitas entender
📋 1. Imagen — el plano del contenedor
Una imagen Docker es una plantilla de solo lectura que define cómo debe ser el contenedor: qué sistema operativo base usar, qué software instalar, qué archivos incluir y qué comandos ejecutar al arrancar.
La analogía perfecta: si el contenedor es una casa, la imagen es el plano arquitectónico. Con el mismo plano puedes construir mil casas idénticas.
Una imagen se construye en capas:
Capa 1: ubuntu:22.04 (imagen base del SO)
+
Capa 2: Python 3.11 instalado
+
Capa 3: pip install requirements.txt
+
Capa 4: COPY de tu código
+
Capa 5: configuración de ejecución
=
Imagen final de tu aplicación
Ventaja de las capas: si solo cambias tu código (capa 4),
Docker reutiliza las capas 1-3 del caché → build más rápido📦 2. Contenedor — la imagen en ejecución
Un contenedor es una instancia en ejecución de una imagen. Es la imagen "viva". De la misma imagen puedes lanzar 10, 100 o 1000 contenedores idénticos.
Imagen "mi-app:1.0" → Contenedor 1 (corriendo en puerto 3001)
→ Contenedor 2 (corriendo en puerto 3002)
→ Contenedor 3 (corriendo en puerto 3003)
Todos idénticos. Todos independientes entre sí.📄 3. Dockerfile — la receta para construir la imagen
Un Dockerfile es un archivo de texto con instrucciones que le dicen a Docker cómo construir una imagen. Es la receta paso a paso.
☁️ 4. Docker Hub — el repositorio de imágenes
Docker Hub es el registro público de imágenes Docker, como un GitHub pero para imágenes. Tiene millones de imágenes listas para usar: bases de datos, servidores web, lenguajes, herramientas. La mayoría son gratuitas.
Instala Docker en tu computadora
Windows y macOS — Docker Desktop
Descarga Docker Desktop desde docker.com/products/docker-desktop. Incluye Docker Engine, Docker CLI y una interfaz gráfica. En Windows requiere WSL2 (Windows Subsystem for Linux).
Linux (Ubuntu/Debian)
# Método oficial recomendado
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Añadir tu usuario al grupo docker (para no usar sudo siempre)
sudo usermod -aG docker $USER
newgrp docker
# Verificar instalación
docker --version
# Docker version 27.x.x
docker run hello-world
# Si ves "Hello from Docker!" → instalación correcta ✅Los comandos esenciales de Docker
Trabajar con imágenes
# Descargar una imagen de Docker Hub
docker pull nginx # última versión
docker pull python:3.11 # versión específica
docker pull node:20-alpine # versión alpine (más ligera)
# Ver imágenes descargadas
docker images
# REPOSITORY TAG IMAGE ID SIZE
# nginx latest a99a39d070bf 192MB
# python 3.11 a5d7930b60cc 1.01GB
# node 20-alpine 9e7f2b2d7a5c 174MB
# Eliminar una imagen
docker rmi nginx
docker rmi a99a39d070bf # por ID
# Eliminar imágenes sin usar (limpieza)
docker image pruneTrabajar con contenedores
# Ejecutar un contenedor (descarga la imagen si no existe)
docker run nginx
# En segundo plano (detached mode) — lo más común
docker run -d nginx
# Con nombre personalizado
docker run -d --name mi-servidor nginx
# Mapeando puertos: puerto-host:puerto-contenedor
docker run -d -p 8080:80 nginx
# → http://localhost:8080 muestra nginx corriendo dentro del contenedor
# Con variables de entorno
docker run -d \
-e POSTGRES_PASSWORD=secreto \
-e POSTGRES_DB=mibasedatos \
-p 5432:5432 \
--name mi-postgres \
postgres:15
# Ver contenedores EN EJECUCIÓN
docker ps
# CONTAINER ID IMAGE STATUS PORTS NAMES
# a1b2c3d4e5f6 nginx Up 2min 0.0.0.0:8080->80/tcp mi-servidor
# Ver TODOS los contenedores (incluidos parados)
docker ps -a
# Ver logs del contenedor
docker logs mi-servidor
docker logs -f mi-servidor # -f = follow (en tiempo real)
# Entrar al terminal del contenedor (como un SSH)
docker exec -it mi-servidor bash
docker exec -it mi-postgres psql -U postgres
# Parar un contenedor
docker stop mi-servidor
# Iniciar un contenedor parado
docker start mi-servidor
# Eliminar un contenedor (debe estar parado)
docker rm mi-servidor
docker rm -f mi-servidor # forzar si está corriendo
# Eliminar contenedores parados, redes no usadas, imágenes sin usar
docker system pruneEl Dockerfile — crear tu propia imagen
Usar imágenes de Docker Hub está bien. Pero para desplegar tu propia aplicación necesitas crear tu propia imagen con un Dockerfile.
Ejemplo 1: Aplicación Node.js
# Dockerfile para una app Node.js/Express
# 1. Imagen base — siempre especifica la versión exacta
FROM node:20-alpine
# 2. Directorio de trabajo dentro del contenedor
WORKDIR /app
# 3. Copiar los archivos de dependencias primero (optimización de caché)
# Si package.json no cambia, Docker reutiliza esta capa
COPY package*.json ./
# 4. Instalar dependencias
RUN npm ci --only=production
# 5. Copiar el resto del código
COPY . .
# 6. Exponer el puerto que usa la app (documentación, no abre el puerto)
EXPOSE 3000
# 7. Comando a ejecutar cuando arranque el contenedor
CMD ["node", "src/index.js"]# Construir la imagen desde el Dockerfile
docker build -t mi-app-node:1.0 .
# -t → nombre:versión de la imagen
# . → directorio donde está el Dockerfile
# Ver que la imagen se creó
docker images | grep mi-app-node
# Ejecutar el contenedor con la imagen creada
docker run -d -p 3000:3000 --name mi-app mi-app-node:1.0
# Probar que funciona
curl http://localhost:3000Ejemplo 2: Aplicación Python/FastAPI
# Dockerfile para una API con Python y FastAPI
FROM python:3.11-slim
WORKDIR /app
# Copiar requirements primero (caché de capas)
COPY requirements.txt .
# Instalar dependencias
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el código
COPY . .
# Variables de entorno
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]El archivo .dockerignore — lo que NO debe entrar
# .dockerignore — como .gitignore pero para Docker
# Evita que archivos innecesarios entren en la imagen
node_modules/ # dependencias — se instalan dentro del contenedor
.git/ # historial de Git — no necesario en la imagen
.env # variables de entorno — se pasan en runtime
*.log # logs locales
dist/ # archivos de build anteriores
__pycache__/ # caché de Python
.pytest_cache/ # caché de tests
README.md # documentación — no necesaria en producción
.DS_Store # archivos del sistema macOS💡 Buena práctica: Un .dockerignore bien configurado reduce drásticamente el tamaño de la imagen y acelera el proceso de build.
Buenas prácticas para Dockerfiles
# ❌ Malo: usa imagen genérica y pesada
FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3 pip
# ✅ Mejor: usa imagen oficial específica y ligera
FROM python:3.11-slim # o python:3.11-alpine para aún más ligero
# ─────────────────────────────────────────────────────────────
# ❌ Malo: ejecutar como root (riesgo de seguridad)
FROM node:20
COPY . .
CMD ["node", "app.js"]
# ✅ Mejor: crear y usar un usuario sin privilegios
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser # ← ejecuta como usuario sin privilegios
CMD ["node", "app.js"]
# ─────────────────────────────────────────────────────────────
# ❌ Malo: orden incorrecto de capas (invalida caché innecesariamente)
FROM node:20-alpine
WORKDIR /app
COPY . . # ← copia TODO primero
RUN npm install # ← si cualquier archivo cambia, reinstala todo
# ✅ Bueno: copiar dependencias primero para aprovechar el caché
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./ # ← solo los archivos de dependencias
RUN npm ci # ← solo se re-ejecuta si package.json cambia
COPY . . # ← copia el resto del códigoDocker Compose — orquestación de múltiples contenedores
La mayoría de aplicaciones reales necesitan varios servicios: la app, una base de datos, una caché, un worker... Docker Compose permite definir y gestionar todos esos contenedores juntos con un solo archivo.
Es especialmente útil si estás construyendo una arquitectura de microservicios o simplemente necesitas tu app junto a su base de datos en desarrollo.
# docker-compose.yml
# Aplicación web + PostgreSQL + Redis
version: '3.9'
services:
# ── Tu aplicación ──────────────────────────────────────────
app:
build: . # construir desde el Dockerfile local
container_name: mi-app
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://usuario:secreto@postgres:5432/midb
- REDIS_URL=redis://redis:6379
depends_on:
postgres:
condition: service_healthy # espera a que postgres esté listo
redis:
condition: service_started
volumes:
- .:/app # monta el código local en el contenedor
- /app/node_modules # excepto node_modules
restart: unless-stopped
# ── Base de datos PostgreSQL ────────────────────────────────
postgres:
image: postgres:15-alpine
container_name: mi-postgres
environment:
POSTGRES_USER: usuario
POSTGRES_PASSWORD: secreto
POSTGRES_DB: midb
volumes:
- postgres_data:/var/lib/postgresql/data # persistencia de datos
ports:
- "5432:5432" # opcional: exponer para herramientas externas
healthcheck:
test: ["CMD-SHELL", "pg_isready -U usuario -d midb"]
interval: 10s
timeout: 5s
retries: 5
# ── Caché Redis ─────────────────────────────────────────────
redis:
image: redis:7-alpine
container_name: mi-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
# ── Volúmenes persistentes ──────────────────────────────────
volumes:
postgres_data: # los datos de postgres sobreviven al reinicio
redis_data: # los datos de redis sobreviven al reinicio# Comandos de Docker Compose
# Levantar todos los servicios (en segundo plano)
docker compose up -d
# Ver el estado de todos los servicios
docker compose ps
# Ver logs de todos los servicios
docker compose logs -f
# Ver logs de un servicio específico
docker compose logs -f app
# Parar todos los servicios (conserva los datos)
docker compose stop
# Parar y eliminar contenedores (conserva los volúmenes)
docker compose down
# Parar y eliminar TODO incluyendo volúmenes (cuidado: borra datos)
docker compose down -v
# Reconstruir las imágenes (cuando cambias el Dockerfile)
docker compose build
# Ejecutar un comando en un servicio
docker compose exec app sh
docker compose exec postgres psql -U usuario -d midb
# Ver uso de recursos
docker compose statsVolúmenes — persistencia de datos
Por defecto, cuando un contenedor se elimina, sus datos desaparecen. Los volúmenes son el mecanismo de Docker para persistir datos entre reinicios y eliminar esa limitación.
# Sin volumen: los datos desaparecen al eliminar el contenedor ❌
docker run -d --name postgres postgres:15
# Con volumen nombrado: datos persistentes ✅
docker run -d \
--name postgres \
-v postgres_datos:/var/lib/postgresql/data \
postgres:15
# Con bind mount: mapea una carpeta local al contenedor
# Útil para desarrollo: editas el código y el contenedor lo ve al instante
docker run -d \
--name mi-app \
-v $(pwd)/src:/app/src \ # tu carpeta local → /app/src en el contenedor
-p 3000:3000 \
mi-app-node:dev
# Gestionar volúmenes
docker volume ls # listar volúmenes
docker volume inspect postgres_datos # detalles del volumen
docker volume rm postgres_datos # eliminar volumenRedes en Docker — cómo se comunican los contenedores
# Por defecto, cada contenedor está aislado en red
# Para que se comuniquen, necesitan estar en la misma red
# Crear una red
docker network create mi-red
# Lanzar contenedores en la misma red
docker run -d --name postgres --network mi-red postgres:15
docker run -d --name mi-app --network mi-red -p 3000:3000 mi-app-node:1.0
# Dentro de mi-app, puede conectarse a postgres por nombre:
# DATABASE_URL=postgresql://user:pass@postgres:5432/db
# ↑
# nombre del contenedor como hostname
# Docker Compose crea automáticamente una red para todos los servicios
# Por eso en docker-compose.yml los servicios se referencian por nombreDocker en el flujo de trabajo real
Así es como Docker encaja en el ciclo de desarrollo profesional, junto con herramientas de control de versiones como Git:
FLUJO DE TRABAJO CON DOCKER
1. DESARROLLO LOCAL
─────────────────
docker compose up -d
→ App + BD + Redis corriendo en local
→ Editas código → el bind mount lo refleja instantáneamente
→ docker compose logs -f para ver errores en tiempo real
2. BUILD DE LA IMAGEN
──────────────────
git push origin main
→ CI/CD detecta el push
→ docker build -t mi-app:v2.3.1 .
→ docker push registro.empresa.com/mi-app:v2.3.1
3. TESTING EN CI
─────────────
→ docker run mi-app:v2.3.1 npm test
→ Si los tests pasan → imagen aprobada
4. DESPLIEGUE EN PRODUCCIÓN
─────────────────────────
→ El servidor descarga la nueva imagen
→ docker pull registro.empresa.com/mi-app:v2.3.1
→ docker compose up -d --no-deps --build app
→ Actualización sin tiempo de inactividad
5. ROLLBACK SI HAY PROBLEMAS
──────────────────────────
→ docker pull registro.empresa.com/mi-app:v2.2.0
→ docker compose up -d
→ En 30 segundos estás en la versión anteriorDocker Hub — imágenes listas para usar
Algunas de las imágenes oficiales más útiles que puedes usar directamente:
# ─── Bases de datos ───────────────────────────────────────────
docker run -d -e POSTGRES_PASSWORD=secreto -p 5432:5432 postgres:15
docker run -d -e MYSQL_ROOT_PASSWORD=secreto -p 3306:3306 mysql:8
docker run -d -p 27017:27017 mongo:7
docker run -d -p 6379:6379 redis:7-alpine
# ─── Servidores web ────────────────────────────────────────────
docker run -d -p 80:80 nginx
docker run -d -p 8080:8080 httpd # Apache
# ─── Runtimes ──────────────────────────────────────────────────
docker run -it python:3.11 python3
docker run -it node:20 node
docker run -it openjdk:21 java --version
# ─── Herramientas de desarrollo ────────────────────────────────
docker run -d -p 8080:8080 adminer # UI para bases de datos
docker run -d -p 9200:9200 elasticsearch:8.12.0
docker run -d -p 5601:5601 kibana:8.12.0
# Con todas estas imágenes, montar un entorno de desarrollo completo
# en una máquina nueva tarda minutos, no horas.Errores comunes de principiantes
❌ No especificar versión de la imagen
# ❌ Malo: "latest" puede romperte el build cuando actualicen la imagen
FROM node:latest
# ✅ Bueno: versión exacta y reproducible
FROM node:20.11.1-alpine3.19❌ No usar .dockerignore
# Sin .dockerignore, COPY . . incluye node_modules (cientos de MB)
# y archivos sensibles como .env
# Siempre crea .dockerignore antes del primer docker build❌ Guardar secretos en la imagen
# ❌ NUNCA hagas esto — los secretos quedan en el historial de la imagen
ENV DATABASE_PASSWORD=mi-password-real
# ✅ Pasa los secretos en tiempo de ejecución, no en build time
docker run -e DATABASE_PASSWORD=mi-password-real mi-app
# o mejor, usa archivos .env con docker compose:
# env_file:
# - .env❌ Ejecutar como root
# ❌ Por defecto los contenedores corren como root
# Si hay una vulnerabilidad, el atacante tiene acceso root
# ✅ Crear un usuario sin privilegios en el Dockerfile
RUN addgroup -S app && adduser -S app -G app
USER app❌ Contenedores que guardan estado
# ❌ Los contenedores son efímeros — sus datos desaparecen al eliminarse
# No guardes datos importantes dentro del filesystem del contenedor
# ✅ Usa volúmenes para cualquier dato que necesite persistir
volumes:
- postgres_data:/var/lib/postgresql/dataResumen: lo que aprendiste hoy
- ✅ Docker empaqueta apps con todas sus dependencias en contenedores portátiles — adiós al "en mi máquina funciona"
- ✅ Un contenedor comparte el kernel del SO host — más ligero y rápido que una máquina virtual
- ✅ Los 4 conceptos clave: imagen (plano), contenedor (instancia), Dockerfile (receta) y Docker Hub (registro)
- ✅ Las imágenes se construyen en capas — el orden en el Dockerfile importa para el rendimiento del caché
- ✅ Siempre usa imágenes con versión específica, nunca
:latesten producción - ✅ Docker Compose orquesta múltiples contenedores con un solo archivo
docker-compose.yml - ✅ Los volúmenes persisten datos entre reinicios de contenedores
- ✅ Los bind mounts mapean carpetas locales al contenedor — ideales para desarrollo
- ✅ Los contenedores en la misma red se comunican usando el nombre del servicio como hostname
- ✅ Nunca guardes secretos en la imagen y ejecuta siempre con un usuario sin privilegios
🧪 ¿Tienes claros los fundamentos de programación y DevOps?
Docker es una herramienta de infraestructura que se usa junto con tus conocimientos de programación y arquitectura. Para sacarle todo el partido, conviene tener claros los conceptos de APIs, control de versiones con Git y arquitectura de aplicaciones. Comprueba dónde estás:
👉 Test: Arquitectura de Aplicaciones Web 👉 Test: Git y GitHub Básico
¿Ya usabas Docker o era la primera vez que lo veías? ¿Qué fue lo que más te costó entender: la diferencia con las VMs, el Dockerfile o los volúmenes? ¿Tienes algún proyecto donde lo pondrías en práctica? Cuéntanos en los comentarios 👇 — respondemos todos. 🚀
No hay comentarios todavía. Sé el primero en compartir tu opinión.