"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:
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.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?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í!)
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.
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
Error ... failed to bind host port 0.0.0.0:8080: address already in use (o port is already allocated)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.
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)
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.
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
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ó.
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
docker rm tu stack, levántalo de nuevo, ¿sigue todo ahí? Haz esa prueba antes de que producción la haga por ti.
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)
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!)
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.