Docker en 10 Minutos

"Funciona en mi máquina" solía ser un chiste. Docker lo convirtió en estrategia de despliegue: empaquetar la máquina junto con la app. Esta página construye el modelo mental en el orden correcto — imagen vs contenedor primero, todo lo demás después — más las tres sorpresas que emboscan a cada principiante: el error de puerto, los datos que desaparecen, y los 40GB de disco que no sabías que estabas gastando.

🎙️ Publicado y grabado:

01El problema: "funciona en mi máquina"

Tu app no solo necesita tu código — necesita una versión de Python, bibliotecas del sistema, configuración, variables de entorno. Desplegar solía significar reconstruir toda esa pila en otra máquina y rezar. Un contenedor empaqueta la app con todo su entorno en una caja sellada y portátil que se ejecuta idéntica en cualquier sitio. Misma idea que los contenedores de carga: nadie desembala tus muebles en cada puerto.

# toda la propuesta, en un comando — ejecutar redis sin instalar redis:
docker run redis

# sin apt install, sin archivos de config, sin conflictos de versión con
# el redis que tu OTRO proyecto necesita. caja sellada.
El primer error que encontrarás
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Docker es un cliente y un servicio en segundo plano (el daemon). Este error significa que el servicio no está corriendo — en Mac/Windows, Docker Desktop simplemente no está abierto. Ábrelo, espera a que el icono de la ballena se estabilice, reintenta. No está roto; solo dormido.

02Imagen vs contenedor — la única distinción que importa

Entiende esto bien y Docker deja de ser confuso. Una imagen es una plantilla congelada — de solo lectura, compartible, versionada. Un contenedor es una instancia en ejecución de una imagen. Receta y pastel. Clase y objeto. Una imagen puede crear veinte contenedores, cada uno con su propio estado, y eliminar un contenedor nunca daña la imagen.

docker pull python:3.12      # descargar una imagen (la plantilla)
docker images                # listar plantillas en esta máquina

docker run -it python:3.12   # hornear un pastel: imagen → contenedor en ejecución
docker ps                    # listar contenedores EN EJECUCIÓN
docker ps -a                 # ...incluyendo los detenidos (¡se quedan ahí!)
Decodificando los nombres
python:3.12 — nombre, dos puntos, tag (la versión). Omitir el tag significa :latest, que es la versión "sistema de honor": "latest" es lo que el autor subió de último, y puede cambiar silenciosamente entre pulls. En cualquier cosa seria, fija el tag — python:3.12, no python.

03docker run: los flags que importan

El noventa por ciento de docker run son cuatro flags: -d (ejecutar en segundo plano), --name (para poder referenciarlo), -p (conectar un puerto a través de la pared de la caja), -e (pasar variables de entorno). El de puertos merece un diagrama, porque todo el mundo lo lee al revés la primera vez:

docker run -d --name web -p 8080:80 -e MODE=prod nginx

-p 8080:80
   └──┬─┘└┬┘
   el puerto de TU máquina : el puerto del CONTENEDOR
   "peticiones a localhost:8080 → dentro de la caja al :80"
   mnemotécnico: host primero, caja segundo — fuera antes que dentro
Te va a pasar
Error ... failed to bind host port 0.0.0.0:8080: address already in use  (o port is already allocated)
Algo ya ocupa el 8080 en tu máquina — muy a menudo una copia anterior del mismo contenedor todavía corriendo. docker ps para encontrarlo, docker stop para pararlo, o simplemente mapea un puerto de host diferente: -p 8081:80. El puerto interno del contenedor nunca entra en conflicto; solo el lado del host.

04Mirar dentro de un contenedor en ejecución

Tres comandos cubren toda la depuración: logs (qué imprimió), exec (abrir un shell dentro de la caja), stop/rm (terminarlo). Si un contenedor "no funciona", la respuesta está en los logs el 95% de las veces — los contenedores se paran en cuanto su proceso principal sale, así que un crash al arrancar parece "simplemente se detuvo".

docker logs web              # todo lo que imprimió
docker logs -f web           # seguir en vivo (como tail -f)

docker exec -it web sh       # un shell DENTRO del contenedor
  # explora: ls, cat config, ps — luego exit

docker stop web              # parada cortés
docker rm web                # eliminar el contenedor (detenido)
El misterio de "se para inmediatamente"
Haces docker run de algo, y docker ps no muestra nada. No se fue — crasheó o terminó al instante. docker ps -a lo muestra con un código de salida; docker logs <nombre> muestra por qué. Un contenedor vive exactamente lo que dura su proceso principal — no es una VM en segundo plano, es un proceso en una caja.

05Dockerfile: tu propia imagen, como receta

Un Dockerfile es la receta que construye una imagen: parte de una base, copia archivos, ejecuta comandos de instalación, declara qué se ejecuta. Cinco instrucciones cubren la mayoría de Dockerfiles reales que leerás y escribirás:

