🚀 Guía para empezar · De 0 al día a día

Aprende Docker
desde cero

Por Francesc Fosas (con la ayuda de Gemini)

Todo lo que necesitas para entender qué es Docker, cómo funciona y usarlo con soltura — sin necesidad de ser experto.

13Secciones
60+Comandos
0 → ✓Nivel
01

🐳 ¿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.

Sin Docker — el problema
Tu App
+
Node 18
+
libssl 1.1
💥 Funciona aquí, falla allá
Con Docker — la solución
📦 Contenedor (todo dentro)
✅ Tu Mac
+
✅ Linux server
+
✅ AWS / GCP
🔒

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
02

⚙️ 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.

💡
¿Docker Desktop o Docker Engine?
El secreto que debes saber: Docker usa tecnologías nativas de Linux para funcionar. Por eso, si instalas Docker Desktop en Windows o Mac, lo que realmente hace es instalar una pequeña máquina virtual de Linux invisible en segundo plano, además de darte una interfaz gráfica bonita. Si estás en Linux, no necesitas la máquina virtual, por eso se suele instalar directamente Docker Engine (solo el motor).
# 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

Terminal
# 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! 🎉
03

🧠 Conceptos clave

Docker tiene su propio vocabulario. Entiende estos 5 conceptos y todo lo demás encajará solo.

1

🖼️ 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).
2

📦 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.
3

📄 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 build procesas ese archivo y obtienes tu imagen.
4

💾 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.
5

🌐 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 db como dirección — sin necesidad de conocer IPs.

Cómo encajan todas las piezas

Tú escribes
📄 Dockerfile
↓ docker build
Se genera
🖼️ Imagen
↓ docker run
Se ejecutan
📦 Contenedor 1
📦 Contenedor 2
📦 Contenedor 3
Con acceso a
💾 Volúmenes
🌐 Redes
🔧 Variables de entorno
04

🖼️ 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.

ℹ️
¿Qué es Docker Hub?
🌐 Explorar hub.docker.com
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.
Gestión de imágenes
# ── 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

Anatomía del nombre
# 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
⚠️
Evita :latest en proyectos reales
El tag 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.
05

📦 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

docker run
# 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

Comandos del día a día
# ── 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:

Seguridad
# ── --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.
🧹
Limpiar cuando Docker ocupa mucho espacio en disco
Con el tiempo, Docker acumula imágenes viejas, contenedores parados y otros restos. Para ver cuánto ocupa: 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.
06

📄 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.

.dockerignore
# 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

docker build
# 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
🔐
Nunca incluyas secretos dentro de una imagen
Las imágenes pueden compartirse y su historial de capas es accesible. Si copias un .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.
07

💾 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
Trabajar con volúmenes
# ── 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
08

🌐 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.

💡
Lo más importante
Si creas una red propia, los contenedores en esa red pueden encontrarse por nombre. Tu app puede conectarse a la base de datos usando simplemente 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
Ejemplo: app + base de datos conectadas
# ── 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.
09

🗄️ 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).

Misma red
# 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.

🎩
El truco de magia: host.docker.internal
Para que un contenedor pueda salir de su "casa" y hablar con la máquina que lo está hospedando (tu ordenador real), Docker proporciona una dirección especial: host.docker.internal. Sustituye "localhost" por esa palabra mágica y todo funcionará.
Conectando al host
# 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.

Conexión externa
# 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
10

🎼 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.

ℹ️
compose.yaml vs docker-compose.yml
Verás los dos nombres. El archivo se llamaba 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

compose.yaml
# "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

docker compose
# ── 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).

compose.prod.yaml
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)
El flujo típico de trabajo con Compose
1. Escribes tu 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.
11

🚀 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:

CI / CD Flow
# 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.

🎯
No necesitas Kubernetes el primer día
Muchos proyectos pequeños y medianos funcionan durante años con Docker Compose o un servicio Serverless básico. Conocer Docker es el primer paso obligatorio; K8s es una herramienta avanzada que resolverá problemas de escala cuando los tengas.
12

🦭 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.

alias docker=podman
# 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.

💡
¿Debería aprender Podman en lugar de Docker?
No. Aprende Docker primero porque es el estándar de la industria, las ofertas de trabajo lo piden, y el 99% de los tutoriales y 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.
13

⚡ CheatSheet — Referencia rápida

Todos los comandos organizados para encontrarlos en segundos. Guarda esta página como favorito.

🖼️ Imágenes
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
📦 Contenedores — arrancar
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
📦 Contenedores — gestión
docker ps
docker ps   -a
docker stop    nombre
docker start   nombre
docker restart nombre
docker rm   -f nombre
docker container prune
🔍 Inspección y debug
docker logs    -f nombre
docker exec  -it nombre bash
docker inspect     nombre
docker stats
docker top         nombre
docker port        nombre
docker cp nombre:/ruta .
💾 Volúmenes
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
🌐 Redes
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
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
🧹 Limpiar espacio en disco
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

🎉
¡Ya tienes todo lo necesario para el día a día!
Con lo visto en esta guía cubres el 95% de lo que vas a necesitar en proyectos reales. Cuando ya te manejes con soltura y quieras profundizar más, la documentación oficial en docs.docker.com es excelente y muy completa.