Skip to content

🎯 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

bash
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 ifs

Contexto alrededor de la coincidencia (imprescindible al leer una traza de error):

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

bash
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 -o es el que la gente no conoce y más se echa de menos. Sin él, grep te 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:

bash
grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" acceso.log | sort | uniq -c | sort -rn | head

Esa 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 .gitignore y busca recursivamente por defecto. rg "TODO" equivale a grep -rn "TODO" . ignorando node_modules. Instálalo (apt install ripgrep) y úsalo a diario — pero aprende grep, porque es el que siempre estará en el servidor.


T3.2 · find — buscar archivos (y actuar sobre ellos)

bash
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 carpetas

Lo que hace a find verdaderamente potente: -exec y -delete

bash
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 mismo find sin él. Mira la lista. find con un patrón mal escrito y -delete es una de las formas más rápidas de arruinarte el día. Este hábito — mirar antes de destruir — vale para rm, DELETE en SQL y terraform apply.


T3.3 · sed — editar texto en un flujo

bash
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 -i se comporta distinto en macOS (BSD) y en Linux (GNU). En macOS -i exige un sufijo: sed -i '' 's/a/b/' f. En Linux, sed -i '' … da error. Si tu script debe funcionar en ambos, usa sed -i.bak (válido en los dos) y borra el .bak después — o usa Perl: perl -pi -e 's/a/b/g' archivo, que es idéntico en todas partes.

💡 El separador de sed no tiene que ser /. Cuando sustituyes rutas, usar / obliga a escapar cada barra. Cambia el delimitador y se lee solo:

bash
sed '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.

bash
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    # media

Recetas reales que usarás:

bash
# 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 -rn es 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 -c exige que la entrada venga ordenada, de ahí el primer sort. Memoriza el patrón entero.

💡 ¿grep, sed o awk? Regla práctica: grep para filtrar líneas, sed para sustituir texto, awk cuando hay columnas o hace falta calcular. Si te ves haciendo aritmética en sed, querías awk. Si awk se 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.

bash
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 CSV

Casos reales:

bash
# 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 --arg escapa 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

bash
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
bash
# 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

⚠️ curl sin --fail devuelve código de salida 0 aunque el servidor responda 500. Para curl, "he recibido una respuesta" ya es éxito. En un script con set -e eso 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-time para 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.

bash
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 -P es 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

bash
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 / zgrep sobre 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.*.gz busca 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.

makefile
.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_modules
bash
make            # 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 en Makefile no convierta tabs a espacios.

💡 Por qué merece la pena aunque uses npm scripts: make no depende del lenguaje. Cuando tu proyecto tiene un backend en Go, un frontend en Node y scripts en Python, make test es un único punto de entrada para los tres. Y .PHONY declara las tareas que no producen un archivo con ese nombre — sin eso, si existe una carpeta llamada test, make test no 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.

bash
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 # cerrarla

Atajos básicos (todos empiezan con el prefijo Ctrl+b):

AtajoAcción
Ctrl+b dDesengancharse
Ctrl+b cNueva ventana
Ctrl+b n / pVentana siguiente / anterior
Ctrl+b %Dividir en vertical
Ctrl+b "Dividir en horizontal
Ctrl+b flechasMoverse 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

bash
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

2. Cuántos errores 500 hubo hoy

bash
grep "$(date +%d/%b/%Y)" /var/log/nginx/access.log | awk '$9 == 500' | wc -l

3. Buscar credenciales filtradas en el código antes de un commit

bash
grep -rEn "(api[_-]?key|password|secret|token)\s*[=:]\s*['\"][^'\"]{8,}" \
     --include="*.{js,ts,py,go,php}" --exclude-dir=node_modules ./src

4. Cambiar una URL en todos los archivos de configuración, con copia de seguridad

bash
find ./config -name "*.yml" -exec sed -i.bak 's|http://viejo|https://nuevo|g' {} +

5. Comprobar que 50 URLs de tu web responden 200

bash
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

bash
find / -type f -size +50M -exec du -h {} + 2>/dev/null | sort -rh | head -10

7. Extraer los tiempos de respuesta de un log y calcular la media y el máximo

bash
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

bash
tail -f app.log | grep --line-buffered -C 2 -E "ERROR|FATAL"

⚠️ --line-buffered es imprescindible con tail -f | grep. Sin él, grep acumula 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ásicaModernaPor qué
greprg (ripgrep)5-10× más rápido, respeta .gitignore
findfdSintaxis mucho más simple: fd "\.log$"
catbatColoreado y con números de línea
lsezaColores, iconos, integración con Git
dudust / ncduVisual, navegable
tophtop / btopInteractivo y legible
cdzoxideAprende tus rutas: z proyecto te lleva
fzf📌 Buscador difuso: se integra con Ctrl+R

💡 Si solo instalas dos, que sean fzf y ripgrep. fzf sustituye el Ctrl+R de Bash por un buscador difuso interactivo sobre todo tu historial, y se integra con find, git checkout y 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:

bash
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.log

Responde a cada pregunta con una sola línea de comandos:

  1. ¿Cuántas peticiones hay en total?
  2. ¿Cuántas devolvieron un error 500?
  3. ¿Cuáles son las 5 IPs con más peticiones, con su recuento?
  4. ¿Qué ruta se pidió más veces?
  5. ¿Cuántos bytes se sirvieron en total, expresado en MB con dos decimales?
  6. ¿Qué IPs provocaron algún error 500? (sin repetidos)
  7. ¿Cuál es el tiempo de respuesta medio en milisegundos?
  8. Muestra un recuento por código de estado, ordenado de más a menos frecuente.
  9. Guarda en sospechosas.txt las IPs con más de 5 errores 4xx o 5xx.
  10. Genera un informe.json válido (compruébalo con jq .) con: total de peticiones, número de errores, número de IPs únicas y bytes totales.

Extra

  1. Escribe analiza-log.sh que reciba la ruta de un log como argumento y produzca todo el informe. Con set -euo pipefail, validación del argumento, y que pase shellcheck.
  2. Añade una opción --top N que 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 -1 te dice cuántas hay, y head -1 acceso.log te 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 directamente awk '$9 == 500 {n++} END {print n+0}' en un solo proceso. El +0 fuerza a imprimir 0 en 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 ms pegado; quítalo con sub(/ms/, "", $11) dentro de awk, o con tr -d 'ms' antes.
  • Punto 9: cuenta por IP solo las líneas de error ($9 >= 400) y filtra el recuento con awk '$1 > 5 {print $2}' al final — recuerda que tras uniq -c la cuenta pasa a ser $1.
  • Punto 10: usa jq -n --argjson para los números, nunca concatenación de cadenas:
    bash
    jq -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.

bash
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 habituales

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