COMANDOS LINUX - CHEATSHEET

Llevo años trasteando con servidores, contenedores y terminales (este mismo blog vive en un droplet con Docker), y hay un puñado de comandos que acabo usando una y otra vez. El problema es que, salvo los cuatro de siempre, las opciones concretas nunca me las acuerdo, y termino buscando lo mismo por enésima vez. Así que este post es sobre todo para mí: una chuleta con los comandos y opciones que de verdad uso, y al final unos cuantos casos reales donde se combinan entre sí.

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 -v "debug" app.log               # las que NO contienen "debug"
grep -c "error" app.log               # cuántas líneas coinciden
grep -w "user" app.log                # palabra completa ("user", no "username")
grep -E "error|warning" app.log       # varias opciones (expresión regular extendida)
grep -C 3 "Exception" app.log         # 3 líneas de contexto antes y después
grep -A 5 "Exception" app.log         # 5 líneas después (-B para antes)
grep -rn "TODO" .                     # recursivo en todo el directorio
grep -rl "TODO" .                     # solo el nombre de los ficheros
grep -rn "TODO" --include="*.js" .    # solo en ficheros .js
grep -rn "TODO" --exclude-dir=node_modules .
zgrep "error" app.log.gz              # dentro de un fichero comprimido

El -C es de los que más uso: una línea de error sola rara vez te dice nada, y el contexto de alrededor suele ser la mitad del diagnóstico.

find: buscar ficheros

find . -name "*.log"                  # por nombre
find . -iname "readme*"               # sin distinguir mayúsculas
find . -type f                        # solo ficheros (-type d para directorios)
find . -maxdepth 1 -type d            # sin bajar más de un nivel
find . -mtime -1                      # modificados en las últimas 24 horas
find . -mtime +30                     # modificados hace más de 30 días
find . -size +100M                    # más grandes de 100 MB
find . -empty                         # vacíos
find . -name "*.tmp" -delete          # buscar y borrar
find . -name "*.sh" -exec chmod +x {} +   # ejecutar un comando sobre cada resultado

Un consejo que me ahorró algún susto: antes de lanzar cualquier find con -delete o -exec rm, lánzalo primero sin esa parte, para ver qué va a borrar exactamente.

curl: hablar con HTTP

curl https://www.mallotore.com                     # GET normal
curl -I https://www.mallotore.com                  # solo las cabeceras
curl -L http://mallotore.com                       # seguir redirecciones
curl -o pagina.html https://www.mallotore.com      # guardar con el nombre que quieras
curl -O https://ejemplo.com/fichero.zip            # guardar con el nombre original
curl -s https://api.ejemplo.com/users              # sin barra de progreso
curl -v https://www.mallotore.com                  # todo el detalle (TLS, cabeceras...)
curl -u usuario:clave https://api.ejemplo.com      # autenticación básica
curl -H "Authorization: Bearer $TOKEN" https://api.ejemplo.com

curl -X POST https://api.ejemplo.com/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Jaime"}'                           # POST con JSON

curl -F "file=@foto.jpg" https://api.ejemplo.com/upload    # subir un fichero

curl -s -o /dev/null -w "%{http_code}\n" https://www.mallotore.com   # solo el código de estado

Existe también la opción -k, que ignora los errores de certificado. Útil para salir del paso en local, pero si te ves usándola contra algo en producción, el problema no es curl.

lsof: quién tiene qué abierto

sudo lsof -i :8080         # qué proceso usa el puerto 8080
sudo lsof -i -nP           # todas las conexiones de red (-nP: sin resolver nombres, más rápido)
lsof -p 1234               # ficheros abiertos por el proceso 1234
lsof -u jaime              # ficheros abiertos por un usuario
lsof +D /var/log           # quién tiene abierto algo dentro de un directorio
sudo lsof +L1              # ficheros borrados que siguen ocupando espacio

El último es oro, y lo explico más abajo en los casos de uso.

nc: probar conexiones de red

