Docker Tutorial en Español: Guía Completa 2026
Tutoriales

Docker Tutorial en Español: Guía Completa 2026

13 min de lectura
24 Vistas
Compartir:

Si alguna vez te has encontrado con el famoso problema de "funciona en mi máquina pero no en el servidor", Docker es la solución definitiva. En esta guía práctica, vamos a recorrer Docker desde los conceptos fundamentales hasta configuraciones avanzadas con Docker Compose, multi-stage builds y debugging — todo con ejemplos que puedes ejecutar ahora mismo en tu terminal.

Llevo más de 3 años usando Docker en proyectos de producción con Laravel, Node.js y Python, y puedo decirte que una vez que lo integras en tu flujo de trabajo, no hay vuelta atrás. Vamos a ello.

¿Qué es Docker y por qué deberías usarlo?

Docker es una plataforma de contenedorización que empaqueta tu aplicación junto con todas sus dependencias (sistema operativo, librerías, configuraciones) en una unidad portable llamada contenedor. A diferencia de las máquinas virtuales, los contenedores comparten el kernel del sistema operativo host, lo que los hace extremadamente ligeros y rápidos.

Docker vs Máquinas Virtuales

CaracterísticaDockerMáquina Virtual
Tiempo de inicioSegundosMinutos
Uso de RAMMínimo (comparte kernel)Alto (OS completo)
Tamaño en discoMBsGBs
AislamientoA nivel de procesoCompleto (hardware virtualizado)
PortabilidadExcelenteBuena

Conceptos esenciales

Antes de escribir una sola línea, necesitas entender estos 5 conceptos:

  • Imagen (Image): Plantilla de solo lectura con las instrucciones para crear un contenedor. Piensa en ella como una "clase" en programación orientada a objetos.
  • Contenedor (Container): Una instancia en ejecución de una imagen. Es la "instancia" de esa clase — puedes tener múltiples contenedores de la misma imagen.
  • Dockerfile: Archivo de texto con las instrucciones paso a paso para construir una imagen. Es tu "receta".
  • Docker Compose: Herramienta para definir y ejecutar aplicaciones multi-contenedor (por ejemplo: app + base de datos + cache).
  • Volumen (Volume): Mecanismo para persistir datos fuera del ciclo de vida del contenedor. Sin volúmenes, los datos se pierden al eliminar el contenedor.

Instalación de Docker

Windows

Descarga Docker Desktop para Windows desde la web oficial. Requiere WSL 2 habilitado. Después de instalar, verifica en PowerShell:

docker --version
# Docker version 27.x.x, build xxxxx

docker compose version
# Docker Compose version v2.x.x

macOS

Descarga Docker Desktop para Mac. Disponible para chips Intel y Apple Silicon (M1/M2/M3/M4):

brew install --cask docker

Linux (Ubuntu/Debian)

En Linux, instala Docker Engine directamente — no necesitas Docker Desktop. Sigue la guía oficial de instalación para Ubuntu:

# Agregar repositorio oficial de Docker
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Agregar tu usuario al grupo docker (evita usar sudo)
sudo usermod -aG docker $USER
newgrp docker

# Verificar instalación
docker run hello-world

Tu primer contenedor

Vamos a ejecutar un contenedor de Nginx para servir una página web estática:

# Descargar y ejecutar Nginx
docker run -d --name mi-web -p 8080:80 nginx:alpine

# Verificar que está corriendo
docker ps

Abre http://localhost:8080 en tu navegador y verás la página de bienvenida de Nginx. Así de simple.

Desglosemos el comando:

  • -d: Ejecuta en segundo plano (detached mode)
  • --name mi-web: Le asigna un nombre legible al contenedor
  • -p 8080:80: Mapea el puerto 8080 de tu máquina al puerto 80 del contenedor
  • nginx:alpine: Imagen de Nginx basada en Alpine Linux (solo ~7MB)

Comandos básicos que usarás a diario

# Ver contenedores en ejecución
docker ps

# Ver TODOS los contenedores (incluyendo detenidos)
docker ps -a

# Detener un contenedor
docker stop mi-web

# Iniciar un contenedor detenido
docker start mi-web

# Ver logs del contenedor
docker logs mi-web
docker logs -f mi-web  # seguir logs en tiempo real

# Ejecutar un comando dentro del contenedor
docker exec -it mi-web sh

