🐳 ¿Qué es Docker y para qué sirve?
Imagina que escribes una aplicación que funciona perfectamente en tu ordenador, pero cuando la subes al servidor falla. El clásico "en mi máquina sí funciona". Esto pasa porque el servidor puede tener distinta versión de Node.js, otro sistema operativo, librerías distintas…
Docker resuelve este problema empaquetando tu aplicación junto con todo lo que necesita (el runtime, las librerías, la configuración) dentro de una unidad portable llamada contenedor. Ese contenedor se comporta igual en cualquier máquina donde lo ejecutes.
Aislamiento
Cada contenedor tiene su propio entorno. No hay conflictos si dos proyectos usan versiones distintas de Python, Node u otras librerías.
Arranque rápido
Un contenedor arranca en segundos. Una máquina virtual tarda minutos porque necesita iniciar un sistema operativo completo.
Portabilidad
Construyes el contenedor una vez y funciona en cualquier sitio que tenga Docker instalado: tu portátil, el servidor, la nube.
Trabajo en equipo
Todo el equipo usa el mismo entorno. Se acabó perder horas configurando el entorno de desarrollo de cada desarrollador nuevo.
Contenedor vs Máquina Virtual
Mucha gente confunde Docker con las máquinas virtuales (VMs). Son conceptos similares pero muy distintos en la práctica:
Contenedor Docker
- Comparte el sistema operativo del host
- Arranca en segundos
- Ocupa MB
- Ideal para ejecutar aplicaciones
Máquina Virtual (VM)
- Tiene su propio sistema operativo completo
- Arranca en minutos
- Ocupa GB
- Ideal para simular máquinas enteras
⚙️ Instalación
La forma más sencilla de empezar es instalar Docker Desktop. Es una aplicación que incluye todo lo necesario: el motor de Docker, Docker Compose (para gestionar varios contenedores a la vez) y una interfaz gráfica para ver qué está corriendo.
# Opción A: Descarga Docker Desktop desde https://docker.com/products/docker-desktop
# Opción B: Con Homebrew (gestor de paquetes para Mac)
brew install --cask docker
# Después de instalar, abre Docker Desktop desde Aplicaciones.
# Cuando veas la ballena 🐳 en la barra de menú, Docker está listo.
# Verifica que funciona abriendo la Terminal:
docker --version
# Docker Desktop en Windows requiere WSL2 habilitado.
# WSL2 (Windows Subsystem for Linux) es lo que permite a Docker
# correr contenedores Linux dentro de Windows.
# Paso 1: Habilitar WSL2 — abre PowerShell como Administrador:
wsl --install
# Paso 2: Reinicia el ordenador.
# Paso 3: Descarga Docker Desktop desde:
# https://docker.com/products/docker-desktop
# O instala con winget desde PowerShell:
winget install Docker.DockerDesktop
# Verifica:
docker --version
# Elimina versiones antiguas si las tienes:
sudo apt-get remove docker docker-engine docker.io containerd runc
# Instala herramientas necesarias para añadir el repositorio:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
# Añade la clave GPG oficial de Docker (sirve para verificar
# que los paquetes son auténticos y no han sido modificados):
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Añade el repositorio oficial de Docker a tus fuentes de apt:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
# Instala Docker:
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Añade tu usuario al grupo "docker" para no tener que escribir
# "sudo" delante de cada comando de Docker:
sudo usermod -aG docker $USER
newgrp docker
# Docker ofrece un script oficial que detecta tu distribución Linux
# y lo instala automáticamente. Compatible con Ubuntu, Debian, Fedora…
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
# Sin sudo para tu usuario:
sudo usermod -aG docker $USER
# Prueba que todo va bien:
docker run hello-world
Prueba que todo funciona
# Ver qué versión se instaló
docker --version
# Resultado esperado: Docker version 25.x.x, build xxxxxxx
# Ejecuta el contenedor de prueba oficial de Docker.
# Docker descargará una imagen pequeña, la ejecutará
# y te mostrará un mensaje de bienvenida.
docker run hello-world
# Si ves "Hello from Docker!" → ¡todo correcto! 🎉
🧠 Conceptos clave
Docker tiene su propio vocabulario. Entiende estos 5 conceptos y todo lo demás encajará solo.
🖼️ Imagen (Image)
Una imagen es una plantilla de solo lectura que contiene todo lo necesario para ejecutar una aplicación: el sistema de archivos base, las librerías instaladas y el comando que debe ejecutarse al arrancar.
💡 Analogía: Piensa en la imagen como la receta de un pastel. Te dice exactamente qué ingredientes lleva y cómo prepararlo, pero no te la puedes comer directamente.
- Las imágenes no se ejecutan directamente — a partir de ellas se crean contenedores.
-
Existen miles de imágenes listas en Docker Hub:
nginx,postgres,node,python… - Puedes crear las tuyas propias con un Dockerfile (sección 9).
📦 Contenedor (Container)
Un contenedor es una instancia en ejecución de una imagen. La imagen es el plano, el contenedor es lo que construyes a partir de ese plano. Puedes crear varios contenedores idénticos de la misma imagen.
💡 Analogía: Si la imagen era la receta, el contenedor es el pastel ya cocinado y listo para comer. Y con una misma receta (imagen), puedes hornear tantos pasteles (contenedores) como quieras.
- Los contenedores son efímeros: si los eliminas, los datos escritos internamente se pierden (a menos que uses volúmenes).
- Cada contenedor tiene su propia red y sistema de archivos, aislado del resto.
📄 Dockerfile
Es un archivo de texto con instrucciones para construir una imagen personalizada. Le dice a Docker: qué imagen base usar, qué instalar, qué archivos copiar y qué comando ejecutar cuando arranque el contenedor.
💡 Analogía: Son los pasos escritos a mano de cómo hacer tu propia receta desde cero, paso a paso, para que cualquier máquina sepa cocinarla.
-
Con el comando
docker buildprocesas ese archivo y obtienes tu imagen.
💾 Volumen (Volume)
Un volumen es un espacio de almacenamiento que vive fuera del contenedor. Sirve para guardar datos que deben sobrevivir aunque el contenedor se elimine o reinicie.
💡 Analogía: Los contenedores tienen mala memoria (son efímeros). Si se destruye el contenedor, pierdes todo lo de dentro. El volumen es como un tupper externo donde guardas los datos importantes; si tiras el pastel, lo del tupper sigue a salvo.
- Ejemplo: los datos de una base de datos PostgreSQL. Sin volumen, al eliminar el contenedor pierdes toda la base de datos. Con volumen, los datos persisten.
🌐 Red (Network)
Docker crea redes virtuales para que los contenedores se puedan comunicar entre sí. Si pones varios contenedores en la misma red, pueden hablarse usando el nombre del contenedor como si fuera un hostname.
💡 Analogía: Es como meter a varias personas en una misma sala de chat privada. Se pueden llamar por su nombre de pila ("hola backend", "hola db") en lugar de usar números de teléfono complicados (las IPs).
-
Ejemplo: tu app puede conectarse a la base de datos usando
dbcomo dirección — sin necesidad de conocer IPs.
Cómo encajan todas las piezas
🖼️ Trabajar con imágenes
Las imágenes son el punto de partida absoluto. ¿Por qué? Porque no puedes arrancar un contenedor (cocinar el pastel) sin tener primero la imagen (la receta). Antes de ejecutar cualquier contenedor necesitas tener la imagen en tu ordenador, ya sea descargándola de Docker Hub (la biblioteca pública de recetas) o construyendo la tuya propia.
Es como un "GitHub para imágenes Docker". Ahí viven miles de imágenes oficiales y de la comunidad que puedes descargar gratis: bases de datos, servidores web, lenguajes de programación, etc.
# ── BUSCAR imágenes en Docker Hub ─────────────────────────────
docker search nginx # Busca imágenes con "nginx" en el nombre
# ── DESCARGAR (pull) ──────────────────────────────────────────
# "pull" descarga la imagen a tu máquina sin ejecutarla todavía
docker pull nginx # Descarga la última versión (latest)
docker pull nginx:1.25-alpine # Una versión específica y variante
docker pull node:20 # Node.js versión 20
docker pull postgres:16 # PostgreSQL versión 16
# ── VER las imágenes descargadas ──────────────────────────────
docker images # Lista todas las imágenes en tu máquina
docker image ls # Mismo resultado, otra forma de escribirlo
# ── VER las capas de una imagen ───────────────────────────────
docker history nginx # Muestra cómo se construyó la imagen paso a paso
# ── ELIMINAR imágenes ─────────────────────────────────────────
docker rmi nginx # Elimina la imagen nginx
docker rmi nginx:1.24 # Elimina una versión específica
docker image prune # Elimina imágenes que ya no usa ningún contenedor
Cómo leer el nombre de una imagen
# Formato: [registro/][usuario/]nombre[:tag]
nginx # Imagen oficial de Docker Hub, última versión
nginx:1.25 # Imagen oficial, versión 1.25 exacta
node:20-alpine # Node 20 sobre Alpine Linux (imagen muy ligera)
miusuario/mi-app:1.0 # Imagen tuya subida a Docker Hub con etiqueta "1.0"
# Las variantes más habituales:
# :latest → la más reciente (es la que se usa si no pones tag)
# :alpine → basada en Alpine Linux, muy ligera (~5MB vs ~200MB)
# :slim → versión recortada, más ligera que la completa
latest puede actualizarse sin previo aviso y
romper tu aplicación. Usa siempre versiones concretas como
node:20.11-alpine para que tu proyecto sea
reproducible. Para practicar está bien, para proyectos en
producción no.
📦 Gestionar contenedores
Aquí está el corazón del trabajo diario. En tu día a día vas a usar
docker run todo el tiempo.
¿Por qué hay tantas opciones? Porque al arrancar una
aplicación necesitas decirle al ordenador qué puertos abrir para que
podamos verla en el navegador, si debe ejecutarse en segundo plano
(para no bloquearnos la consola), o qué contraseñas inyectarle de
forma segura. No necesitas aprenderlas todas de golpe, las vas usando
según las necesitas.
docker run — todas las opciones importantes
# Lo más básico: ejecutar nginx
docker run nginx
# Problema: esto bloquea tu terminal. Cuando la cierras, el contenedor muere.
# ── -d (detached) → ejecutar en segundo plano ────────────────
docker run -d nginx
# Ahora corre en segundo plano y te devuelve el control de la terminal.
# ── --name → ponerle nombre al contenedor ────────────────────
docker run -d --name mi-nginx nginx
# Sin nombre, Docker asigna uno aleatorio (tipo "loving_einstein").
# Con nombre puedes referirte a él fácilmente en los demás comandos.
# ── -p → abrir un puerto (host:contenedor) ───────────────────
docker run -d -p 8080:80 nginx
# Ahora al abrir localhost:8080 en tu navegador ves nginx.
# El 8080 es el puerto de tu máquina, el 80 es el del contenedor.
# ── -it → modo interactivo (entrar dentro del contenedor) ────
docker run -it ubuntu bash
# Entras dentro del contenedor con una terminal bash.
# Es como conectarte por SSH a un servidor. Para salir: exit
# ── --rm → eliminar el contenedor al terminar ────────────────
docker run --rm ubuntu echo "Hola"
# Ejecuta el comando y limpia el contenedor solo. Ideal para tareas puntuales.
# ── -e → pasar variables de entorno ──────────────────────────
# Las apps leen configuración de variables de entorno en lugar
# de tener contraseñas escritas en el código (mucho más seguro).
docker run -d \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secreto \
postgres
# ── --env-file → cargar variables desde un archivo .env ──────
docker run --env-file .env mi-app
# Más limpio que poner muchos -e y no expone secretos en la terminal.
# ── Ejemplo real completo: PostgreSQL listo en segundos ──────
docker run -d \
--name mi-postgres \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=supersecret \
-e POSTGRES_DB=miapp \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
Ver y gestionar contenedores que ya están corriendo
# ── VER qué contenedores están en marcha ─────────────────────
docker ps # Solo los que están corriendo
docker ps -a # Todos, incluyendo los que están parados
# ── PARAR y ARRANCAR ─────────────────────────────────────────
docker stop mi-nginx # Para el contenedor (puede volver a arrancarse)
docker start mi-nginx # Lo arranca de nuevo
docker restart mi-nginx # Para y arranca en un solo comando
# ── ELIMINAR un contenedor ───────────────────────────────────
docker rm mi-nginx # Debe estar parado antes
docker rm -f mi-nginx # -f fuerza la eliminación aunque esté corriendo
docker container prune # Elimina todos los contenedores que estén parados
# ── VER los logs (mensajes que imprime la aplicación) ────────
docker logs mi-nginx # Muestra todos los logs
docker logs -f mi-nginx # -f (follow): sigue los logs en tiempo real
docker logs --tail 50 mi-nginx # Solo las últimas 50 líneas
# ── ENTRAR en un contenedor que ya está corriendo ────────────
docker exec -it mi-nginx bash # Abre una terminal bash dentro
docker exec -it mi-nginx sh # Usa "sh" si la imagen no tiene bash (ej. Alpine)
docker exec mi-nginx ls /etc # Ejecuta un solo comando sin quedarte dentro
# ── VER cuánta CPU y RAM consume cada contenedor ─────────────
docker stats # Se actualiza en tiempo real. Ctrl+C para salir.
# ── COPIAR archivos entre tu máquina y el contenedor ─────────
docker cp mi-nginx:/etc/nginx/nginx.conf . # Del contenedor a tu máquina
docker cp ./mi-config.conf mi-nginx:/etc/nginx/ # De tu máquina al contenedor
Buenas prácticas: Seguridad básica en contenedores
Al ejecutar contenedores en producción o servidores expuestos, nunca confíes plenamente en la configuración por defecto. Un par de banderas pueden salvarte de un gran dolor de cabeza:
# ── --read-only → Sistema de archivos inmutable ──────────────
docker run -d --read-only mi-app
# Evita que un atacante pueda instalar malware dentro del contenedor
# si logra colarse, porque no podrá escribir en el disco.
# ── Escaneo de vulnerabilidades ──────────────────────────────
docker scout cves mi-app:1.0
# Docker Scout revisa tu imagen y te dice si las librerías
# o el sistema base que usas tienen agujeros de seguridad conocidos.
docker system df. Para limpiar todo lo que no se usa:
docker system prune. Para ser más agresivo e incluir
imágenes: docker system prune -a.
📄 Escribir un Dockerfile
El Dockerfile es el archivo que define cómo se construye tu imagen personalizada. Cada línea es una instrucción que Docker ejecuta en orden. El resultado es una imagen lista para distribuir y ejecutar en cualquier sitio.
Instrucciones más importantes
| Instrucción | Para qué sirve | Ejemplo |
|---|---|---|
| FROM | Define la imagen base sobre la que construyes. Siempre es la primera instrucción. | FROM node:20-alpine |
| WORKDIR | Establece el directorio de trabajo dentro del contenedor. Todos los comandos siguientes se ejecutan desde ahí. | WORKDIR /app |
| COPY | Copia archivos de tu máquina al contenedor. | COPY package.json . |
| RUN | Ejecuta un comando durante la construcción (instalar dependencias, compilar, etc.). | RUN npm install |
| ENV | Define variables de entorno disponibles cuando el contenedor arranque. | ENV NODE_ENV=production |
| EXPOSE |
Documenta en qué puerto escucha la app. No abre el puerto — eso
lo hace -p en docker run.
|
EXPOSE 3000 |
| CMD | El comando que se ejecuta cuando arranca el contenedor. | CMD ["node", "index.js"] |
| USER | Cambia al usuario con el que se ejecutan los comandos siguientes. No usar root es buena práctica de seguridad. | USER node |
De básico a bien hecho — ejemplos reales
# Dockerfile básico para Node.js
# Funciona bien para aprender y para proyectos simples
FROM node:20-alpine
# Imagen base: Node.js 20 sobre Alpine Linux (imagen muy ligera)
WORKDIR /app
# Todos los comandos siguientes se ejecutan desde /app
COPY package.json package-lock.json ./
# Copiamos primero el package.json (motivo explicado en la pestaña siguiente)
RUN npm install
# Instalamos las dependencias
COPY . .
# Ahora copiamos el resto del código
EXPOSE 3000
CMD ["node", "src/index.js"]
# Cuando arranque el contenedor, ejecuta este comando
# Dockerfile optimizado — las mismas ideas pero con buenas prácticas
FROM node:20-alpine
WORKDIR /app
# Copiamos PRIMERO el package.json y LUEGO instalamos.
# ¿Por qué? Docker guarda en caché cada instrucción. Si tu código
# cambia pero package.json no, Docker reutiliza la capa del
# npm install y el build es mucho más rápido.
COPY package*.json ./
RUN npm ci --omit=dev
# npm ci es más estricto y reproducible que npm install.
# --omit=dev no instala devDependencies (imagen más pequeña).
COPY src/ ./src/
# Buena práctica de seguridad: ejecutar como usuario no-root.
# Si alguien compromete la app, no tiene permisos de root en el sistema.
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]
# Dockerfile para Python (Flask, FastAPI, Django…)
FROM python:3.12-slim
# :slim es una versión más ligera de Debian
# Configuración recomendada para Python en Docker:
# PYTHONDONTWRITEBYTECODE → no genera archivos .pyc innecesarios
# PYTHONUNBUFFERED → los logs aparecen inmediatamente en "docker logs"
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Instalamos dependencias primero (para aprovechar la caché de Docker)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
# Para FastAPI: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Multi-stage Build — el estándar para frontend (React, Angular, Vue, TS) en producción
# ── ETAPA 1: Construcción (Builder) ───────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
# Instalamos dependencias y compilamos el proyecto
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Esto genera una carpeta "dist" o "build" con HTML/CSS/JS estáticos.
# ── ETAPA 2: Producción (Servidor) ────────────────────────────
# 💡 Analogía: La Etapa 1 es la cocina (pesa mucho, hay harina por todas partes).
# La Etapa 2 es el comedor de lujo. Solo pasamos el plato ya cocinado (dist/).
# Nuestro cliente (el usuario final) nunca ve la cocina, por eso la imagen pesa tan poco.
FROM nginx:alpine
# Usamos Nginx, un servidor web súper rápido y ligero (~15MB).
# Al empezar una nueva instrucción FROM, dejamos atrás la imagen de Node.
# ¡No arrastramos el código fuente ni los node_modules a producción!
COPY --from=builder /app/dist /usr/share/nginx/html
# Copiamos SOLO los archivos generados en la etapa anterior.
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
El archivo .dockerignore
Igual que .gitignore le dice a Git qué archivos ignorar,
.dockerignore le dice a Docker qué no copiar dentro de la
imagen. Esto la hace más pequeña y evita incluir archivos sensibles
como contraseñas.
# Dependencias — se instalan dentro del contenedor
node_modules/
__pycache__/
.venv/
# Control de versiones — no hace falta dentro de la imagen
.git/
.gitignore
# Variables de entorno y secretos — NUNCA dentro de una imagen
.env
.env.*
*.key
*.pem
# Artefactos de desarrollo
*.log
coverage/
# El propio Dockerfile y compose no necesitan estar dentro
Dockerfile*
compose*.yaml
.dockerignore
Construir tu imagen
# El punto (.) indica que el Dockerfile está en la carpeta actual
docker build -t mi-app:1.0 .
# Sin caché (cuando algo va mal y quieres empezar limpio)
docker build --no-cache -t mi-app .
# Ver el progreso detallado paso a paso
docker build --progress=plain -t mi-app .
# Subir tu imagen a Docker Hub
docker tag mi-app:1.0 miusuario/mi-app:1.0 # Renombrar para Docker Hub
docker login # Autenticarse
docker push miusuario/mi-app:1.0 # Subir
.env con contraseñas o claves
de API dentro de la imagen, cualquiera que la descargue podrá
acceder a esos secretos. Pasa los secretos como variables de
entorno al ejecutar el contenedor, no en el Dockerfile.
💾 Volúmenes y persistencia de datos
Recuerda la regla de oro:
los contenedores tienen amnesia (son efímeros). Si
eliminas un contenedor o se reinicia por un error, todo lo que haya
guardado internamente desaparece como lágrimas en la lluvia.
¿Por qué esto es un problema? Imagina que
arrancas un contenedor de MySQL, guardas los usuarios de tu web, y
mañana borras el contenedor sin querer. Perderías toda la base de
datos entera. Para eso usamos volúmenes.
Los volúmenes resuelven esto almacenando datos en un lugar que sobrevive al ciclo de vida del contenedor. Hay dos tipos:
Volúmenes Docker (recomendado)
Docker gestiona dónde se guardan los datos. Son la opción recomendada para bases de datos y datos en producción. No necesitas saber en qué carpeta exacta se guardan.
-v nombre-volumen:/ruta/contenedor
Bind Mounts (para desarrollo)
Montas una carpeta de tu máquina directamente en el contenedor. Perfecto cuando quieres que los cambios en tu código se reflejen al instante dentro del contenedor.
-v /ruta/tu/maquina:/ruta/contenedor
# ── Crear y gestionar volúmenes ───────────────────────────────
docker volume create mis-datos # Crea un volumen llamado "mis-datos"
docker volume ls # Lista todos los volúmenes
docker volume inspect mis-datos # Ver detalles del volumen
docker volume rm mis-datos # Elimina el volumen y sus datos
docker volume prune # Elimina volúmenes que no usa ningún contenedor
# ── Volumen para persistir la base de datos ───────────────────
# Si eliminas y recreas el contenedor, los datos siguen en "pgdata".
docker run -d \
--name mi-postgres \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secreto \
postgres:16
# ── Bind mount para desarrollo ────────────────────────────────
# $(pwd) es tu carpeta actual en Linux/Mac. En Windows PowerShell usa ${PWD}.
# Al cambiar código en tu editor, el contenedor lo ve de inmediato.
docker run -d \
-v $(pwd):/app \
-p 3000:3000 \
mi-app-node
# ── Solo lectura (:ro) ────────────────────────────────────────
# El contenedor puede leer los archivos pero no modificarlos
docker run -v $(pwd)/config:/app/config:ro mi-app
🌐 Redes en Docker
Cuando tienes varios contenedores (tu aplicación en Node/Python y tu
base de datos en Postgres), estos necesitan
comunicarse entre sí de forma rápida y segura.
¿Por qué crear una red en vez de usar localhost?
Porque cada contenedor vive aislado en su propia "casa". Tu aplicación
intentará llamar a `localhost:5432`, pero llamará a *su propia puerta*
en vez de a la del contenedor de la base de datos. Para solucionar
esto creamos Redes Privadas donde todos están en el mismo bloque de
vecinos.
db como hostname — sin IPs ni configuración extra.
bridge (por defecto)
Red interna que Docker crea automáticamente. Los contenedores se ven entre ellos por IP, pero no por nombre. Funciona para pruebas simples.
Por defecto
Red personalizada (recomendada)
Una red que tú creas. Los contenedores se pueden encontrar por nombre — mucho más cómodo. Úsala siempre que tengas varios contenedores que deban comunicarse.
Recomendado
host
El contenedor comparte directamente la red de tu máquina. Sin aislamiento. Solo disponible en Linux.
Linux only
none
Sin red. El contenedor queda completamente aislado, sin acceso a Internet ni a otros contenedores.
Aislado
# ── Crear y listar redes ──────────────────────────────────────
docker network create mi-red # Crea una red llamada "mi-red"
docker network ls # Lista todas las redes existentes
docker network rm mi-red # Elimina la red
# ── Ejemplo completo: app + base de datos ─────────────────────
# Paso 1: crear la red
docker network create app-network
# Paso 2: arrancar la base de datos en esa red
docker run -d --name db \
--network app-network \
-e POSTGRES_PASSWORD=secreto \
postgres:16-alpine
# Paso 3: arrancar la app en la misma red
docker run -d --name backend \
--network app-network \
-e DATABASE_URL=postgresql://postgres:secreto@db:5432/miapp \
-p 3000:3000 \
mi-backend:latest
# En la URL de conexión usamos "db" como hostname.
# Docker resuelve "db" al contenedor que se llama "db" automáticamente.
# Sin la red personalizada, esto no funcionaría.
🗄️ Bases de Datos y Docker
Conectar tu aplicación a una base de datos es la duda número uno de cualquiera que empieza con Docker. Hay tres escenarios reales que vas a encontrarte en tu día a día. Vamos a verlos uno por uno.
Escenario 1: App y BBDD en Docker (El caso ideal)
Tanto tu aplicación como la base de datos corren dentro de contenedores de Docker. Solo tienes que asegurarte de meter a los dos en la misma Red (la sala de chat privada que vimos antes).
# 1. Crear la red
docker network create mi-red
# 2. Base de datos (se llama "db")
docker run -d --name db --network mi-red -e POSTGRES_PASSWORD=secreto postgres:16
# 3. Tu aplicación conecta a "db" en lugar de "localhost"
docker run -d --name app --network mi-red -e DATABASE_URL="postgresql://postgres:secreto@db:5432/bd" mi-app
Escenario 2: App en Docker y BBDD en tu PC (El clásico dolor de cabeza)
Tienes Postgres o MySQL instalado directamente en tu Windows/Mac, e
intentas que tu contenedor se conecte a localhost:5432.
¡Falla! Porque el localhost del
contenedor es su propio interior, no tu ordenador.
host.docker.internal. Sustituye "localhost" por esa
palabra mágica y todo funcionará.
# En lugar de usar localhost o 127.0.0.1:
# DATABASE_URL="postgresql://user:pass@localhost:5432/bd" ❌ MAL
# Usa host.docker.internal:
# DATABASE_URL="postgresql://user:pass@host.docker.internal:5432/bd" ✅ BIEN
docker run -d -e DATABASE_URL="postgresql://user:pass@host.docker.internal:5432/bd" mi-app
Escenario 3: App en Docker y BBDD en la Nube (Producción)
Tienes tu base de datos alojada en AWS, Supabase, PlanetScale o cualquier servicio externo. Tu contenedor solo necesita la URL de conexión que te da ese proveedor.
# Tu aplicación sale a internet de forma transparente
docker run -d -e DATABASE_URL="postgresql://usuario:pass@aws-rds-url.com:5432/bd" mi-app
🎼 Docker Compose
Cuando tu aplicación tiene varios contenedores (app + base de datos +
caché…), gestionarlos uno a uno con docker run se vuelve
tedioso. Docker Compose te permite definir todos los
contenedores en un solo archivo compose.yaml y
arrancarlos todos de una vez con un solo comando.
docker-compose.yml y el comando era
docker-compose (con guión). En las versiones modernas
es compose.yaml y docker compose (sin
guión). Ambos funcionan, pero usa el moderno si puedes.
Ejemplo: app Node.js + PostgreSQL + Redis
# "services" es la lista de contenedores que quieres levantar
services:
# ── Contenedor 1: tu aplicación backend ──────────────────────
backend:
build: . # Construye la imagen desde el Dockerfile de esta carpeta
ports:
- "3000:3000"
env_file:
- .env
volumes:
- ./src:/app/src # Bind mount: editas el código y el contenedor lo ve al momento
depends_on:
- db # Espera a que el contenedor "db" esté en marcha antes de arrancar
- redis
restart: unless-stopped # Se reinicia si falla, a menos que lo pares tú explícitamente
# ── Contenedor 2: base de datos ───────────────────────────────
db:
image: postgres:16-alpine # Usa esta imagen sin construir nada
env_file:
- .env # ¡Seguridad! Las contraseñas van en un archivo .env aparte
volumes:
- pgdata:/var/lib/postgresql/data # Volumen para que los datos persistan
# ── Contenedor 3: Redis (caché en memoria) ────────────────────
redis:
image: redis:7-alpine
# Declaración de los volúmenes que se usan arriba
volumes:
pgdata:
Comandos de Compose para el día a día
# ── ARRANCAR ─────────────────────────────────────────────────
docker compose up # Arranca y muestra los logs en pantalla
docker compose up -d # Arranca en segundo plano
docker compose up --build # Reconstruye las imágenes antes de arrancar
# ── PARAR ────────────────────────────────────────────────────
docker compose down # Para y elimina los contenedores
docker compose down -v # También elimina los volúmenes (¡cuidado, pierdes datos!)
docker compose stop # Solo para, sin eliminar (puedes arrancar de nuevo)
# ── VER ESTADO Y LOGS ────────────────────────────────────────
docker compose ps # Estado de cada servicio
docker compose logs # Logs de todos los servicios
docker compose logs -f backend # Seguir en tiempo real solo el backend
# ── EJECUTAR COMANDOS DENTRO ──────────────────────────────────
docker compose exec backend bash # Abrir shell en el backend
docker compose exec db psql -U admin # Conectar a la consola de PostgreSQL
# ── RECONSTRUIR ──────────────────────────────────────────────
docker compose build backend # Reconstruye solo la imagen del backend
Producción: Healthchecks y Límites (Avanzado)
Para que tu app no se caiga en producción, necesitas dos cosas: saber si el contenedor está sano (Healthchecks) y evitar que consuma toda la memoria del servidor (Resource Limits).
services:
backend:
image: mi-backend:1.0
depends_on:
db:
condition: service_healthy # Espera a que la BD esté 100% lista, no solo arrancada
deploy:
resources:
limits:
cpus: '0.5' # Máximo 50% de un núcleo del servidor
memory: 512M # Si la app intenta consumir más, Docker la reinicia
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin"] # Comando para verificar que la BD responde
interval: 10s # Cada cuánto tiempo hace la prueba
timeout: 5s # Tiempo máximo que espera respuesta
retries: 5 # Intentos antes de declararla "unhealthy" (rota)
compose.yaml con todos los servicios
que necesita tu proyecto.2. Ejecutas
docker compose up -d para arrancarlo
todo.3. Desarrollas tu código normalmente (con bind mount los cambios se reflejan al momento).
4. Cuando terminas o quieres limpiar:
docker compose down.
🚀 El salto final a Producción
Hasta ahora hemos visto cómo usar Docker para el desarrollo y para
despliegues sencillos. Pero, ¿qué pasa cuando tienes miles de usuarios
o necesitas alta disponibilidad? En la industria real,
docker compose up -d en un servidor (VPS) suele quedarse
corto porque si el servidor físico se cae, tu aplicación también.
Cómo escala la industria
El ciclo de vida real en la industria
Así es como se ve el flujo de trabajo completo desde que terminas tu código en local hasta que llega al servidor de producción:
# 1. En tu ordenador (o en GitHub Actions): Construyes la imagen
docker build -t mi-app:v1.0 .
# 2. Le pones la etiqueta del registro privado de tu empresa
docker tag mi-app:v1.0 registry.empresa.com/mi-app:v1.0
# 3. La subes a la nube (AWS ECR, Google Artifact Registry...)
docker push registry.empresa.com/mi-app:v1.0
# 4. El servidor de Producción la descarga y la ejecuta
# (Esto suele hacerlo automáticamente Docker Compose o Kubernetes)
docker pull registry.empresa.com/mi-app:v1.0
docker run -d -p 80:3000 registry.empresa.com/mi-app:v1.0
Registries Privados
En producción no construyes la imagen en el servidor final. Construyes la imagen en un sistema de CI/CD (GitHub Actions, GitLab CI) y la subes a un Registry privado (AWS ECR, Google Artifact Registry, Docker Hub Pro). El servidor solo la descarga y ejecuta.
Serverless Containers
Si no quieres administrar servidores (instalar Linux, actualizar, securizar), puedes usar servicios como AWS Fargate o Google Cloud Run. Les das tu imagen Docker, les dices "necesito 2 copias con 1GB RAM" y ellos se encargan de todo.
Kubernetes (K8s)
Cuando tienes un ecosistema con decenas de microservicios y requieres auto-escalado complejo, entra en juego un orquestador como Kubernetes. K8s toma tus contenedores Docker y los distribuye inteligentemente por un clúster de servidores.
🦭 Anexo: La alternativa Podman
A medida que te adentras en el mundo de los contenedores, seguramente escucharás hablar de Podman. Especialmente si trabajas en entornos Linux o corporativos estrictos.
¿Qué es Podman?
Podman (Pod Manager) es una herramienta de código abierto desarrollada principalmente por Red Hat. Sirve exactamente para lo mismo que Docker: gestionar contenedores e imágenes. De hecho, está diseñado para ser 100% compatible con Docker.
# Si sabes Docker, ya sabes Podman. Los comandos son idénticos:
podman run -d -p 80:80 nginx
podman ps
podman build -t mi-app .
Entonces, ¿por qué existe y en qué se diferencia?
1. Es "Rootless" por defecto
Por defecto, el motor de Docker requiere permisos de administrador (root). Si un atacante escapa de un contenedor en Docker, es root en la máquina. Podman ejecuta los contenedores con tu usuario normal, añadiendo una capa gigante de seguridad.
2. No tiene Daemon
Docker tiene un proceso en segundo plano (daemon) corriendo siempre para gestionar todo. Si ese proceso se cae, caen tus contenedores. Podman es una arquitectura sin daemon (daemonless); lanza los contenedores como procesos aislados de tu sistema operativo.
Dockerfile de internet están hechos para
él. Piensa en Podman como un "modo avanzado" al que puedes
cambiarte transparentemente el día que tu empresa necesite
seguridad extrema en servidores Linux.
⚡ CheatSheet — Referencia rápida
Todos los comandos organizados para encontrarlos en segundos. Guarda esta página como favorito.
docker pull imagen:tag docker images docker rmi imagen docker build -t nombre:tag . docker push usuario/imagen docker history imagen docker image prune -a
docker run -d --name x imagen docker run -it imagen bash docker run --rm imagen cmd docker run -p 8080:80 imagen docker run -e KEY=val imagen docker run -v vol:/ruta imagen docker run --network red imagen
docker ps docker ps -a docker stop nombre docker start nombre docker restart nombre docker rm -f nombre docker container prune
docker logs -f nombre docker exec -it nombre bash docker inspect nombre docker stats docker top nombre docker port nombre docker cp nombre:/ruta .
docker volume create nombre docker volume ls docker volume rm nombre docker volume inspect nombre docker volume prune # En docker run: -v nombre:/ruta/contenedor -v $(pwd):/ruta/contenedor
docker network create red docker network ls docker network rm red docker network inspect red docker network connect red cnt docker network prune # En docker run: --network nombre-red
docker compose up -d docker compose up --build docker compose down docker compose down -v docker compose ps docker compose logs -f docker compose exec svc bash docker compose build
docker system df # Ver cuánto ocupa docker system prune # Limpiar lo no usado docker system prune -a # Más agresivo docker container prune docker image prune -a docker volume prune docker network prune docker builder prune
Opciones de docker run — referencia rápida
| Flag | Qué hace | Ejemplo |
|---|---|---|
| -d | Ejecutar en segundo plano (no bloquea la terminal) | docker run -d nginx |
| -it | Modo interactivo con terminal (para entrar dentro) | docker run -it ubuntu bash |
| --rm | Eliminar el contenedor automáticamente al terminar | docker run --rm alpine echo hi |
| --name | Ponerle nombre al contenedor | docker run --name web nginx |
| -p host:cont | Abrir y mapear un puerto (tu máquina → contenedor) | -p 8080:80 |
| -e KEY=VAL | Pasar una variable de entorno | -e NODE_ENV=prod |
| --env-file | Cargar variables de entorno desde un archivo | --env-file .env |
| -v vol:/ruta | Montar un volumen Docker | -v pgdata:/var/lib/pgsql |
| -v $(pwd):/app | Montar carpeta de tu máquina (bind mount) | -v $(pwd)/src:/app/src |
| --network | Conectar a una red específica | --network mi-red |
| --restart | Política de reinicio automático si el contenedor falla | --restart unless-stopped |