nc (netcat) es la navaja suiza de la red: abre una conexión TCP o UDP a donde le digas, o se queda escuchando en un puerto. Yo lo uso sobre todo para responder a la pregunta "¿llego a ese servicio o no?", antes de ponerme a sospechar de la aplicación.

nc -zv mallotore.com 443        # ¿está abierto el puerto 443? (-z: solo comprobar, sin enviar datos)
nc -zv servidor 20-25           # un rango de puertos
nc -zv -w 3 servidor 5432       # con timeout de 3 segundos, para no quedarte esperando
nc -zvu servidor 53             # por UDP
nc -l 1234                      # quedarse escuchando en el puerto 1234

Y un truco que me ha sacado de más de un apuro: pasar un fichero entre dos máquinas sin montar nada más. En la que recibe:

nc -l 1234 > fichero.tar.gz

Y en la que envía:

nc servidor 1234 < fichero.tar.gz

Ojo, que va sin cifrar, así que solo para redes de confianza. Y un aviso: hay varias versiones de netcat (la de OpenBSD, la tradicional, ncat de Nmap) y las opciones cambian un poco entre ellas. Por ejemplo, en la tradicional, para escuchar hay que escribir nc -l -p 1234. Si algo no te funciona como esperas, mira cuál tienes instalada.

chmod y chown: permisos

Los permisos se leen en tres grupos (usuario, grupo, resto), y cada uno suma: lectura = 4, escritura = 2, ejecución = 1. Así, 7 es todo (4+2+1), 6 es leer y escribir, y 5 leer y ejecutar.

chmod 644 fichero.txt      # yo leo y escribo, el resto solo lee
chmod 755 script.sh        # yo todo, el resto lee y ejecuta
chmod 600 ~/.ssh/id_rsa    # solo yo, y nadie más (SSH lo exige para las claves)
chmod +x script.sh         # darle permiso de ejecución
chmod u+w,g-w,o-r fichero  # en modo letras: añadir y quitar permisos concretos
chmod -R 755 carpeta/      # recursivo
chown jaime:www-data fichero       # cambiar dueño y grupo
chown -R jaime:jaime carpeta/

Ojo con el chmod -R 755 a lo bruto: deja todos los ficheros como ejecutables. Lo fino es tratar directorios y ficheros por separado:

find . -type d -exec chmod 755 {} +
find . -type f -exec chmod 644 {} +

Y si en algún foro te dicen que lo arregles con chmod 777, desconfía: eso no arregla nada, solo quita la puerta.

tar: empaquetar y comprimir

tar -czvf backup.tar.gz carpeta/          # crear comprimido con gzip
tar -xzvf backup.tar.gz                   # extraer
tar -xzvf backup.tar.gz -C /destino       # extraer en otro directorio
tar -tzvf backup.tar.gz                   # ver el contenido sin extraer
tar -czvf backup.tar.gz --exclude="node_modules" proyecto/
tar --zstd -cvf backup.tar.zst carpeta/   # con zstd, más rápido y comprime mejor

La forma de acordarme de las letras: crear, extraer o listar; z de gzip; v de verbose; y f siempre al final, porque justo detrás va el nombre del fichero.

Y unos cuantos más que uso a diario

Espacio en disco:

df -h                                  # espacio libre por disco
du -sh carpeta/                        # lo que ocupa una carpeta
du -h --max-depth=1 | sort -h          # qué ocupa más dentro de la carpeta actual

Procesos y puertos:

ps aux | grep java                     # buscar un proceso
pgrep -a java                          # lo mismo, más limpio
kill 1234                              # pedirle a un proceso que termine
kill -9 1234                           # obligarlo (último recurso, no le deja limpiar)
pkill java                             # terminar procesos por nombre, sin buscar el PID
pkill -f "java -jar mi-app.jar"        # buscando en la línea de comandos completa
pkill -u jaime                         # todos los procesos de un usuario
sudo ss -tulpn                         # puertos a la escucha (el sustituto de netstat)

pkill lo uso muchísimo, pero con el mismo cuidado que el find -delete: sobre todo con -f, el patrón puede coincidir con más procesos de los que crees. Antes de matar nada, lanzo pgrep -af con el mismo patrón para ver exactamente a quién le va a tocar.