FROM python:3.12-slim          # partir de esta imagen base
WORKDIR /app                   # cd (y mkdir) dentro de la imagen
COPY requirements.txt .        # copiar lista de deps primero — ¡ver siguiente sección!
RUN pip install -r requirements.txt
COPY . .                       # ahora el resto del código
CMD ["python", "app.py"]       # qué se ejecuta al iniciar un contenedor
docker build -t myapp:1.0 .    # hornearlo (-t = nombre:tag, . = aquí)
docker run -p 8000:8000 myapp:1.0

06Capas y caché: por qué importa ese orden de COPY

Cada línea del Dockerfile crea una capa, y Docker cachea cada una: si una línea y todo lo anterior no cambió, se reutiliza al instante. Una regla se deriva, y es la mayor optimización de Dockerfile: ordena las líneas de lo que menos cambia a lo que más cambia. Por eso las dependencias se copian antes que el código.

# ✗ orden ingenuo — cambias una línea de código,
#   y pip reinstala TODO (5 min por build):
COPY . .
RUN pip install -r requirements.txt

# ✓ amigable con la caché — la capa de deps solo se
#   reconstruye cuando requirements.txt cambia (build: ~2 seg):
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Esto también explica por qué imágenes de la misma base comparten espacio en disco, y por qué el primer build es lento pero el segundo es instantáneo. La caché no es magia — es un stack de diffs congelados, invalidado de arriba abajo desde la primera línea que cambió.

07Volúmenes: dónde van tus datos realmente

Aquí está la sorpresa que le cuesta datos reales a la gente: el sistema de archivos de un contenedor muere con el contenedor. Ejecutas una base de datos en un contenedor, escribes un mes de registros, docker rm — todo desapareció. Un volumen es un disco persistente que vive fuera del contenedor y se monta dentro. Las bases de datos siempre llevan volúmenes.

# volumen con nombre — Docker gestiona dónde vive
docker run -d --name db \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

# bind mount — TU carpeta aparece dentro de la caja
# (así es como los entornos de dev recargan código en vivo)
docker run -v ./src:/app/src myapp

docker volume ls             # ver tus volúmenes
Dilo una vez, recuérdalo siempre
Los contenedores son ganado, los volúmenes son el establo. Deberías poder eliminar y recrear cualquier contenedor sin perder nada — si eso no es verdad, algún estado vive dentro de un contenedor que no debería. La prueba: docker rm tu stack, levántalo de nuevo, ¿sigue todo ahí? Haz esa prueba antes de que producción la haga por ti.

08Compose: todo tu stack en un archivo

Las apps reales son varios contenedores — app, base de datos, caché. docker compose lee un archivo YAML que los describe todos y gestiona el conjunto como una unidad. Magia extra: los contenedores en un archivo compose se encuentran por nombre de servicio — tu app se conecta a db:5432, no a una dirección IP.

# compose.yaml
services:
  web:
    build: .
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
  db:                          # ← "db" también es su hostname
    image: postgres:16
    volumes: ["pgdata:/var/lib/postgresql/data"]
volumes:
  pgdata:
docker compose up -d         # arrancar todo
docker compose logs -f web   # seguir los logs de un servicio
docker compose down          # parar y eliminar (los volúmenes sobreviven)

09Limpieza: los 40GB que no sabías

Docker nunca elimina nada por su cuenta. Cada imagen descargada, cada contenedor detenido, cada capa de build huérfana se queda en disco — en silencio, para siempre. Seis meses después, "¿por qué está lleno mi disco?" tiene una respuesta de una palabra. Dos comandos lo gestionan:

docker system df             # ¿cuánto espacio está consumiendo Docker?

docker system prune          # eliminar contenedores parados + imágenes colgantes
docker system prune -a       # ...más TODAS las imágenes sin uso (agresivo)

# prune nunca toca: contenedores corriendo, sus imágenes, volúmenes.
# los volúmenes necesitan: docker volume prune (cuidado — ¡datos!)

10Chuleta

Organizada por la frase que tienes en la cabeza.

# "ejecutar esto"
docker run -d --name x -p 8080:80 image:tag

# "¿qué está corriendo / qué acaba de morir?"
docker ps · docker ps -a

# "¿por qué está roto?"
docker logs -f x

# "déjame mirar dentro"
docker exec -it x sh

# "port is already allocated"
docker ps → para el viejo, o -p 8081:80

# "mi build es lento"
copiar archivo de deps → instalar → DESPUÉS copiar código

# "¿dónde fueron mis datos?" (se pregunta una vez, nunca más)
volúmenes para todo lo que deba sobrevivir: -v nombre:/ruta

# "disco lleno"
docker system df → docker system prune

# "arrancar/parar todo el stack"
docker compose up -d · docker compose down

Eso es Docker funcional. Los conceptos van profundo (namespaces, cgroups, registries) pero esta página es lo que usas cada semana. Además combina naturalmente con el resto del stack: los contenedores son procesos Linux — la línea de comandos aplica dentro de ellos, y tu Dockerfile trackeado en git es toda la historia de despliegue.