# Eliminar un contenedor (debe estar detenido)
docker stop mi-web && docker rm mi-web

# Eliminar TODOS los contenedores detenidos, imágenes sin uso, etc.
docker system prune -a

Creando tu propio Dockerfile

Vamos a crear una aplicación Node.js real y contenedorizarla. Primero, el proyecto:

mkdir docker-api && cd docker-api
npm init -y
npm install express

Crea el archivo server.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Simulamos una pequeña "base de datos" en memoria
let tareas = [
  { id: 1, titulo: 'Aprender Docker', completada: false },
  { id: 2, titulo: 'Crear un Dockerfile', completada: false },
];

app.get('/api/tareas', (req, res) => {
  res.json({ total: tareas.length, datos: tareas });
});

app.post('/api/tareas', (req, res) => {
  const nueva = {
    id: tareas.length + 1,
    titulo: req.body.titulo,
    completada: false,
  };
  tareas.push(nueva);
  res.status(201).json(nueva);
});

app.delete('/api/tareas/:id', (req, res) => {
  tareas = tareas.filter(t => t.id !== parseInt(req.params.id));
  res.json({ mensaje: 'Tarea eliminada' });
});

app.listen(PORT, () => {
  console.log(`API corriendo en http://localhost:${PORT}`);
});

Ahora el Dockerfile:

# Usamos la imagen oficial de Node.js basada en Alpine
FROM node:20-alpine

# Creamos el directorio de trabajo
WORKDIR /app

# Copiamos PRIMERO los archivos de dependencias
# Esto aprovecha la caché de Docker (si package.json no cambia, no reinstala)
COPY package*.json ./

# Instalamos dependencias (solo producción)
RUN npm ci --only=production

# Copiamos el resto del código
COPY . .

# Exponemos el puerto (documentación, no abre el puerto realmente)
EXPOSE 3000

# Ejecutamos como usuario no-root por seguridad
USER node

# Comando para iniciar la app
CMD ["node", "server.js"]

Y el archivo .dockerignore (tan importante como .gitignore):

node_modules
npm-debug.log
.git
.gitignore
.env
Dockerfile
docker-compose.yml
README.md

Construye y ejecuta:

# Construir la imagen
docker build -t mi-api-node .

# Ejecutar el contenedor
docker run -d --name api -p 3000:3000 mi-api-node

# Probar la API
curl http://localhost:3000/api/tareas

Multi-Stage Builds: imágenes más pequeñas y seguras

En proyectos reales, tu imagen de desarrollo tiene herramientas (compiladores, devDependencies) que no necesitas en producción. Los multi-stage builds resuelven esto usando múltiples etapas FROM en un solo Dockerfile:

# --- Etapa 1: Build ---
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# Si tu proyecto necesita compilación (TypeScript, etc.)
# RUN npm run build

# --- Etapa 2: Producción ---
FROM node:20-alpine AS production

WORKDIR /app

# Copiamos SOLO lo necesario desde la etapa de build
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production

COPY --from=builder /app/server.js ./

# Seguridad: no ejecutar como root
USER node

EXPOSE 3000
CMD ["node", "server.js"]

Con este enfoque, la imagen final solo contiene las dependencias de producción y tu código compilado. En proyectos TypeScript o React, la diferencia puede ser de 800MB vs 150MB.

Volúmenes: persistencia de datos

Los contenedores son efímeros — cuando los eliminas, todo su contenido desaparece. Los volúmenes son el mecanismo de Docker para persistir datos:

# Crear un volumen con nombre
docker volume create datos-postgres

# Usar el volumen al ejecutar un contenedor
docker run -d \
  --name mi-postgres \
  -e POSTGRES_PASSWORD=secreto123 \
  -e POSTGRES_DB=miapp \
  -v datos-postgres:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16-alpine

# Los datos sobreviven aunque elimines el contenedor
docker stop mi-postgres && docker rm mi-postgres

# Si creas otro contenedor con el mismo volumen, los datos siguen ahí
docker run -d \
  --name mi-postgres-2 \
  -e POSTGRES_PASSWORD=secreto123 \
  -v datos-postgres:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16-alpine

Bind mounts para desarrollo

Durante el desarrollo, quieres que los cambios en tu código local se reflejen inmediatamente en el contenedor, sin reconstruir la imagen. Para eso usamos bind mounts:

# Montamos el directorio actual como volumen
docker run -d \
  --name api-dev \
  -p 3000:3000 \
  -v $(pwd):/app \
  -v /app/node_modules \
  mi-api-node

El truco de -v /app/node_modules crea un volumen anónimo para node_modules, evitando que los módulos del host sobreescriban los del contenedor (que pueden ser de un OS diferente).

Docker Compose: aplicaciones multi-contenedor

Las aplicaciones reales no son un solo contenedor. Típicamente tienes: aplicación + base de datos + cache + cola de trabajo. Docker Compose orquesta todo esto con un solo archivo YAML.

Crea el archivo docker-compose.yml:

version: '3.8'

services:
  # Tu aplicación Node.js
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - PORT=3000
      - MONGO_URI=mongodb://mongo:27017/miapp
      - REDIS_URL=redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      mongo:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  # Base de datos MongoDB
  mongo:
    image: mongo:7
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    healthcheck:
      test: mongosh --eval "db.adminCommand('ping')" --quiet
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  # Cache con Redis
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  # Interfaz web para MongoDB (opcional pero útil)
  mongo-express:
    image: mongo-express
    ports:
      - "8081:8081"
    environment:
      - ME_CONFIG_MONGODB_SERVER=mongo
    depends_on:
      - mongo

volumes:
  mongo-data:
  redis-data:

Los comandos esenciales de Docker Compose:

# Levantar todos los servicios
docker compose up -d

# Ver logs de todos los servicios
docker compose logs -f

# Ver logs solo de un servicio
docker compose logs -f api

# Detener todos los servicios
docker compose down

# Detener y eliminar volúmenes (¡cuidado! borra datos)
docker compose down -v

# Reconstruir imágenes (cuando cambias el Dockerfile)
docker compose up -d --build

# Ver el estado de los servicios
docker compose ps

Healthchecks: verificación de salud

Nota cómo usamos healthcheck y depends_on con condition: service_healthy. Esto asegura que tu API no intente conectarse a MongoDB antes de que esté lista — un problema muy común que causa errores de conexión al iniciar.

Docker Networking: comunicación entre contenedores

Docker Compose crea automáticamente una red interna donde los contenedores se comunican usando sus nombres de servicio como hostname. Por eso en la variable MONGO_URI usamos mongo (el nombre del servicio) en lugar de localhost:

# Dentro de Docker Compose
MONGO_URI=mongodb://mongo:27017/miapp    ✅ Correcto
MONGO_URI=mongodb://localhost:27017/miapp  ❌ No funciona entre contenedores

Para inspeccionar las redes:

# Listar redes de Docker
docker network ls

# Inspeccionar una red específica
docker network inspect docker-api_default

Debugging: cuando las cosas no funcionan

Docker puede ser frustrante cuando algo falla sin explicación clara. Estas son las técnicas de debugging que uso constantemente:

# 1. Ver logs del contenedor
docker logs mi-contenedor --tail 50

# 2. Entrar al contenedor para investigar
docker exec -it mi-contenedor sh

# 3. Inspeccionar la configuración del contenedor
docker inspect mi-contenedor

# 4. Ver uso de recursos en tiempo real
docker stats

# 5. Ver los procesos dentro del contenedor
docker top mi-contenedor

# 6. Copiar archivos desde/hacia el contenedor
docker cp mi-contenedor:/app/logs/error.log ./error.log
docker cp ./config.json mi-contenedor:/app/config.json

# 7. Ver el historial de capas de una imagen
docker history mi-api-node

Errores comunes y cómo resolverlos

Error: "port is already allocated"

# Encontrar qué proceso usa el puerto
# En Linux/Mac:
lsof -i :3000
# En Windows:
netstat -ano | findstr :3000

# Solución: cambiar el puerto o detener el proceso

Error: "no space left on device"

# Limpiar todo lo que no se usa
docker system prune -a --volumes

# Ver cuánto espacio usa Docker
docker system df

Error: "EACCES permission denied" en volúmenes

# Problema común con USER node y bind mounts
# Solución: ajustar permisos en el Dockerfile
RUN mkdir -p /app && chown -R node:node /app
USER node

Ejemplo real: Laravel + MySQL + Redis con Docker