Logs en directo:

tail -f app.log                        # ir viendo lo que se escribe
tail -n 200 app.log                    # las últimas 200 líneas
journalctl -u nginx -f                 # logs de un servicio de systemd
docker logs -f --tail 100 ghost        # logs de un contenedor

Copiar entre máquinas:

scp fichero.txt usuario@servidor:/ruta/
rsync -avz --progress carpeta/ usuario@servidor:/ruta/   # solo copia lo que ha cambiado

JSON en la terminal, con jq, que no viene de serie pero es imprescindible:

curl -s https://api.ejemplo.com/users | jq '.'              # formateado y con colores
curl -s https://api.ejemplo.com/users | jq '.[].name'       # sacar un campo

Repetir un comando cada X segundos:

watch -n 5 "df -h"

Y un atajo que no es un comando pero que me cambió la vida: Ctrl+R en la terminal busca en el historial. Escribes un trozo del comando que lanzaste hace tres días, y ahí aparece.

Casos de uso

Donde de verdad se nota lo útiles que son es cuando los combinas. Estos son los que más me han salvado.

"No arranca porque el puerto está ocupado". Pasa mucho al levantar contenedores:

sudo lsof -i :80
# o
sudo ss -tulpn | grep :80

Te dice qué proceso lo tiene y su PID. A partir de ahí, decides si lo paras o cambias de puerto.

"La aplicación no llega a la base de datos". Antes de revisar cadenas de conexión y configuraciones, compruebo si hay conexión siquiera. Con Docker Compose, desde dentro del propio contenedor:

docker compose exec ghost nc -zv mysqldb 3306

Si responde, el problema está en la aplicación (credenciales, configuración). Si no, en la red. Te ahorra buscar el fallo en el sitio equivocado. Eso sí, nc tiene que estar instalado en el contenedor, cosa que en imágenes muy mínimas no siempre pasa.

Y una trampa que descubrí probándolo en la imagen alpine de Ghost, donde nc es el de BusyBox: si el puerto está cerrado, no imprime nada. Ni un "Connection refused" ni nada. Cuando funciona te dice open, y cuando no, silencio. Para distinguirlo no te fíes del texto, mira el código de salida con echo $? (0 si llega, 1 si no).

"El disco está lleno". El clásico de cualquier máquina. En producción no me ha tocado sufrirlo, pero precisamente por lo típico que es merece la pena tenerlo claro. El orden importa: primero confirmar qué partición está llena, y luego bajar hasta el directorio o fichero concreto:

