🎯 Meta del capítulo: dominar las diez herramientas que aparecen en el 95% de los scripts, pipelines de CI y sesiones de depuración reales. No para memorizarlas: para saber qué existe y reconocer cuándo el problema que tienes delante ya está resuelto por una de ellas.
🧠 La filosofía Unix, en una frase: programas pequeños que hacen una cosa bien y se comunican por texto. Ninguna de estas herramientas es impresionante por sí sola. Encadenadas con
|resuelven en una línea lo que en un lenguaje de programación serían 40.
T3.1 · grep — buscar texto
grep "ERROR" app.log # líneas que contienen ERROR
grep -i "error" app.log # sin distinguir mayúsculas
grep -n "ERROR" app.log # con número de línea
grep -c "ERROR" app.log # solo CONTAR cuántas
grep -v "DEBUG" app.log # INVERTIR: las que NO contienen
grep -r "TODO" ./src # recursivo por carpetas
grep -l "apiKey" -r ./src # solo los NOMBRES de archivo con coincidencia
grep -w "id" archivo # palabra completa (no "uuid" ni "idioma")
grep -q "ERROR" app.log # silencioso: solo devuelve 0/1 → para ifsContexto alrededor de la coincidencia (imprescindible al leer una traza de error):
grep -A 5 "Exception" app.log # 5 líneas DESPUÉS (After)
grep -B 3 "Exception" app.log # 3 líneas ANTES (Before)
grep -C 3 "Exception" app.log # 📌 3 líneas alrededor (Context)Expresiones regulares:
grep -E "ERROR|FATAL" app.log # -E = regex extendida: alternancia
grep -E "^2026-07-28" app.log # ^ = inicio de línea
grep -E "[0-9]{3}\.[0-9]{1,3}" archivo # dígitos
grep -oE "[a-z]+@[a-z]+\.[a-z]{2,}" archivo # -o = imprimir SOLO lo que coincide💡
grep -oes el que la gente no conoce y más se echa de menos. Sin él,grepte da la línea entera; con él, te da únicamente el trozo que coincide. Perfecto para extraer todas las IPs, emails o IDs de un log:bashgrep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" acceso.log | sort | uniq -c | sort -rn | headEsa línea te da las IPs que más peticiones hicieron, ordenadas. Es literalmente el primer comando que se ejecuta cuando sospechas de un ataque.
💡 Alternativa moderna:
ripgrep(rg). Es mucho más rápido, respeta.gitignorey busca recursivamente por defecto.rg "TODO"equivale agrep -rn "TODO" .ignorandonode_modules. Instálalo (apt install ripgrep) y úsalo a diario — pero aprendegrep, porque es el que siempre estará en el servidor.
T3.2 · find — buscar archivos (y actuar sobre ellos)
find . -name "*.log" # por nombre (sensible a mayúsculas)
find . -iname "*.LOG" # sin distinguir mayúsculas
find . -type f # solo archivos (-type d = solo directorios)
find . -maxdepth 2 -name "*.json" # limitar la profundidad
find . -size +100M # de más de 100 MB
find /var/log -mtime +30 # modificados hace MÁS de 30 días
find . -mmin -10 # modificados en los últimos 10 minutos
find . -empty # vacíos
find . -name "node_modules" -prune -o -name "*.js" -print # excluir carpetasLo que hace a find verdaderamente potente: -exec y -delete
find . -name "*.tmp" -delete # borrar directamente
find /var/log -name "*.log" -mtime +30 -delete # rotación manual de logs
find . -name "*.sh" -exec chmod +x {} \; # {} = cada archivo encontrado
find . -name "*.js" -exec grep -l "TODO" {} + # + agrupa: MUCHO más rápido que \;⚠️
\;ejecuta el comando una vez por archivo;+lo ejecuta una vez con todos. Con 10.000 archivos,\;lanza 10.000 procesos (lento) y+lanza uno o dos. Usa+siempre que el comando acepte varios argumentos.
⚠️ Antes de un
-delete, ejecuta el mismofindsin él. Mira la lista.findcon un patrón mal escrito y-deletees una de las formas más rápidas de arruinarte el día. Este hábito — mirar antes de destruir — vale pararm,DELETEen SQL yterraform apply.
T3.3 · sed — editar texto en un flujo
sed 's/viejo/nuevo/' archivo # sustituir la PRIMERA vez de cada línea
sed 's/viejo/nuevo/g' archivo # 📌 g = TODAS las veces
sed 's/viejo/nuevo/gi' archivo # además, sin distinguir mayúsculas
sed -i 's/localhost/produccion.com/g' config.yml # -i = modificar EL ARCHIVO
sed -i.bak 's/viejo/nuevo/g' config.yml # y guardar copia config.yml.bak
sed -n '10,20p' archivo.log # imprimir solo las líneas 10 a 20
sed '/^#/d' config.conf # BORRAR las líneas que empiezan por #
sed '/^$/d' archivo # borrar líneas vacías
sed '1d' datos.csv # quitar la cabecera⚠️
sed -ise comporta distinto en macOS (BSD) y en Linux (GNU). En macOS-iexige un sufijo:sed -i '' 's/a/b/' f. En Linux,sed -i '' …da error. Si tu script debe funcionar en ambos, usased -i.bak(válido en los dos) y borra el.bakdespués — o usa Perl:perl -pi -e 's/a/b/g' archivo, que es idéntico en todas partes.
💡 El separador de
sedno tiene que ser/. Cuando sustituyes rutas, usar/obliga a escapar cada barra. Cambia el delimitador y se lee solo:bashsed 's|/var/www/viejo|/var/www/nuevo|g' nginx.conf # mucho más legible
T3.4 · awk — procesar datos por columnas
awk trata cada línea como una fila con columnas separadas por espacios. $1 es la primera columna, $2 la segunda, $0 la línea entera y NF el número de campos.
awk '{print $1}' acceso.log # primera columna
awk '{print $1, $7}' acceso.log # columnas 1 y 7
awk -F',' '{print $2}' datos.csv # -F = separador (CSV)
awk '{print NF}' archivo # cuántas columnas tiene cada línea
awk 'NR==5' archivo # la línea 5 (NR = número de registro)
awk 'END {print NR}' archivo # contar líneas (como wc -l)
awk '$3 > 100 {print $1}' datos.txt # filtrar por valor de columna
awk '/ERROR/ {print $0}' app.log # filtrar por patrón
awk '{suma += $3} END {print suma}' ventas.txt # SUMAR una columna
awk '{suma += $3} END {print suma/NR}' v.txt # mediaRecetas reales que usarás:
# 1. Porcentaje de disco como número limpio (para comparar en un if)
df -h / | awk 'NR==2 {print $5}' | tr -d '%'
# 2. Contar códigos de estado HTTP en un log de Nginx
awk '{print $9}' acceso.log | sort | uniq -c | sort -rn
# 3. Las 10 URLs más pedidas
awk '{print $7}' acceso.log | sort | uniq -c | sort -rn | head -10
# 4. Total de bytes servidos, en megas
awk '{suma += $10} END {printf "%.2f MB\n", suma/1024/1024}' acceso.log
# 5. Ver PID y comando de los procesos que más RAM consumen
ps aux | awk 'NR>1 {print $4, $2, $11}' | sort -rn | head -5🧠
sort | uniq -c | sort -rnes EL idiom de la línea de comandos. Ordenar → contar repetidos → ordenar por cantidad descendente. Con eso respondes "¿qué es lo que más se repite?" sobre cualquier cosa: IPs, errores, URLs, usuarios.uniq -cexige que la entrada venga ordenada, de ahí el primersort. Memoriza el patrón entero.
💡 ¿grep, sed o awk? Regla práctica:
greppara filtrar líneas,sedpara sustituir texto,awkcuando hay columnas o hace falta calcular. Si te ves haciendo aritmética ensed, queríasawk. Siawkse te va a más de 10 líneas, querías Python.
T3.5 · jq — el awk del JSON
Toda API moderna devuelve JSON. jq lo filtra, transforma y consulta.
curl -s https://api.ejemplo.com/usuarios | jq . # formatear (pretty print)
jq '.nombre' datos.json # un campo
jq -r '.nombre' datos.json # 📌 -r = sin comillas (raw)
jq '.usuarios[0]' datos.json # primer elemento
jq '.usuarios[].email' datos.json # el email de TODOS
jq '.usuarios | length' datos.json # cuántos hay
jq '.usuarios[] | select(.activo == true)' datos.json # filtrar
jq '.usuarios[] | select(.edad > 30) | .nombre' datos.json # filtrar y proyectar
jq '[.usuarios[].edad] | add / length' datos.json # media de edad
jq '{nombre: .nombre, correo: .email}' datos.json # REMODELAR la salida
jq -r '.usuarios[] | [.id, .nombre] | @csv' datos.json # exportar a CSVCasos reales:
# Sacar un token de la respuesta de login y usarlo en la siguiente petición
TOKEN=$(curl -s -X POST https://api.com/login \
-d '{"email":"a@b.com","pass":"x"}' \
-H 'Content-Type: application/json' | jq -r '.token')
curl -s https://api.com/perfil -H "Authorization: Bearer $TOKEN" | jq .
# Leer un valor de package.json en un script de CI
VERSION=$(jq -r '.version' package.json)
# Construir JSON válido desde variables (escapado correcto garantizado)
jq -n --arg nombre "$NOMBRE" --argjson edad "$EDAD" '{nombre: $nombre, edad: $edad}'⚠️ Nunca construyas JSON concatenando cadenas en Bash. Si un valor lleva una comilla, un salto de línea o una barra invertida, generas JSON inválido — o peor, inyectas datos.
jq -n --argescapa correctamente siempre. Es el mismo principio que las consultas preparadas en SQL (capítulo 1) y por la misma razón.
T3.6 · curl — hablar con APIs desde la terminal
curl https://api.com/usuarios # GET simple
curl -s https://api.com/usuarios # silencioso (sin barra de progreso)
curl -I https://miapp.com # 📌 solo cabeceras
curl -v https://miapp.com # verboso: TLS, cabeceras, todo
curl -L https://acortado.com/x # seguir redirecciones
curl -o salida.json https://api.com/datos # guardar a archivo
curl -w "\n%{http_code} en %{time_total}s\n" -o /dev/null -s https://miapp.com# POST con JSON
curl -X POST https://api.com/usuarios \
-H "Content-Type: application/json" \
-d '{"nombre":"Ana","email":"ana@x.com"}'
# Con token
curl https://api.com/perfil -H "Authorization: Bearer $TOKEN"
# Subir un archivo (multipart)
curl -X POST https://api.com/subir -F "archivo=@foto.jpg" -F "titulo=Mi foto"
# En scripts: --fail hace que curl devuelva error si el HTTP es 4xx/5xx
curl --fail --silent --show-error --max-time 10 https://api.com/salud⚠️
curlsin--faildevuelve código de salida 0 aunque el servidor responda 500. Paracurl, "he recibido una respuesta" ya es éxito. En un script conset -eeso significa que un health check contra un servidor caído pasa. La combinación correcta en scripts es siempre:curl --fail --silent --show-error(o-fsS), y--max-timepara que no se cuelgue eterno.
💡 Truco de oro para depurar: en las DevTools del navegador, clic derecho sobre cualquier petición → Copy as cURL. Te pega el comando exacto, con todas las cabeceras y cookies. A partir de ahí puedes reproducir el problema fuera del navegador y modificarlo a voluntad.
T3.7 · xargs — convertir una lista en argumentos
Algunos comandos leen de una tubería y otros solo aceptan argumentos. xargs traduce entre ambos.
find . -name "*.log" | xargs rm # borrar todo lo encontrado
cat urls.txt | xargs -n1 curl -s -o /dev/null -w "%{http_code} %{url_effective}\n"
# 📌 Con nombres que llevan espacios: -print0 + -0 (el separador es un byte nulo)
find . -name "*.log" -print0 | xargs -0 rm
# -I{} para colocar el argumento donde quieras
ls *.txt | xargs -I{} mv {} {}.bak
# -P: EN PARALELO (aquí, 4 a la vez)
cat urls.txt | xargs -P 4 -I{} curl -s -o /dev/null {}💡
xargs -Pes paralelismo gratis. Descargar 100 URLs de una en una tarda minutos; con-P 8, segundos. Es la forma más simple de acelerar un script que hace muchas operaciones independientes de red o de disco.
T3.8 · Comprimir y empaquetar
tar -czf copia.tar.gz carpeta/ # 📌 Crear (c) Zip gzip (z) Fichero (f)
tar -xzf copia.tar.gz # eXtraer
tar -xzf copia.tar.gz -C /destino # extraer en otro sitio
tar -tzf copia.tar.gz # LISTAR el contenido sin extraer
tar -czf copia.tar.gz --exclude='node_modules' --exclude='.git' proyecto/
gzip archivo.log # comprime → archivo.log.gz (borra el original)
gunzip archivo.log.gz # descomprime
zcat archivo.log.gz | grep ERROR # 📌 leer un .gz SIN descomprimirlo
zgrep ERROR archivo.log.gz # lo mismo, más corto💡
zcat/zgrepsobre logs rotados. Los logs viejos se guardan comprimidos (app.log.1.gz,app.log.2.gz…). No los descomprimas para buscar:zgrep ERROR /var/log/app.log.*.gzbusca en todos a la vez sin tocar el disco.
T3.9 · make — el lanzador de tareas universal
Un Makefile documenta y estandariza los comandos del proyecto. Funciona en cualquier lenguaje.
.PHONY: ayuda instalar dev test lint build desplegar limpiar
ayuda: ## Muestra esta ayuda
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
instalar: ## Instala dependencias
pnpm install
dev: ## Arranca en desarrollo
docker compose up -d
pnpm dev
test: ## Ejecuta los tests
pnpm test
lint: ## Comprueba estilo y scripts
pnpm lint
shellcheck scripts/*.sh
build: test lint ## Compila (solo si test y lint pasan)
pnpm build
desplegar: build ## Despliega a producción
./scripts/deploy.sh $(VERSION)
limpiar: ## Borra artefactos
rm -rf dist node_modulesmake # ejecuta la primera tarea → la ayuda
make dev
make desplegar VERSION=v1.4.2⚠️ En un Makefile, la indentación DEBE ser un TABULADOR, no espacios. Es el error nº1 y el mensaje (
missing separator) no lo explica. Configura tu editor para que enMakefileno convierta tabs a espacios.
💡 Por qué merece la pena aunque uses
npm scripts:makeno depende del lenguaje. Cuando tu proyecto tiene un backend en Go, un frontend en Node y scripts en Python,make testes un único punto de entrada para los tres. Y.PHONYdeclara las tareas que no producen un archivo con ese nombre — sin eso, si existe una carpeta llamadatest,make testno hará nada.
T3.10 · tmux — sesiones que sobreviven a la desconexión
Problema real: lanzas una migración de 40 minutos por SSH, se cae la conexión y el proceso muere a medias. tmux lo evita: el proceso vive en el servidor, tú te enganchas y desenganchas.
tmux new -s despliegue # crear sesión con nombre
# … lanza lo que quieras …
Ctrl+b d # DESENGANCHARSE (detach) — el proceso sigue vivo
tmux ls # listar sesiones
tmux attach -t despliegue # volver a engancharse
tmux kill-session -t despliegue # cerrarlaAtajos básicos (todos empiezan con el prefijo Ctrl+b):
| Atajo | Acción |
|---|---|
Ctrl+b d | Desengancharse |
Ctrl+b c | Nueva ventana |
Ctrl+b n / p | Ventana siguiente / anterior |
Ctrl+b % | Dividir en vertical |
Ctrl+b " | Dividir en horizontal |
Ctrl+b flechas | Moverse entre paneles |
Ctrl+b [ | Modo scroll (salir con q) |
💡 Regla profesional: cualquier comando que dure más de un minuto en un servidor remoto, va dentro de
tmux. Migraciones,docker build, importaciones de base de datos, backups. El día que se te caiga el WiFi a mitad de una migración de producción entenderás por qué esto no es opcional.
T3.11 · Encadenarlo todo: casos reales resueltos
1. Las 10 IPs con más peticiones en el log de Nginx
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -102. Cuántos errores 500 hubo hoy
grep "$(date +%d/%b/%Y)" /var/log/nginx/access.log | awk '$9 == 500' | wc -l3. Buscar credenciales filtradas en el código antes de un commit
grep -rEn "(api[_-]?key|password|secret|token)\s*[=:]\s*['\"][^'\"]{8,}" \
--include="*.{js,ts,py,go,php}" --exclude-dir=node_modules ./src4. Cambiar una URL en todos los archivos de configuración, con copia de seguridad
find ./config -name "*.yml" -exec sed -i.bak 's|http://viejo|https://nuevo|g' {} +5. Comprobar que 50 URLs de tu web responden 200
cat urls.txt | xargs -P 8 -I{} \
curl -s -o /dev/null -w "%{http_code} {}\n" {} | grep -v "^200"(imprime solo las que NO devolvieron 200 — silencio significa que todo va bien)
6. Los 10 archivos más grandes del servidor
find / -type f -size +50M -exec du -h {} + 2>/dev/null | sort -rh | head -107. Extraer los tiempos de respuesta de un log y calcular la media y el máximo
grep -oE 'tiempo=[0-9.]+' app.log | cut -d= -f2 |
awk '{s+=$1; if($1>max) max=$1} END {printf "media=%.3fs max=%.3fs n=%d\n", s/NR, max, NR}'8. Monitorizar en vivo solo los errores, con contexto
tail -f app.log | grep --line-buffered -C 2 -E "ERROR|FATAL"⚠️
--line-bufferedes imprescindible contail -f | grep. Sin él,grepacumula la salida en un búfer de 4 KB y no ves nada hasta que se llena — parece que no funciona cuando en realidad solo está esperando. Es una de esas trampas que cuestan media hora la primera vez.
T3.12 · Herramientas modernas que merecen un hueco
Ninguna sustituye a las clásicas (que siempre estarán en el servidor), pero en tu máquina te hacen más rápido:
| Clásica | Moderna | Por qué |
|---|---|---|
grep | rg (ripgrep) | 5-10× más rápido, respeta .gitignore |
find | fd | Sintaxis mucho más simple: fd "\.log$" |
cat | bat | Coloreado y con números de línea |
ls | eza | Colores, iconos, integración con Git |
du | dust / ncdu | Visual, navegable |
top | htop / btop | Interactivo y legible |
cd | zoxide | Aprende tus rutas: z proyecto te lleva |
| — | fzf | 📌 Buscador difuso: se integra con Ctrl+R |
💡 Si solo instalas dos, que sean
fzfyripgrep.fzfsustituye elCtrl+Rde Bash por un buscador difuso interactivo sobre todo tu historial, y se integra confind,git checkouty prácticamente cualquier lista. Es la mejora de productividad más grande por menos esfuerzo de toda esta tabla.
✅ Ejercicio del capítulo
Vas a analizar un log de servidor web con las herramientas del capítulo. Primero genera datos realistas:
mkdir -p ~/practica-t3 && cd ~/practica-t3
for i in $(seq 1 2000); do
ip="192.168.1.$((RANDOM % 20))"
rutas=(/ /api/usuarios /api/pedidos /login /static/app.js)
ruta="${rutas[$((RANDOM % 5))]}"
codigos=(200 200 200 200 301 404 500)
cod="${codigos[$((RANDOM % 7))]}"
bytes=$((RANDOM % 50000))
ms=$((RANDOM % 2000))
echo "$ip - - [28/Jul/2026:10:$((RANDOM % 60)):00 +0000] \"GET $ruta HTTP/1.1\" $cod $bytes ${ms}ms"
done > acceso.logResponde a cada pregunta con una sola línea de comandos:
- ¿Cuántas peticiones hay en total?
- ¿Cuántas devolvieron un error 500?
- ¿Cuáles son las 5 IPs con más peticiones, con su recuento?
- ¿Qué ruta se pidió más veces?
- ¿Cuántos bytes se sirvieron en total, expresado en MB con dos decimales?
- ¿Qué IPs provocaron algún error 500? (sin repetidos)
- ¿Cuál es el tiempo de respuesta medio en milisegundos?
- Muestra un recuento por código de estado, ordenado de más a menos frecuente.
- Guarda en
sospechosas.txtlas IPs con más de 5 errores 4xx o 5xx. - Genera un
informe.jsonválido (compruébalo conjq .) con: total de peticiones, número de errores, número de IPs únicas y bytes totales.
Extra
- Escribe
analiza-log.shque reciba la ruta de un log como argumento y produzca todo el informe. Conset -euo pipefail, validación del argumento, y que paseshellcheck. - Añade una opción
--top Nque cambie cuántas filas se muestran en los rankings.
💡 Pistas de la solución (abre solo si te atascas)
- Primero identifica las columnas:
awk '{print NF}' acceso.log | head -1te dice cuántas hay, yhead -1 acceso.logte deja contarlas a ojo. En este formato:$1=IP,$7=ruta,$9=código,$10=bytes,$11=tiempo. - Punto 2:
awk '$9 == 500' acceso.log | wc -l— o directamenteawk '$9 == 500 {n++} END {print n+0}'en un solo proceso. El+0fuerza a imprimir0en vez de vacío si no hubo ninguno. - Punto 3: el idiom
sort | uniq -c | sort -rn | head -5. - Punto 5:
awk '{s += $10} END {printf "%.2f MB\n", s/1024/1024}'. - Punto 6: filtra primero y proyecta después:
awk '$9 == 500 {print $1}' | sort -u. - Punto 7: el tiempo lleva el sufijo
mspegado; quítalo consub(/ms/, "", $11)dentro de awk, o contr -d 'ms'antes. - Punto 9: cuenta por IP solo las líneas de error (
$9 >= 400) y filtra el recuento conawk '$1 > 5 {print $2}'al final — recuerda que trasuniq -cla cuenta pasa a ser$1. - Punto 10: usa
jq -n --argjsonpara los números, nunca concatenación de cadenas:bashjq -n --argjson total "$TOTAL" --argjson errores "$ERR" '{total: $total, errores: $errores}' - Punto 11: calcula cada métrica en una función y guarda el log en una variable validada (
LOG="${1:?Uso: $0 <archivo.log>}", más[[ -r "$LOG" ]] || fatal "no legible").
🧠 Autoevaluación
Porque uniq solo compara líneas adyacentes: colapsa repeticiones consecutivas, no busca por todo el archivo. Si las líneas iguales están dispersas, uniq no las agrupa y el recuento sale mal. sort las junta primero. (sort -u hace ambas cosas, pero pierde el recuento — por eso sigue haciendo falta uniq -c cuando quieres números.)
grep: filtrar líneas que coinciden con un patrón. Entra un archivo, salen algunas líneas.sed: transformar texto — sustituir, borrar o extraer rangos de líneas. Entra un archivo, sale el mismo archivo modificado.awk: cuando los datos tienen columnas o hay que calcular (sumar, promediar, contar, comparar campos).
Si estás peleándote con aritmética en sed, querías awk. Si tu programa de awk pasa de unas 10 líneas, querías Python.
\; ejecuta el comando una vez por cada archivo encontrado: con 10.000 archivos, 10.000 procesos nuevos. + agrupa todos los archivos en el menor número posible de invocaciones, igual que haría xargs.
La diferencia de rendimiento es enorme (segundos frente a minutos). Solo necesitas \; cuando el comando acepta un único argumento o cuando {} no va al final de la línea.
Porque curl considera "éxito" el hecho de haber recibido una respuesta, aunque esa respuesta sea un 500 Internal Server Error: devuelve código de salida 0. En un script con set -e, un health check contra un servidor completamente roto pasaría como correcto y el despliegue continuaría.
--fail (-f) hace que devuelva un código de error ante respuestas 4xx y 5xx. La combinación recomendada en scripts es curl -fsS --max-time N: falla ante errores HTTP, sin barra de progreso, pero mostrando el mensaje de error, y sin colgarse indefinidamente.
Porque cualquier valor que contenga una comilla doble, una barra invertida, un salto de línea o un carácter de control romperá el JSON — y si esos datos vienen de una fuente externa, estás ante una inyección: el atacante controla la estructura del documento, no solo su contenido.
jq -n --arg clave "$valor" escapa correctamente cualquier entrada. Es exactamente el mismo principio que las consultas preparadas frente a la concatenación de SQL: nunca mezcles datos con sintaxis.
nohup … & evita que el proceso muera al cerrar la sesión, pero pierdes la interacción: no puedes ver su salida en vivo, ni responder a un prompt, ni parar y retomar el trabajo. Solo te queda leer el archivo de log a posteriori.
tmux mantiene viva una sesión de terminal completa en el servidor: te desenganchas, se cae el WiFi, te reconectas desde otro ordenador y sigues viendo exactamente la misma pantalla, con el proceso interactivo intacto. Para una migración larga o una depuración en producción, es la diferencia entre trabajar y volver a empezar.
df -h # 1. ¿qué partición y cuánto?
du -sh /* 2>/dev/null | sort -rh | head -10 # 2. ¿qué directorio de primer nivel manda?
du -sh /var/* | sort -rh | head -10 # 3. bajar por la rama culpable
find / -type f -size +500M -exec du -h {} + 2>/dev/null | sort -rh | head
# 4. archivos individuales enormes
journalctl --disk-usage && docker system df # 5. los dos sospechosos habitualesLa estrategia es descender por el árbol: nivel superior primero, luego la rama más grande, y así hasta el archivo concreto. Los culpables casi siempre son logs sin rotar (/var/log), el journal de systemd, o imágenes y volúmenes de Docker sin limpiar (docker system prune -a).
Siguiente: 00-fundamentos-backend.md — ya tienes las herramientas; ahora entendamos qué es realmente el backend.