Para los que trabajan con PHP/Laravel (como yo en varios proyectos), aquí un docker-compose completo:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/var/www/html
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      - DB_HOST=mysql
      - DB_DATABASE=laravel
      - DB_USERNAME=laravel
      - DB_PASSWORD=secreto
      - REDIS_HOST=redis

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - .:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: secreto
      MYSQL_ROOT_PASSWORD: rootsecreto
    volumes:
      - mysql-data:/var/lib/mysql
    ports:
      - "3306:3306"
    healthcheck:
      test: mysqladmin ping -h localhost -u root --password=rootsecreto
      interval: 10s
      timeout: 5s
      retries: 3

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  mysql-data:

Buenas prácticas para producción

  1. Usa imágenes Alpine: node:20-alpine pesa ~50MB vs ~350MB de node:20. Consulta el Docker Hub de Node.js para ver las opciones disponibles.
  2. No ejecutes como root: Siempre agrega USER node (o el usuario apropiado) en tu Dockerfile. Si un atacante compromete tu contenedor, limitas el daño.
  3. Un proceso por contenedor: No metas tu app, la base de datos y Redis en un solo contenedor. Docker está diseñado para un proceso principal por contenedor.
  4. Usa .dockerignore: Excluye node_modules, .git, .env, archivos de test y documentación. Reduce el contexto de build y evita fugas de información sensible.
  5. Ordena las instrucciones del Dockerfile: Pon primero lo que cambia menos (instalación de sistema, COPY package.json, RUN npm ci) y al final lo que cambia más (COPY . .). Esto maximiza el uso de caché.
  6. Fija versiones de las imágenes: Usa node:20.11-alpine en lugar de node:latest. Con latest, tu build puede romperse cualquier día sin que cambies nada en tu código.
  7. Escanea vulnerabilidades: Ejecuta docker scout quickview mi-imagen para detectar vulnerabilidades conocidas en tus dependencias. Más información en la documentación de Docker Scout.
  8. Usa healthchecks: Permiten que Docker (y orquestadores como Kubernetes) sepan si tu app está realmente funcionando, no solo si el proceso está vivo.

Cheat sheet de comandos Docker

ComandoDescripción
docker build -t nombre .Construir imagen desde Dockerfile
docker run -d -p 3000:3000 nombreEjecutar contenedor en background
docker psListar contenedores activos
docker logs -f nombreSeguir logs en tiempo real
docker exec -it nombre shAbrir shell dentro del contenedor
docker stop nombreDetener contenedor
docker rm nombreEliminar contenedor detenido
docker imagesListar imágenes locales
docker rmi nombreEliminar imagen
docker system prune -aLimpiar todo lo no utilizado
docker compose up -dLevantar servicios en background
docker compose downDetener y eliminar servicios
docker compose logs -fVer logs de todos los servicios

Próximos pasos

Con los fundamentos cubiertos, te recomiendo explorar estos temas para llevar Docker al siguiente nivel:

  • Docker en CI/CD: Integra Docker con GitHub Actions o GitLab CI para automatizar builds y deployments.
  • Docker Registry: Publica tus imágenes en Docker Hub o un registry privado.
  • Kubernetes: Cuando necesites orquestar decenas o cientos de contenedores en producción.
  • Docker Compose Profiles: Para manejar diferentes configuraciones (development, testing, production) en un solo archivo.

Docker ha cambiado fundamentalmente la forma en que desarrollamos y desplegamos software. Lo que antes requería horas de configuración manual ahora se resume en un docker compose up. Si estás empezando, mi consejo es: contenedoriza tu próximo proyecto desde el día uno. No esperes a que crezca — es más fácil empezar con Docker que migrarlo después.

J
Escrito por
Jesús García

Apasionado por la tecnologia y las finanzas personales. Escribo sobre innovacion, inteligencia artificial, inversiones y estrategias para mejorar tu economia. Mi objetivo es hacer que temas complejos sean accesibles para todos.

Compartir artículo:

Artículos relacionados

Comentarios

Deja un comentario

Herramientas Recomendadas

Las que usamos en nuestros proyectos

Enlaces de afiliado. Sin costo adicional para ti.

¿Necesitas servicios de tecnología?

Ofrecemos soluciones integrales de desarrollo web, aplicaciones móviles, consultoría y más.

Desarrollo Web Apps Móviles Consultoría