df -h                                       # PRIMERO: qué partición está llena
du -sh /var/log/* | sort -rh | head -10     # LUEGO: qué directorio o fichero concreto
sudo find / -xdev -type f -size +500M 2>/dev/null   # ficheros enormes en esa partición
docker system df                            # si usas Docker, lo que ocupan imágenes y volúmenes

Y aquí viene la trampa en la que cae todo el mundo alguna vez: encuentras un log gigante, lo borras con rm... y df -h sigue diciendo que el disco está lleno. ¿Por qué? Porque si un proceso tiene ese fichero abierto, rm solo borra el nombre. El contenido (el inodo) sigue vivo, huérfano pero ocupando disco, hasta que el proceso cierre el fichero. Para encontrarlos:

sudo lsof +L1      # ficheros borrados pero que algún proceso sigue teniendo abiertos

Si ya has caído, la solución es reiniciar ese proceso para que suelte el fichero. Pero lo correcto es no borrar el log, sino truncarlo, es decir, vaciarlo sin eliminarlo:

truncate -s 0 /var/log/app/error.log
# equivalente:
> /var/log/app/error.log

Eso sí, con un matiz muy importante: truncar es destructivo e irreversible. No es como mover algo a la papelera: el contenido desaparece por completo en ese instante, y no hay forma de comprimirlo o archivarlo después. Si ese log tiene valor para entender qué ha pasado (no solo para apagar el fuego, sino para averiguar por qué empezó), la única secuencia que conserva la información es comprimirlo antes de truncar, nunca después:

gzip -c /var/log/app/error.log > /var/log/app/error.log.$(date +%Y%m%d_%H%M%S).gz
truncate -s 0 /var/log/app/error.log

Al final, la decisión real no es una secuencia de pasos, sino un equilibrio: preservar la evidencia (comprimir primero, y tardar un poco más) o restaurar el servicio ya (truncar ya, aceptando la pérdida). Las dos son legítimas según lo grave que sea la situación, pero en el momento de decidir tienes que elegir una, porque una vez truncado, ya no hay vuelta atrás.

Que no vuelva a pasar: logrotate. Lo mejor para el disco lleno por logs es no llegar a él. logrotate viene instalado en casi cualquier distribución y se encarga de rotar, comprimir y borrar los logs antiguos solo. Basta con un fichero en /etc/logrotate.d/miapp:

/var/log/miapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

Rota cada día, guarda 14 días comprimidos y borra lo más antiguo. El copytruncate es justo lo que hemos visto antes, pero automatizado: copia el log y trunca el original, para las aplicaciones que no saben volver a abrir su fichero de log. Tiene un pequeño precio: lo que se escriba justo entre la copia y el truncado se pierde. Si tu aplicación sabe reabrir sus logs cuando se lo pides, es mejor usar un postrotate que se lo indique.

Y si tus aplicaciones corren en Docker, los logs de los contenedores también crecen sin límite por defecto. Se controla en el propio compose.yaml, en cada servicio:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

Limpiar logs antiguos:

find /var/log/miapp -name "*.log" -mtime +30 -delete

Analizar los logs de nginx. Con el formato de log por defecto, la IP es el primer campo y el código de estado el noveno, así que:

# las 10 IPs que más peticiones hacen
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

# las URLs que están dando 404
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head

La primera es muy útil cuando notas el servidor cargado y sospechas de algún bot con demasiado entusiasmo.

Comprobar la redirección a HTTPS y la caducidad del certificado. Si has leído el post de SSL gratis con Let's Encrypt en Docker, esto te vendrá bien para verificar que todo va como debe:

curl -I http://mallotore.com                      # debería devolver un 301 hacia https
curl -vI https://www.mallotore.com 2>&1 | grep -i "expire"

Copia de seguridad del blog y llevarla a otra máquina. Esta es una versión de andar por casa, para ver cómo se combinan tar, date y rsync; la mía real es un script aparte que hace bastante más:

tar -czvf ghost-$(date +%F).tar.gz ghost/content
rsync -avz --progress ghost-*.tar.gz usuario@backup:/copias/

El $(date +%F) mete la fecha en el nombre (ghost-2026-09-26.tar.gz), así no machacas copias anteriores.

Vigilar que la web responde:

watch -n 10 'curl -s -o /dev/null -w "%{http_code}" https://www.mallotore.com'

Las versiones modernas

Casi todos los comandos de este post vienen de serie en cualquier Linux, y por eso merece la pena conocerlos: en cualquier servidor al que te conectes, van a estar ahí. Pero en tu máquina, hay alternativas modernas que son más rápidas y cómodas, y que yo ya uso casi siempre:

  • ripgrep (rg) en vez de grep -r: muchísimo más rápido, y respeta el .gitignore sin decirle nada.
  • fd en vez de find: sintaxis más simple (fd "\.log$") y también respeta el .gitignore.
  • bat en vez de cat: con colores y números de línea.
  • htop o btop en vez de top: lo mismo, pero legible.
  • ncdu en vez de du: navegas por las carpetas viendo qué ocupa más, de forma interactiva. Para el caso del disco lleno, es una maravilla.

Para cerrar

Ninguno de estos comandos es nuevo, y precisamente por eso siguen siendo tan útiles: llevan décadas ahí y seguirán estando. No hace falta saberse todas las opciones de memoria (yo desde luego no me las sé, por eso existe este post), pero sí saber que existen, para que cuando el disco esté lleno o el puerto ocupado sepas por dónde tirar. Lo demás es cuestión de tener la chuleta a mano.