Ahora mismo tienes tres o cuatro contenedores en tu máquina y todavía puedes llevarlos en la cabeza. Dentro de un mes tendrás treinta: los cuatro servicios de Aurora Libros, media docena de pruebas olvidadas, contenedores de otros proyectos y un montón de Exited de hace semanas ocupando disco y nombres. Sin herramientas de gestión, ese docker ps -a se convierte en una pared de texto imposible de leer, y el docker rm masivo que ejecutas a las once de la noche se lleva por delante algo que necesitabas.
Esta lección es la de la operación diaria. Vas a exprimir docker ps hasta el fondo —columna por columna, con sus filtros y sus plantillas—, vas a montar un panel de control de los cuatro servicios de Aurora Libros, vas a aprender a componer comandos con -q para operar sobre decenas de contenedores sin destrozar nada, a copiar ficheros entre el host y un contenedor con docker cp —con lo que por fin llenarás la tabla libros con los ocho títulos—, a ver con docker diff qué ha cambiado un contenedor respecto a su imagen, y a entender por qué docker commit es una tentación que hay que resistir.
Contenido
docker ps: las siete columnas, una a una- Opciones de listado:
-a,-q,-s,-n,-l - Filtrar con
--filter - Dar formato con
--formaty plantillas Go - Etiquetas: organizar la flota con
--label - Componer comandos con
-q(y hacerlo con cabeza) - Borrar contenedores:
rm,-f,-vycontainer prune - Renombrar y modificar en caliente:
renameyupdate docker cp: mover ficheros en los dos sentidosdocker diff: qué ha cambiado respecto a la imagendocker commity por qué no debes usarlo- El panel de control de Aurora Libros
docker ps: las siete columnas, una a una
docker ps: las siete columnas, una a unaCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f8a1c9e7b2d postgres:16-alpine "docker-entrypoint.s…" 35 minutes ago Up 35 minutes 127.0.0.1:5432->5432/tcp aurora-db
9d4b7e2f1a6c redis:7-alpine "docker-entrypoint.s…" 32 minutes ago Up 4 minutes (healthy) 127.0.0.1:6379->6379/tcp aurora-cache| Columna | Qué es exactamente | Detalles que conviene saber |
|---|---|---|
| CONTAINER ID | Los 12 primeros caracteres del ID de 64 | Basta con escribir los primeros que sean únicos: docker stop 3f8 funciona |
| IMAGE | La referencia de la imagen tal y como se escribió | Si arrancaste con un digest, verás el digest; si la etiqueta se movió después, este texto ya no dice la verdad |
| COMMAND | El comando efectivo: ENTRYPOINT + CMD |
Aparece truncado con …. Se ve entero con --no-trunc |
| CREATED | Cuándo se creó el contenedor | No es cuándo se arrancó: un contenedor creado hace un mes y arrancado hace un minuto pone "5 weeks ago" |
| STATUS | Estado y tiempo en él | Up 35 minutes, Exited (0) 2 hours ago, Up 4 minutes (healthy), Created, Paused |
| PORTS | Publicaciones activas | 127.0.0.1:5432->5432/tcp es host→contenedor. Si solo pone 5432/tcp, está expuesto pero no publicado |
| NAMES | El nombre asignado o inventado | Único en la máquina |
Dos observaciones sobre la salida de arriba, que sirven de repaso:
aurora-cacheponeUp 4 minutesy noUp 32 minutesporque lo reiniciaste en la lección anterior: el contador deSTATUSse pone a cero con cada arranque, mientras queCREATEDno se mueve.- El
(healthy)deaurora-cacheno aparece por arte de magia: la imagen oficial de Redis 7 trae su propioHEALTHCHECK.aurora-dbno muestra nada porque la imagen de PostgreSQL no lo define. La salud se estudia a fondo en la lección 03-04.
Para ver el comando completo:
- Opciones de listado:
-a, -q, -s, -n, -l
-a, -q, -s, -n, -l| Opción | Qué hace | Uso típico |
|---|---|---|
-a, --all |
Muestra todos, incluidos Exited y Created |
Encontrar el contenedor que murió |
-q, --quiet |
Solo los IDs, uno por línea | Componer con otros comandos |
-s, --size |
Añade la columna SIZE |
Saber cuánto ocupa de verdad |
-n N |
Los N últimos creados, corriendo o no | docker ps -n 3 |
-l, --latest |
El último creado | docker logs $(docker ps -lq) |
--no-trunc |
No trunca IDs ni comandos | Copiar un ID completo |
-s: el tamaño real de un contenedor
NAMES IMAGE SIZE
aurora-cache redis:7-alpine 0B (virtual 41.2MB)
aurora-db postgres:16-alpine 127kB (virtual 278MB)Esta columna tiene dos números y la diferencia entre ambos es la lección 01-05 hecha comando:
- El primero (
0B,127kB) es la capa de escritura: lo que ese contenedor ha escrito por encima de la imagen. Es lo único que le pertenece en exclusiva y lo único que se pierde al borrarlo. - El
virtuales el tamaño total que ve el contenedor: su capa de escritura más todas las capas de la imagen, que son compartidas y de solo lectura.
Diez contenedores de redis:7-alpine no ocupan 412 MB, sino 41,2 MB más diez capas de escritura minúsculas. Y aurora-db con sus 127 kB de escritura llama la atención: acabas de crear una base de datos entera, ¿dónde están los datos? En un volumen anónimo que la imagen oficial declara, y que no cuenta como capa de escritura. Sujeta esa pregunta: es el corazón de la lección 03-06.
- Filtrar con
--filter
--filter--filter (o -f) acepta pares clave=valor y se puede repetir. Los filtros disponibles:
| Filtro | Ejemplo | Qué selecciona |
|---|---|---|
status |
--filter status=exited |
Por estado: created, running, paused, restarting, exited, dead |
name |
--filter name=aurora |
Nombre que contiene esa cadena (es una subcadena, no un igual) |
ancestor |
--filter ancestor=redis:7-alpine |
Contenedores creados a partir de esa imagen |
label |
--filter label=proyecto=aurora-libros |
Por etiqueta, con o sin valor |
exited |
--filter exited=1 |
Los que salieron con ese código |
health |
--filter health=unhealthy |
Por estado de salud: starting, healthy, unhealthy, none |
before / since |
--filter since=aurora-db |
Creados antes/después de ese contenedor |
id |
--filter id=3f8a1c9e7b2d |
Por ID |
volume |
--filter volume=aurora-datos |
Los que montan ese volumen (lección 03-06) |
network |
--filter network=aurora-net |
Los conectados a esa red (lección 03-05) |
publish / expose |
--filter publish=5432 |
Los que publican o exponen ese puerto |
Recetas listas para copiar:
# Todo lo que sea de Aurora Libros, vivo o muerto
docker ps -a --filter name=aurora
# Solo lo que está corriendo de una imagen concreta
docker ps --filter ancestor=postgres:16-alpine
# Contenedores que fallaron: salieron con un código distinto de 0
docker ps -a --filter status=exited --filter exited=1
# Contenedores enfermos ahora mismo
docker ps --filter health=unhealthy
# Lo creado después de aurora-db (útil para acotar una sesión de trabajo)
docker ps -a --filter since=aurora-dbEjemplo real:
Dos avisos sobre el comportamiento de los filtros:
- Varios
--filterdistintos se combinan con Y lógico.--filter status=running --filter name=auroraexige las dos condiciones. - Varios valores del mismo filtro se combinan con O lógico.
--filter status=exited --filter status=createddevuelve los de cualquiera de los dos estados. namees una subcadena, no una igualdad.--filter name=aurora-dbtambién encuentraaurora-db-copia. Si necesitas exactitud, usa una expresión anclada:--filter name=^aurora-db$.
- Dar formato con
--format y plantillas Go
--format y plantillas Go--format usa plantillas del lenguaje Go, las mismas que ya empleaste con docker image ls en la lección 02-05.
| Marcador | Contenido |
|---|---|
{{.ID}} |
ID corto |
{{.Names}} |
Nombre |
{{.Image}} |
Imagen |
{{.Command}} |
Comando efectivo |
{{.CreatedAt}} |
Fecha completa de creación |
{{.RunningFor}} |
Tiempo transcurrido en texto |
{{.Status}} |
Estado con su tiempo |
{{.State}} |
Solo el estado (running, exited…) |
{{.Ports}} |
Puertos publicados |
{{.Size}} |
Tamaño (requiere -s) |
{{.Labels}} |
Todas las etiquetas |
{{.Label "clave"}} |
El valor de una etiqueta |
{{.Mounts}} |
Volúmenes montados |
{{.Networks}} |
Redes conectadas |
La palabra table al principio activa la cabecera y la alineación en columnas:
Sin table, la salida es texto libre, perfecta para guionizar:
Y hay dos formatos preconstruidos muy socorridos:
docker ps --format json | head -1
docker ps --format "{{json .}}" | jq -r '.Names + " | " + .Status'{"Command":"\"docker-entrypoint.s…\"","CreatedAt":"2026-08-04 19:28:41 +0200 CEST","ID":"3f8a1c9e7b2d",...}
aurora-db | Up 41 minutes
aurora-cache | Up 10 minutesSi un formato te gusta, hazlo permanente en ~/.docker/config.json:
A partir de ese momento, un docker ps a secas usa tu formato. Es uno de los ajustes que más agradece el uso diario.
- Etiquetas: organizar la flota con
--label
--labelCuando en la misma máquina conviven contenedores de tres proyectos, filtrar por nombre deja de ser fiable. Las etiquetas son metadatos arbitrarios que asignas al crear el contenedor:
docker run -d --name prueba-etiquetas \
--label proyecto=aurora-libros \
--label componente=demo \
--label entorno=local \
alpine:3.20 sleep 300Y después filtras por ellas con precisión quirúrgica:
docker ps --filter label=proyecto=aurora-libros --format "{{.Names}}"
docker ps --filter label=componente --format "{{.Names}}"
docker rm -f prueba-etiquetas--filter label=clave=valor exige el valor exacto; --filter label=clave solo exige que la etiqueta exista.
Vamos a etiquetar de verdad la flota de Aurora Libros. Como las etiquetas, igual que los puertos y las variables, se fijan al crear el contenedor y no se pueden añadir después (lección 03-01), hay que recrear. No pasa nada: aurora-db todavía no tiene datos que perder.
docker rm -f aurora-db aurora-cache
docker run -d --name aurora-db \
--label proyecto=aurora-libros \
--label componente=base-de-datos \
--label entorno=local \
-e POSTGRES_USER=aurora \
-e POSTGRES_PASSWORD=aurora_secreta \
-e POSTGRES_DB=aurora_libros \
-p 127.0.0.1:5432:5432 \
postgres:16-alpine
docker run -d --name aurora-cache \
--label proyecto=aurora-libros \
--label componente=cache \
--label entorno=local \
-p 127.0.0.1:6379:6379 \
redis:7-alpinedocker ps --filter label=proyecto=aurora-libros \
--format 'table {{.Names}}\t{{.Label "componente"}}\t{{.Status}}'Una convención de etiquetas que funciona bien y que usaremos en todo el curso:
| Etiqueta | Valores en Aurora Libros | Para qué sirve |
|---|---|---|
proyecto |
aurora-libros |
Separar de otros proyectos de la máquina |
componente |
base-de-datos, cache, api, web |
Identificar el papel de cada contenedor |
entorno |
local, pruebas, produccion |
Distinguir instancias del mismo servicio |
Con estas tres etiquetas, operaciones como "para todo lo de Aurora Libros en local sin tocar nada más" se vuelven triviales, y es exactamente lo que Docker Compose hará por ti automáticamente en el módulo 4: sus etiquetas com.docker.compose.project son este mismo mecanismo.
- Componer comandos con
-q (y hacerlo con cabeza)
-q (y hacerlo con cabeza)docker ps -q imprime solo IDs, y eso lo convierte en la entrada de cualquier otro comando:
Recetas que se usan a diario:
# Parar todos los contenedores de una imagen concreta
docker stop $(docker ps -q --filter ancestor=postgres:16-alpine)
# Borrar todos los contenedores parados
docker rm $(docker ps -aq --filter status=exited)
# Parar toda la flota de Aurora Libros
docker stop $(docker ps -q --filter label=proyecto=aurora-libros)
# Reiniciar solo los que están enfermos
docker restart $(docker ps -q --filter health=unhealthy)Y ahora las tres precauciones, porque estos comandos son afilados:
Primera: si el filtro no encuentra nada, el comando falla.
Molesto pero inofensivo. Se evita comprobando antes:
IDS=$(docker ps -q --filter label=proyecto=aurora-libros)
[ -n "$IDS" ] && docker stop $IDS || echo "No hay nada que parar"Segunda: docker rm -f $(docker ps -aq) es el comando más peligroso de esta lección. Borra absolutamente todos los contenedores de la máquina, incluidos los de otros proyectos, y sin preguntar. Si necesitas hacer limpieza, hazla acotada:
Tercera: mira antes de disparar. La regla de oro es ejecutar primero la versión que solo lista:
# 1. ¿Qué voy a tocar exactamente?
docker ps -a --filter status=exited --format "table {{.Names}}\t{{.Status}}"
# 2. Si la lista es la que esperaba, entonces sí
docker rm $(docker ps -aq --filter status=exited)Ese medio segundo de más ha salvado muchas bases de datos de desarrollo.
- Borrar contenedores:
rm, -f, -v y container prune
rm, -f, -v y container prunedocker rm solo funciona sobre contenedores parados. Con uno en marcha:
Error response from daemon: cannot remove container "aurora-cache":
container is running: stop the container before removing or force remove| Opción | Efecto |
|---|---|
| (ninguna) | Solo borra contenedores parados |
-f, --force |
Envía SIGKILL y borra. Sin periodo de gracia |
-v, --volumes |
Borra además los volúmenes anónimos asociados (nunca los que tienen nombre) |
-l, --link |
Elimina un enlace heredado, no el contenedor. Prácticamente en desuso |
Sobre -f, un matiz importante que enlaza con la lección anterior: docker rm -f no es docker stop + docker rm. Envía SIGKILL directamente, sin los diez segundos de gracia. Sobre una base de datos, eso es desenchufar el servidor. La secuencia correcta cuando hay datos de por medio es:
Qué ocurre exactamente al borrar un contenedor:
| Se destruye | Sobrevive |
|---|---|
| La capa de escritura y todo lo que hubiera en ella | La imagen de la que salió |
| Los logs del contenedor | Los volúmenes con nombre (lección 03-06) |
| Su configuración y su nombre | Los ficheros del host montados como bind mount |
Los volúmenes anónimos, solo si usas -v |
Los volúmenes anónimos, si no usas -v (quedan huérfanos) |
docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
7d3f9a1b2c48e6015a2c9f7b3e1d8a4c6f0b2d9e7a5c3f1b8d6a4e2c9f7b5d3a
Total reclaimed space: 41.9kBBorra todos los contenedores parados. Vale la pena repetir el aviso de la lección 02-05: un contenedor parado puede ser algo que estabas depurando o una base de datos de desarrollo que apagaste ayer. Acota siempre que puedas:
# Solo los parados hace más de 24 horas
docker container prune --filter "until=24h"
# Solo los parados de un proyecto concreto
docker container prune --filter "label=proyecto=aurora-libros"
# Sin confirmación interactiva (para scripts; úsalo con doble cuidado)
docker container prune -f --filter "until=168h"
- Renombrar y modificar en caliente:
rename y update
rename y updateYa viste docker rename en la lección anterior. Su compañero es docker update, que sí puede cambiar cosas de un contenedor en marcha, aunque una lista muy concreta:
docker update --restart=unless-stopped aurora-db
docker update --memory 512m --memory-swap 512m aurora-dbSe puede cambiar con update |
No se puede cambiar nunca |
|---|---|
Memoria (--memory, --memory-swap, --memory-reservation) |
Puertos publicados |
CPU (--cpus, --cpu-shares, --cpuset-cpus) |
Variables de entorno |
Número de procesos (--pids-limit) |
Montajes y volúmenes |
Política de reinicio (--restart) |
Imagen, CMD o ENTRYPOINT |
Peso de E/S de disco (--blkio-weight) |
Etiquetas y redes |
El detalle de todas esas opciones de recursos y de las políticas de reinicio es el contenido de la lección 03-07; aquí solo importa retener la frontera: update toca lo que vive en los cgroups; todo lo que vive en los namespaces (red, montajes, entorno) se fija al crear el contenedor.
docker cp: mover ficheros en los dos sentidos
docker cp: mover ficheros en los dos sentidosUno de los dos lados lleva el prefijo contenedor: y el otro es una ruta del host. Funciona en las dos direcciones y también con contenedores parados.
Del host al contenedor: cargar el catálogo
Ha llegado el momento de llenar la base de datos. Tienes ~/aurora-libros/db/init.sql desde la lección 01-07, con la tabla libros y los ocho títulos:
Y ahora ejecútalo dentro del contenedor:
Comprueba el resultado:
docker exec aurora-db psql -U aurora -d aurora_libros \
-c "SELECT id, titulo, autor, precio FROM libros ORDER BY id LIMIT 4;"
docker exec aurora-db psql -U aurora -d aurora_libros -t \
-c "SELECT COUNT(*) FROM libros;" id | titulo | autor | precio
----+---------------------------------------+------------------------+--------
1 | El jardín de senderos que se bifurcan | Jorge Luis Borges | 14.50
2 | Rayuela | Julio Cortázar | 19.90
3 | Cien años de soledad | Gabriel García Márquez | 17.95
4 | La sombra del viento | Carlos Ruiz Zafón | 21.00
(4 rows)
8Los ocho libros de Aurora Libros están en una base de datos real, en un contenedor real. Es la primera vez en todo el curso que existen fuera de un fichero .sql. Todavía nadie los puede leer desde la API —eso llega en la lección 03-05—, pero ya están ahí.
Un apunte de método: cargar el esquema a mano con docker cp funciona pero no es la forma correcta, porque hay que repetirlo cada vez que recrees el contenedor y nadie se acuerda de hacerlo. En la lección 03-06 lo resolverás bien, montando init.sql en /docker-entrypoint-initdb.d/ para que PostgreSQL lo ejecute solo al inicializarse.
Del contenedor al host: extraer un fichero
El sentido inverso es igual de fácil y resulta imprescindible al investigar un incidente:
docker cp aurora-db:/var/lib/postgresql/data/pg_hba.conf ~/aurora-libros/pg_hba.conf.copia
head -3 ~/aurora-libros/pg_hba.conf.copiaSuccessfully copied 5.12kB to /home/junior/aurora-libros/pg_hba.conf.copia
# PostgreSQL Client Authentication Configuration File
# ===================================================Y un caso muy habitual: rescatar el log de una aplicación que —contra toda buena práctica— escribe en un fichero en vez de en la salida estándar.
Detalles del comportamiento de docker cp que evitan sorpresas:
| Situación | Resultado |
|---|---|
docker cp fichero contenedor:/ruta/ (termina en /) |
Copia dentro del directorio |
docker cp fichero contenedor:/ruta (sin /) |
Si /ruta es un directorio, copia dentro; si no, crea o sobrescribe ese fichero |
docker cp carpeta/. contenedor:/destino |
Copia el contenido de la carpeta |
docker cp carpeta contenedor:/destino |
Copia la carpeta entera dentro del destino |
Con -a/--archive |
Conserva propietario y grupo (UID/GID) |
Sin -a |
Los ficheros quedan como root dentro del contenedor |
Ese último punto muerde a menudo: si copias un fichero a un contenedor que corre con USER node, el fichero pertenecerá a root y la aplicación quizá no pueda leerlo. Y una limitación clara: docker cp no puede copiar de un contenedor a otro directamente; hay que pasar por el host.
docker diff: qué ha cambiado respecto a la imagen
docker diff: qué ha cambiado respecto a la imagendocker diff compara el sistema de ficheros actual del contenedor con el de la imagen de la que salió:
C /tmp
A /tmp/init.sql
C /run
A /run/postgresql
A /run/postgresql/.s.PGSQL.5432
C /var/lib/postgresqlTres letras y su significado:
| Letra | Significa | Ejemplo |
|---|---|---|
| A | Added: fichero o directorio nuevo | /tmp/init.sql, el que acabas de copiar |
| C | Changed: modificado (o un directorio cuyo contenido cambió) | /tmp, porque ahora contiene algo nuevo |
| D | Deleted: borrado respecto a la imagen | Un fichero de configuración que se sustituyó |
Es la manifestación visible del copy-on-write de la lección 01-05: lo que ves aquí es exactamente el contenido de la capa de escritura, y por eso docker diff y la columna SIZE de docker ps -s hablan de lo mismo.
Y ahora la observación importante:
Ni una línea. Acabas de crear una tabla y ocho registros, y el directorio de datos de PostgreSQL no aparece en el diff. La razón es que /var/lib/postgresql/data no está en la capa de escritura: la imagen oficial declara ahí un VOLUME, así que Docker creó un volumen anónimo y lo montó en esa ruta. Y docker diff nunca mira dentro de los montajes.
Esto es exactamente lo que anticipaba la lección 02-04 al hablar de por qué VOLUME en un Dockerfile tiene efectos secundarios, y es la puerta de entrada a la lección 03-06: hay datos que no viven ni en la imagen ni en la capa de escritura, sino en un tercer sitio. Un sitio que ahora mismo, para aurora-db, es anónimo y frágil.
Un caso de uso muy práctico de docker diff: descubrir qué escribe una aplicación de la que no te fías.
docker run -d --name diff-demo nginx:alpine
sleep 2 && curl -s localhost > /dev/null 2>&1
docker diff diff-demo
docker rm -f diff-demoC /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /run
A /run/nginx.pid
C /var/cache/nginx
A /var/cache/nginx/client_tempEn diez líneas sabes que Nginx modifica su configuración al arrancar (lo hace su docker-entrypoint.sh), crea un fichero de PID y varios directorios de caché. Es información valiosísima cuando vas a hacer el sistema de ficheros de solo lectura por seguridad, algo que verás en la lección 05-03.
docker commit y por qué no debes usarlo
docker commit y por qué no debes usarlodocker commit convierte el estado actual de un contenedor en una imagen nueva:
docker commit --message "Catálogo cargado a mano" aurora-db aurora-db:con-datos
docker image ls aurora-db --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"sha256:8f2e9c1a7b34d6058e2f9a1c7b3d5e8a0c2f4b6d8e0a2c4f6b8d0e2a4c6f8b0d2
REPOSITORY:TAG SIZE
aurora-db:con-datos 278MBFunciona. Y precisamente por eso es peligroso. Estos son los motivos por los que no se construyen imágenes así, retomando todo el módulo 2:
| Problema | Consecuencia |
|---|---|
| No es reproducible | No existe ningún fichero que describa cómo se llegó a ese estado. Si mañana necesitas la misma imagen con Node 22.14, no hay forma de regenerarla |
| No es auditable | docker image history mostrará una capa enorme con el comentario que escribiste, sin decir qué se instaló, qué se borró ni de dónde vino |
| No se versiona | Un Dockerfile vive en Git, se revisa en una pull request y tiene historial. Un commit vive en la cabeza de quien lo hizo |
| Arrastra basura | Ficheros temporales, historial de shell, logs, credenciales escritas durante la sesión de depuración... todo se hornea en la imagen |
| Pesa de más | Es una capa monolítica que rompe la caché y el compartir capas que tanto cuidaste en la lección 02-02 |
| Rompe la cadena de confianza | Nadie puede responder a "¿qué hay dentro de esto?" sin abrirlo y mirar |
En el vocabulario del oficio, una imagen así se llama imagen artesanal o snowflake, y es el equivalente contenedorizado de aquel servidor que nadie se atrevía a reiniciar. Tiene, eso sí, un uso legítimo:
# Forense: congelar un contenedor roto ANTES de borrarlo, para investigarlo con calma
docker commit aurora-api aurora-api:forense-2026-08-04
docker rm aurora-api
docker run --rm -it --entrypoint sh aurora-api:forense-2026-08-04Guardar la escena del crimen antes de limpiarla. Para eso sí, y para nada más.
- El panel de control de Aurora Libros
Cerramos juntando todo lo de la lección en un comando que usarás a diario:
docker ps -a --filter label=proyecto=aurora-libros \
--format 'table {{.Label "componente"}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'COMPONENTE NAMES STATUS PORTS
cache aurora-cache Up 22 minutes 127.0.0.1:6379->6379/tcp
base-de-datos aurora-db Up 22 minutes 127.0.0.1:5432->5432/tcpConviértelo en un alias permanente en tu ~/.bashrc o ~/.zshrc:
alias aurora='docker ps -a --filter label=proyecto=aurora-libros --format "table {{.Label \"componente\"}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias aurora-parar='docker stop $(docker ps -q --filter label=proyecto=aurora-libros)'
alias aurora-arrancar='docker start $(docker ps -aq --filter label=proyecto=aurora-libros)'Y una versión con salud y tamaño, para cuando algo va mal:
docker ps -as --filter label=proyecto=aurora-libros \
--format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'NAMES STATUS SIZE
aurora-cache Up 23 minutes 0B (virtual 41.2MB)
aurora-db Up 23 minutes 3.14kB (virtual 278MB)Fíjate en que la capa de escritura de aurora-db ha crecido de 127 kB a 3,14 kB… un momento, ha bajado. Lo que ocurre es que el contenedor es nuevo: lo recreaste al añadirle las etiquetas, y esos 3,14 kB son casi exclusivamente el init.sql que copiaste con docker cp. Los datos de los ocho libros no están ahí, sino en el volumen anónimo del que hablábamos. Un recordatorio más de que hay una pieza del rompecabezas que aún no has colocado.
Errores Comunes y Consejos
- Ejecutar
docker rm -f $(docker ps -aq)"para limpiar". Se lleva todo lo de la máquina, incluidos contenedores de otros proyectos y bases de datos de desarrollo con semanas de datos. Filtra siempre porlabelo porname. - Confiar en
--filter name=como si fuera una igualdad. Es una subcadena:name=aurora-dbtambién encuentraaurora-db-backup. Ancla con^...$cuando importe. - Leer la columna
CREATEDcomo "arrancado hace". Es la fecha de creación. Para el tiempo en marcha, miraSTATUSo{{.RunningFor}}. - Interpretar el tamaño
virtualcomo espacio ocupado. Diez contenedores de la misma imagen comparten sus capas: el disco real es la suma de las capas de escritura más una copia de la imagen. - Usar
docker rm -fsobre una base de datos. Es un SIGKILL sin gracia. Primerodocker stop --time 30, despuésdocker rm. - Olvidar
-val borrar y acumular volúmenes anónimos huérfanos. Cada contenedor de PostgreSQL borrado sin-vdeja atrás su volumen. Se localizan condocker volume ls -f dangling=true(lección 03-06). - Copiar ficheros con
docker cpa un contenedor sin privilegios y no entender elpermission denied. El fichero llega como root. Usa-a, o ajusta después condocker exec -u root ... chown. - Construir imágenes con
docker commit. Funciona hoy y te deja sin explicación mañana. El Dockerfile es la fuente de la verdad. - Consejo: guarda tus formatos favoritos en
~/.docker/config.jsonconpsFormat. Undocker pslegible por defecto es un regalo diario. - Consejo: antes de cualquier operación masiva, ejecuta la versión que solo lista. Mira la lista, y solo entonces cambia
psporrm.
Ejercicios
Ejercicio 1: monta tu propio panel de control
Crea cinco contenedores de prueba con alpine:3.20 y sleep 300, etiquetados así: dos con proyecto=aurora-libros y entorno=local, dos con proyecto=aurora-libros y entorno=pruebas, y uno con proyecto=otro-cliente. Después escribe un único comando docker ps que muestre solo los de Aurora Libros en entorno de pruebas, en una tabla con las columnas nombre, entorno y tiempo en marcha. Finalmente, para y borra en un solo comando exclusivamente los de otro-cliente, dejando el resto intacto, y demuestra con otro comando que los cuatro de Aurora Libros siguen vivos.
Ejercicio 2: investiga qué escribe un contenedor (y dónde no lo escribe)
Arranca un contenedor de redis:7-alpine llamado diff-redis. Antes de tocar nada, ejecuta docker diff y anota el resultado. Después escribe 100 claves con redis-cli, fuerza un guardado en disco con redis-cli SAVE, y vuelve a ejecutar docker diff. Responde:
- ¿Aparece el fichero de datos de Redis en el
docker diff? ¿Y en la columnaSIZEdedocker ps -s? - Averigua con
docker inspect --format '{{json .Mounts}}'dónde está realmente ese fichero. - Cópialo al host con
docker cpy comprueba su tamaño conls -lh. - Borra el contenedor con
docker rm -f(sin-v) y explica qué ha pasado exactamente con esas 100 claves. ¿Se han perdido? ¿Dónde están?
Ejercicio 3: rescate y limpieza segura
Simula el final de una jornada de trabajo caótica:
- Crea seis contenedores que terminen inmediatamente: tres con código 0 (
alpine:3.20 true) y tres con código 1 (alpine:3.20 sh -c 'exit 1'), todos con la etiquetaproyecto=pruebas-limpieza. - Escribe un comando que liste solo los que fallaron, con su nombre y su estado.
- De uno de los fallidos, extrae con
docker cpel fichero/etc/os-releaseal host antes de borrarlo. - Borra en un solo comando solo los que fallaron, dejando los tres correctos.
- Finalmente, limpia el resto con
docker container pruneacotado a la etiqueta, y verifica que niaurora-dbniaurora-cachese han visto afectados.
Soluciones
Solución al ejercicio 1
for n in 1 2; do
docker run -d --name prueba-local-$n \
--label proyecto=aurora-libros --label entorno=local \
alpine:3.20 sleep 300
done
for n in 1 2; do
docker run -d --name prueba-pruebas-$n \
--label proyecto=aurora-libros --label entorno=pruebas \
alpine:3.20 sleep 300
done
docker run -d --name prueba-otro --label proyecto=otro-cliente alpine:3.20 sleep 300El comando del panel, con dos filtros combinados con Y lógico:
docker ps --filter label=proyecto=aurora-libros --filter label=entorno=pruebas \
--format 'table {{.Names}}\t{{.Label "entorno"}}\t{{.RunningFor}}'NAMES ENTORNO RUNNING FOR
prueba-pruebas-2 pruebas 12 seconds ago
prueba-pruebas-1 pruebas 14 seconds agoLos de entorno=local no aparecen: cumplen el primer filtro pero no el segundo, y ambos deben cumplirse.
Borrado quirúrgico de otro-cliente:
# Primero MIRAR
docker ps -a --filter label=proyecto=otro-cliente --format "{{.Names}}"
# Después ACTUAR
docker rm -f $(docker ps -aq --filter label=proyecto=otro-cliente)
# Y COMPROBAR
docker ps --filter label=proyecto=aurora-libros --format "{{.Names}}" | wc -lLos cuatro de Aurora Libros siguen en pie. La secuencia mirar → actuar → comprobar es la que convierte un comando peligroso en una operación rutinaria.
Solución al ejercicio 2
Prácticamente nada: Redis recién arrancado solo escribe su fichero de PID. Ahora, carga y guardado:
docker exec diff-redis sh -c 'for i in $(seq 1 100); do redis-cli SET libro:$i "titulo-$i" > /dev/null; done'
docker exec diff-redis redis-cli DBSIZE
docker exec diff-redis redis-cli SAVE
docker exec diff-redis ls -lh /data
docker diff diff-redis
docker ps -s --filter name=diff-redis --format "{{.Names}}: {{.Size}}"(integer) 100
OK
-rw-r--r-- 1 redis redis 3.4K Aug 4 20:41 dump.rdb
C /run
A /run/redis_6379.pid
diff-redis: 0B (virtual 41.2MB)1. Aquí está la sorpresa del ejercicio: dump.rdb existe (lo acabas de listar con ls -lh, 3,4 kB) pero no aparece en el docker diff, y la capa de escritura sigue midiendo 0 B. Las dos herramientas están de acuerdo entre sí y las dos dicen la verdad: ese fichero no está en la capa de escritura del contenedor.
2. La explicación está en los montajes:
[
{
"Type": "volume",
"Name": "b41f7c9e2a8d05f31c6e8b4a2d97f0c5e3a1b8d6f4c2e0a9b7d5f3c1e8a6b4d2",
"Source": "/var/lib/docker/volumes/b41f7c9e2a8d.../_data",
"Destination": "/data",
"Driver": "local",
"RW": true
}
]La imagen oficial de Redis declara VOLUME /data, así que Docker creó un volumen anónimo —ese nombre que parece un hash es exactamente eso: un volumen sin nombre— y lo montó en /data. Todo lo que Redis escribe ahí va al volumen, no a la capa de escritura, y por eso docker diff no lo ve.
3.
Successfully copied 5.63kB to /tmp/dump-aurora.rdb
-rw-r--r-- 1 junior junior 3.4K Aug 4 20:42 /tmp/dump-aurora.rdbdocker cp sí lee a través de los montajes: para él, /data/dump.rdb es simplemente una ruta del sistema de ficheros del contenedor. Los 3,4 kB coinciden con el ls de dentro; los 5,63 kB que informa Docker son el tamaño del flujo tar con el que se hace la copia, con sus cabeceras y su relleno a bloques.
4.
La respuesta correcta es más interesante que un simple "se han perdido":
- Las claves que estaban en memoria murieron con el proceso, como en el ejercicio de la lección anterior.
- Pero el
dump.rdbcon las 100 claves sigue existiendo, en un volumen que ahora está huérfano: sin contenedor que lo use, con un nombre que nadie recuerda y ocupando disco indefinidamente. Como usastedocker rm -fsin-v, el volumen no se borró. - Si mañana arrancas otro
redis:7-alpine, Docker creará un volumen anónimo nuevo y vacío: no reutilizará el anterior. Desde el punto de vista práctico, los datos están perdidos aunque los bytes sigan en tu disco.
Eso es exactamente lo que le pasa hoy a aurora-db con los ocho libros que acabas de cargar, y es el problema que resuelve la lección 03-06 poniéndole al volumen un nombre.
Solución al ejercicio 3
for n in 1 2 3; do
docker run --name ok-$n --label proyecto=pruebas-limpieza alpine:3.20 true
docker run --name fallo-$n --label proyecto=pruebas-limpieza alpine:3.20 sh -c 'exit 1'
done2. Listar solo los que fallaron, combinando status y exited:
docker ps -a --filter label=proyecto=pruebas-limpieza --filter exited=1 \
--format "table {{.Names}}\t{{.Status}}"NAMES STATUS
fallo-3 Exited (1) 6 seconds ago
fallo-2 Exited (1) 7 seconds ago
fallo-1 Exited (1) 8 seconds agoEl filtro exited=1 es el que hace el trabajo fino: distingue "terminó" de "terminó mal", algo que status=exited por sí solo no puede.
3. Rescatar un fichero de un contenedor parado:
Detalle importante: docker cp funciona con el contenedor parado. No hace falta arrancarlo, y eso lo convierte en la herramienta ideal para hacer autopsias de contenedores que ya no levantan.
4. Borrado selectivo de los fallidos:
docker rm $(docker ps -aq --filter label=proyecto=pruebas-limpieza --filter exited=1)
docker ps -a --filter label=proyecto=pruebas-limpieza --format "{{.Names}} {{.Status}}"2f8c1a9e4b73
7d1e5b2c8f04
9a3f7c1e5d28
ok-3 Exited (0) 1 minute ago
ok-2 Exited (0) 1 minute ago
ok-1 Exited (0) 1 minute agoLos tres correctos siguen ahí. No hizo falta -f porque ya estaban parados.
5. Limpieza final acotada y verificación:
docker container prune -f --filter "label=proyecto=pruebas-limpieza"
docker ps -a --filter label=proyecto=aurora-libros \
--format 'table {{.Names}}\t{{.Status}}'Deleted Containers:
1c7e9a3f5b28...
4b8d2f6a1c93...
6e0a4c8f2b17...
Total reclaimed space: 0B
NAMES STATUS
aurora-cache Up 41 minutes
aurora-db Up 41 minutesaurora-db y aurora-cache intactos, porque el --filter label= acotó el prune a la etiqueta correcta. Compara mentalmente con lo que habría pasado con un docker container prune -f a secas: los tres ok-* habrían caído igual, pero también cualquier contenedor parado de cualquier otro proyecto de tu máquina. Las etiquetas no son burocracia: son el mecanismo que hace que la limpieza sea selectiva en vez de indiscriminada.
Conclusión
Ya no miras docker ps: lo lees. Sabes qué dice cada una de sus siete columnas y, sobre todo, qué no dice —que CREATED no es la hora de arranque, que IMAGE puede haberse quedado obsoleta si la etiqueta se movió, y que el número virtual de -s no es espacio ocupado sino espacio visto—. Sabes filtrar por estado, nombre, imagen, etiqueta, código de salida y salud, combinando filtros con Y y con O, y sabes dar forma a la salida con plantillas Go hasta convertirla en un panel de control de Aurora Libros que cabe en un alias.
Has aprendido a componer comandos con -q y, más importante, a hacerlo con red: mirar → actuar → comprobar, filtrando siempre por etiqueta para que un borrado masivo no se lleve nunca lo que no era suyo. Conoces la diferencia entre docker rm y docker rm -f —que no es stop + rm, sino un SIGKILL sin gracia—, sabes qué se destruye y qué sobrevive al borrar un contenedor, y usas docker container prune acotado con until y label en vez de a pelo. Has etiquetado la flota con proyecto, componente y entorno, que es exactamente el mecanismo que Docker Compose automatizará por ti en el módulo 4.
Con docker cp mueves ficheros en los dos sentidos, incluso con el contenedor parado, y eso te ha servido para algo memorable: los ocho libros de Aurora Libros están cargados en PostgreSQL, con su tabla, su índice y su INSERT 0 8. Con docker diff sabes ver la capa de escritura convertida en lista de ficheros —A, C y D— y has descubierto la pieza que falta: el directorio de datos de PostgreSQL no aparece en el diff, porque no está en la capa de escritura, sino en un volumen anónimo que la imagen declara y que nadie controla. Y sabes por qué docker commit no sirve para construir imágenes, aunque sea perfecto para congelar la escena de un crimen antes de limpiarla.
Sabes mirar la flota cuando todo va bien. Falta lo otro. En la siguiente lección, Inspección y Depuración de Contenedores, te enfrentarás a los contenedores que no funcionan y no dicen por qué: leerás docker logs a fondo con sus filtros de tiempo y su regla de oro sobre stdout, entrarás con docker exec incluso en imágenes mínimas que no tienen ni ping —con contenedores efímeros que comparten sus namespaces—, exprimirás el JSON de docker inspect con plantillas y jq, y vigilarás en directo con docker top, docker stats y docker events. Y aplicarás todo eso a tres averías reales de Aurora Libros, entre ellas la que lleva dos lecciones esperando: por qué aurora-api no encuentra a aurora-db aunque los dos estén corriendo en la misma máquina.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
