🎯 Meta del capítulo: perder el miedo a la pantalla negra. Al terminar sabrás moverte por un servidor Linux, leer permisos, matar un proceso colgado, ver qué está ocupando el puerto 8080 y conectarte por SSH a una máquina remota — sin buscar en Google cada comando.
🧭 ¿Por qué esto es el capítulo 1 de todo? Tu código correrá en Linux. Tu base de datos correrá en Linux. Tu contenedor Docker es un Linux. Tu pipeline de CI ejecuta un script de shell. El día que algo falle en producción a las 3 AM no vas a tener interfaz gráfica: vas a tener una terminal y 20 minutos. Este capítulo es ese seguro de vida.
T1.1 · Terminal, shell, Bash: no son lo mismo
Tres palabras que se usan como sinónimos y no lo son:
┌──────────────────────────────────────────────────────────┐
│ TERMINAL (la ventana) │
│ Windows Terminal, iTerm2, GNOME Terminal, Alacritty… │
│ Solo dibuja texto y captura tu teclado. │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ SHELL (el intérprete) │ │
│ │ bash, zsh, fish, sh, PowerShell… │ │
│ │ Lee lo que escribes y decide qué ejecutar. │ │
│ │ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ PROGRAMAS (lo que de verdad corre) │ │ │
│ │ │ ls, git, docker, python, node… │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘- Terminal = el marco de la ventana. Puedes cambiarlo sin que nada más cambie.
- Shell = el programa que interpreta tus órdenes.
bashes el estándar de facto en servidores. - Comandos = programas de verdad que viven en el disco (
/usr/bin/lses un archivo).
🧠 Analogía: la terminal es el teléfono, la shell es la persona que contesta y los comandos son las cosas que esa persona hace por ti. Cambiar de teléfono no cambia quién contesta.
¿Qué shell tengo?
echo $SHELL # la shell configurada para tu usuario → /bin/bash o /bin/zsh
bash --version # versión de bash⚠️ macOS usa
zshpor defecto desde 2019, y subashes la versión 3.2 de 2007 (por temas de licencia GPLv3). Si escribes scripts en un Mac y los despliegas en un servidor Linux con Bash 5, cosas como los arrays asociativos (declare -A) fallarán en el Mac. Solución: instala Bash moderno conbrew install bash.
Windows: usa WSL2 (Windows Subsystem for Linux). No es una emulación: es un kernel Linux real. Instálalo con wsl --install en PowerShell como administrador. A partir de ahí trabajas como si estuvieras en Ubuntu.
T1.2 · Anatomía de un comando
Todos los comandos siguen la misma gramática:
ls -la /var/log
│ │ │
│ │ └── ARGUMENTO: sobre qué actúa
│ └─────────── OPCIONES / FLAGS: cómo actúa
└────────────────────── COMANDO: qué programa ejecutarLas opciones vienen en dos sabores:
| Forma | Ejemplo | Notas |
|---|---|---|
| Corta | -l | Una letra. Se pueden agrupar: -l -a -h = -lah |
| Larga | --all | Más legible. En scripts, usa siempre la larga |
| Con valor | -p 8080 o --port=8080 | El espacio o el = según el programa |
💡 Regla de oro para scripts: en la terminal escribe
-la(rápido); en un script escribe--long --all(legible dentro de seis meses). Tu yo del futuro no recuerda qué era-z.
¿Cómo sé qué hace un comando? Tres niveles, de rápido a exhaustivo:
ls --help # resumen rápido (1 pantalla)
man ls # manual completo (se sale con q, se busca con /palabra)
type ls # ¿es un programa, un alias o algo de la shell?
which ls # ¿en qué archivo del disco vive?🧠
manes un superpoder infravalorado. Dentro deman:/busca,nva a la siguiente coincidencia,qsale.man 5 crontabte da la sección 5 (formatos de archivo) en vez de la 1 (comandos) — las secciones existen porque hay nombres que son a la vez comando y archivo.
T1.3 · El sistema de archivos: un solo árbol
En Windows hay varios árboles (C:\, D:\). En Linux hay uno solo, que empieza en / (raíz). Los discos, USBs y unidades de red se "montan" como carpetas dentro de ese árbol.
/ ← la raíz. Todo cuelga de aquí.
├── bin/ → comandos esenciales (ls, cp, bash)
├── etc/ → CONFIGURACIÓN del sistema (todo texto plano)
│ ├── nginx/
│ ├── ssh/sshd_config
│ └── passwd → lista de usuarios
├── home/ → carpetas personales de los usuarios
│ └── ana/ → "~" para Ana
├── opt/ → software de terceros instalado a mano
├── proc/ → NO es disco real: estado vivo del kernel
├── root/ → home del usuario root (¡no es "/"!)
├── srv/ → datos servidos (a veces webs)
├── tmp/ → temporal. Se BORRA al reiniciar
├── usr/ → programas de usuario
│ ├── bin/ → la mayoría de comandos que usas
│ ├── lib/ → librerías
│ └── local/ → lo que instalas tú compilando
└── var/ → datos VARIABLES (crecen con el tiempo)
├── log/ → 📌 LOGS. Aquí vas cuando algo falla
├── lib/ → estado de programas (bases de datos, docker)
└── www/ → webs servidas por Apache/NginxLas cuatro carpetas que de verdad vas a pisar como backend:
| Carpeta | Para qué | Ejemplo real |
|---|---|---|
/etc | Configurar servicios | /etc/nginx/sites-available/miapp |
/var/log | Investigar fallos | /var/log/nginx/error.log |
/home/usuario | Tu código y tus claves | ~/.ssh/, ~/apps/miapp |
/tmp | Basura de usar y tirar | descargas temporales de un script |
Rutas absolutas vs relativas
/var/log/nginx/error.log # ABSOLUTA: empieza en / → siempre significa lo mismo
../logs/error.log # RELATIVA: depende de dónde estés paradoAtajos que debes tener en los dedos:
| Símbolo | Significa |
|---|---|
. | El directorio actual |
.. | El directorio padre |
~ | Tu carpeta personal (/home/tu-usuario) |
- | El directorio anterior (cd - = "vuelve donde estaba") |
/ | La raíz |
⚠️ Error clásico: en un script, nunca uses rutas relativas. Un cron job se ejecuta desde
/y tu./config.ymlno existirá allí. Los scripts usan rutas absolutas o calculan la suya:bashDIR_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"Esta línea (que explicamos en T2) da la carpeta donde vive el script, sin importar desde dónde lo llames.
T1.4 · Moverse y manipular archivos
Navegación
pwd # ¿dónde estoy? (print working directory)
cd /var/log # ir a una ruta absoluta
cd .. # subir un nivel
cd # ir a mi home (sin argumentos = ~)
cd - # volver al anterior
ls # listar
ls -l # formato largo (permisos, dueño, tamaño, fecha)
ls -a # incluir ocultos (los que empiezan por .)
ls -h # tamaños legibles (4.0K en vez de 4096)
ls -lt # ordenar por fecha, más reciente primero
ls -lS # ordenar por tamaño
ls -la /etc # el combo que usarás el 90% del tiempo💡 La tecla TAB es obligatoria. Escribe
cd /var/loy pulsa TAB: se completa solo. Pulsa TAB dos veces y te muestra todas las opciones. No autocompletar es escribir con un dedo.
Crear, copiar, mover, borrar
mkdir carpeta # crear carpeta
mkdir -p a/b/c # crear el árbol entero aunque no exista (-p = parents)
touch archivo.txt # crear archivo vacío (o actualizar su fecha)
cp origen.txt destino.txt # copiar archivo
cp -r carpeta/ copia/ # copiar carpeta y su contenido (-r = recursive)
cp -a carpeta/ copia/ # -a = archivo: conserva permisos, fechas, enlaces
mv viejo.txt nuevo.txt # RENOMBRAR (mover a otro nombre)
mv archivo.txt /tmp/ # MOVER a otra carpeta
rm archivo.txt # borrar archivo
rm -r carpeta/ # borrar carpeta con contenido
rm -f archivo # forzar (no preguntar, no fallar si no existe)⚠️⚠️
rmNO tiene papelera. Lo que borras, se fue. Tres reglas que te ahorrarán un incidente:
- Nunca escribas
rm -rfcon una variable sin comprobar: si$DIRestá vacía,rm -rf $DIR/se convierte enrm -rf /. Escriberm -rf "${DIR:?variable vacía}"— el:?aborta si está vacía.- Antes de borrar con comodines, lista primero: cambia el
rmporlsy mira qué sale.ls *.log→ si la lista es la correcta, ahora sírm *.log.- En servidores, alias
rm='rm -i'es una tirita, no una cura — te acostumbras a pulsar "sí" sin leer. Mejor: piensa antes de pulsar Enter.
Leer archivos sin abrir un editor
cat archivo.txt # volcar entero (solo para archivos pequeños)
less archivo.log # 📌 EL BUENO: paginado, buscable, no carga todo en RAM
head -n 20 archivo.log # primeras 20 líneas
tail -n 20 archivo.log # últimas 20 líneas
tail -f /var/log/nginx/error.log # 📌 SEGUIR EN VIVO (follow) — el comando del debugging
tail -f app.log | grep ERROR # seguir en vivo, filtrando solo errores
wc -l archivo.log # contar líneas💡
tail -fes el comando que más vas a usar en tu vida profesional. Despliegas, ejecutastail -fsobre el log, provocas la petición y ves el error aparecer en tiempo real. Se corta conCtrl+C. Si el archivo rota (se renombra), usatail -F(mayúscula): reengancha solo.
Dentro de less: /texto busca, n/N siguiente/anterior, G al final, g al principio, q sale, F imita a tail -f.
T1.5 · Permisos: el tema que todos evitan (y que rompe todo)
Cuando haces ls -l ves algo así:
-rw-r--r-- 1 ana desarrollo 1420 jul 28 10:12 config.yml
drwxr-xr-x 2 ana desarrollo 4096 jul 28 10:10 scripts
│└┬┘└┬┘└┬┘ │ │
│ │ │ │ │ └── GRUPO propietario
│ │ │ │ └───────── USUARIO propietario
│ │ │ └──────────────── permisos para OTROS (todos los demás)
│ │ └─────────────────── permisos para el GRUPO
│ └────────────────────── permisos para el DUEÑO
└──────────────────────── tipo: - archivo · d directorio · l enlaceCada bloque de tres son los mismos tres permisos:
| Letra | Valor | En un ARCHIVO | En un DIRECTORIO |
|---|---|---|---|
r | 4 | Leer el contenido | Listar lo que hay dentro (ls) |
w | 2 | Modificar el contenido | Crear/borrar archivos dentro |
x | 1 | Ejecutarlo como programa | Entrar en él (cd) |
🧠 La trampa de
xen directorios. En un directorio,xno es "ejecutar": es atravesar. Un directorio conrpero sinxte deja ver los nombres pero no abrir nada dentro. Uno conxpero sinrte deja abrir archivos si sabes el nombre exacto, pero no listarlos. Por eso los directorios casi siempre son755y los archivos644.
Notación numérica (la que verás en todos lados)
Se suman los valores: rwx = 4+2+1 = 7, rw- = 4+2 = 6, r-x = 4+1 = 5, r-- = 4.
644 = rw- r-- r-- → dueño lee/escribe, resto solo lee (archivos normales)
755 = rwx r-x r-x → dueño todo, resto lee y ejecuta (directorios y scripts)
600 = rw- --- --- → SOLO el dueño. Nadie más ve nada (claves, .env)
777 = rwx rwx rwx → ⚠️ cualquiera puede todo (casi siempre un error)chmod 755 script.sh # asignar permisos con números
chmod +x script.sh # añadir permiso de ejecución (forma simbólica)
chmod -R 755 carpeta/ # recursivo
chown ana:desarrollo archivo.txt # cambiar dueño:grupo
chown -R www-data:www-data /var/www # el clásico al desplegar una web⚠️
chmod 777es el "apaga y enciende" de los permisos: nunca es la solución. Si algo falla por permisos, la pregunta correcta es "¿qué usuario está ejecutando el proceso y qué necesita leer?". Poner 777 en/var/wwwsignifica que cualquier proceso comprometido del servidor puede reescribir tu web. En claves SSH es incluso peor: SSH se niega a usar una clave privada que no sea600, precisamente para protegerte.
Casos reales que te vas a encontrar
# 1. "Permission denied" al ejecutar mi script
chmod +x deploy.sh && ./deploy.sh
# 2. Nginx da 403 Forbidden sobre mi web
sudo chown -R www-data:www-data /var/www/miapp
sudo chmod -R 755 /var/www/miapp
# 3. SSH rechaza mi clave: "Permissions 0644 are too open"
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
# 4. Mi archivo .env con contraseñas
chmod 600 .env # que solo lo lea el dueñosudo: pedir permisos de administrador
sudo apt update # ejecutar UN comando como root
sudo -u postgres psql # ejecutar como OTRO usuario
sudo -i # abrir una shell como root (sal con exit)💡
sudo !!repite el último comando con sudo. Te ahorra reescribir la línea entera cuando se te olvida. (!!es "el comando anterior" en la historia de Bash.)
⚠️ No trabajes como root todo el rato. Un
rm -rfmal escrito como usuario normal rompe tu carpeta; como root rompe el servidor.sudoexiste para que el momento de escribirlo sea el momento de pensar.
T1.6 · Procesos: qué está corriendo y cómo pararlo
Un proceso es un programa en ejecución. Cada uno tiene un PID (número identificador).
ps aux # todos los procesos del sistema
ps aux | grep node # filtrar los de node
pgrep -af node # más limpio: PIDs + línea de comando completa
top # monitor en vivo (q para salir)
htop # 📌 el mismo pero usable (sudo apt install htop)Matar un proceso
kill 12345 # pedir amablemente que termine (señal TERM/15)
kill -9 12345 # MATAR sin piedad (señal KILL/9)
pkill -f "node servidor.js" # matar por nombre/patrón, sin buscar el PID🧠 La diferencia entre
killykill -9importa mucho en producción.
kill(SIGTERM) es "por favor, termina": el programa recibe el aviso, cierra conexiones a la base de datos, termina las peticiones en curso y sale limpio. Esto se llama graceful shutdown.kill -9(SIGKILL) es "desenchufa el cable": el kernel lo elimina, el programa no se entera de nada. Transacciones a medias, archivos corruptos, conexiones colgadas.Regla: siempre
killprimero. Solokill -9si tras 30 segundos sigue vivo. Docker hace exactamente esto:docker stopenvía SIGTERM, espera 10s y luego SIGKILL.
Trabajos en primer y segundo plano
npm run dev # ocupa la terminal
Ctrl+C # matarlo
Ctrl+Z # PAUSARLO y recuperar la terminal
bg # reanudarlo en SEGUNDO plano
fg # traerlo de vuelta al primer plano
jobs # ver los trabajos de esta terminal
npm run dev & # lanzarlo directamente en segundo plano
nohup npm start & # que sobreviva al cerrar la sesión SSH⚠️
&ynohupno son forma de correr algo en producción. Si el servidor reinicia, tu app no vuelve. Para eso está systemd (capítulo 14) o Docker (capítulo 12), que reinician el proceso automáticamente si muere.
T1.7 · Redes: los cinco comandos que resuelven el 90% de los problemas
# 1. ¿Qué está escuchando en qué puerto? (EL comando cuando "el puerto está ocupado")
ss -tulpn
sudo ss -tulpn | grep :8080
# 2. ¿Llego a esa máquina?
ping google.com
# 3. ¿Responde ese servicio HTTP?
curl -I https://miapp.com # solo cabeceras
curl -v https://miapp.com # verboso: ver todo el intercambio TLS y HTTP
# 4. ¿A qué IP resuelve este dominio?
dig +short miapp.com
nslookup miapp.com
# 5. ¿Está abierto ese puerto en el servidor remoto?
nc -zv miapp.com 5432El flujo mental cuando "no funciona":
graph TD
A["Mi app no responde"] --> B{"¿El proceso está vivo?<br/>ps aux | grep app"}
B -->|No| C["Míralo en los logs:<br/>journalctl -u miapp -n 50"]
B -->|Sí| D{"¿Escucha en el puerto?<br/>ss -tulpn | grep 8080"}
D -->|No| E["Error de configuración:<br/>¿bind a 127.0.0.1 en vez de 0.0.0.0?"]
D -->|Sí| F{"¿Responde en local?<br/>curl localhost:8080"}
F -->|No| G["El fallo está DENTRO de la app"]
F -->|Sí| H{"¿Responde desde fuera?"}
H -->|No| I["Firewall o proxy:<br/>ufw status · config de Nginx"]
H -->|Sí| J["El problema es DNS o del cliente"]💡 El error de red nº1 de los principiantes: tu app escucha en
127.0.0.1:8080(solo localhost) y te extraña que no responda desde fuera ni desde otro contenedor Docker. Dentro de un contenedor siempre debes escuchar en0.0.0.0. Enss -tulpnlo ves en la columna de dirección:127.0.0.1:8080es privado;0.0.0.0:8080o*:8080acepta desde cualquier sitio.
T1.8 · SSH: entrar en un servidor remoto
SSH (Secure Shell) es cómo te conectas a una máquina remota. Es cifrado y es el estándar universal.
ssh ana@203.0.113.10 # conectar (te pedirá contraseña)
ssh -p 2222 ana@203.0.113.10 # si el puerto no es el 22
exit # salirClaves SSH: deja de usar contraseñas
Una clave SSH es un par de archivos: una privada (secreta, nunca sale de tu máquina) y una pública (se copia al servidor). El servidor te reconoce sin que viaje ninguna contraseña.
# 1. Generar el par (ed25519 es el algoritmo recomendado hoy: corto y seguro)
ssh-keygen -t ed25519 -C "ana@portatil"
# → crea ~/.ssh/id_ed25519 (PRIVADA) y ~/.ssh/id_ed25519.pub (PÚBLICA)
# 2. Copiar la pública al servidor
ssh-copy-id ana@203.0.113.10
# 3. Ya puedes entrar sin contraseña
ssh ana@203.0.113.10⚠️ La clave privada NUNCA se comparte, NUNCA se sube a Git, NUNCA se copia al servidor. Si alguien te pide tu
id_ed25519(sin.pub), te está intentando engañar. Lo que se copia es siempre el archivo terminado en.pub. Y ponle passphrase cuando la generes: si te roban el portátil, la clave sigue siendo inútil sin ella.
~/.ssh/config: el archivo que te cambia la vida
En vez de recordar IPs, usuarios y puertos, escribes esto una vez:
# ~/.ssh/config
Host produccion
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
Host github.com
User git
IdentityFile ~/.ssh/id_githubY a partir de ahí: ssh produccion. También funciona con scp produccion:... y con Git.
Copiar archivos entre máquinas
scp archivo.txt produccion:/home/deploy/ # subir
scp produccion:/var/log/app.log ./ # bajar
scp -r carpeta/ produccion:/home/deploy/ # carpeta entera
# rsync: mejor para carpetas grandes — solo copia lo que cambió
rsync -avz --progress ./dist/ produccion:/var/www/miapp/
rsync -avz --delete ./dist/ produccion:/var/www/miapp/ # --delete: espeja exacto💡
rsyncsobrescpsiempre que sea una carpeta. Si se corta la transferencia,rsynccontinúa donde iba;scpempieza de cero. Y-zcomprime en tránsito.
Túnel SSH: acceder a un puerto privado del servidor
Tu base de datos de producción no debe estar abierta a internet. Pero necesitas conectarte con tu cliente gráfico. Solución: un túnel.
ssh -L 5433:localhost:5432 produccion
# │ │ │
# │ │ └── puerto en el SERVIDOR (Postgres remoto)
# │ └──────────── "localhost" visto DESDE el servidor
# └───────────────── puerto en TU máquinaAhora tu localhost:5433 es el PostgreSQL del servidor. Conéctate con DBeaver a localhost:5433 como si fuera local, sin abrir el 5432 a internet.
T1.9 · Instalar cosas y gestionar servicios
# Debian / Ubuntu
sudo apt update # refrescar la lista de paquetes (¡siempre primero!)
sudo apt install htop # instalar
sudo apt upgrade # actualizar lo instalado
sudo apt remove htop # desinstalar
# Fedora / RHEL / Rocky # Alpine (imágenes Docker pequeñas)
sudo dnf install htop # apk add htopsystemd gestiona los servicios (programas que corren siempre):
sudo systemctl status nginx # 📌 ¿está vivo? ¿por qué falló?
sudo systemctl start nginx # arrancar
sudo systemctl stop nginx # parar
sudo systemctl restart nginx # reiniciar
sudo systemctl reload nginx # recargar config SIN cortar conexiones
sudo systemctl enable nginx # que arranque solo al reiniciar el servidor
journalctl -u nginx -n 50 # últimas 50 líneas de log del servicio
journalctl -u nginx -f # seguir en vivo
journalctl -u nginx --since "10 min ago"💡
reloadvsrestart:restartmata el proceso y lo levanta (hay unos segundos de caída).reloadle dice que relea su configuración sin soltar las conexiones abiertas. Para cambios de config de Nginx, siemprereload— y antes,sudo nginx -tpara validar que la config no tiene errores de sintaxis. Recargar una config rota tira la web entera.
T1.10 · Espacio en disco y "el servidor no responde"
df -h # 📌 espacio por partición — lo PRIMERO que se mira
du -sh * # tamaño de cada elemento de la carpeta actual
du -sh /var/log/* | sort -rh | head -10 # los 10 mayores consumidores
free -h # memoria RAM disponible
uptime # cuánto lleva encendido + carga del sistema⚠️ Disco lleno = fallos absurdos e inexplicables. Cuando
/está al 100%, PostgreSQL deja de aceptar escrituras, Nginx no puede escribir logs, Docker no puede arrancar contenedores y los mensajes de error no tienen ningún sentido.df -hes siempre el primer comando de cualquier investigación. El culpable habitual:/var/logcon logs sin rotar, o imágenes viejas de Docker (docker system prune -alibera gigas).
T1.11 · Atajos de teclado que te hacen 3× más rápido
| Atajo | Qué hace |
|---|---|
Tab | Autocompletar (¡úsalo siempre!) |
Ctrl+C | Matar el comando actual |
Ctrl+D | Fin de entrada / cerrar sesión (como exit) |
Ctrl+L | Limpiar pantalla (igual que clear) |
Ctrl+R | 📌 Buscar en el historial — escribe parte del comando |
Ctrl+A / Ctrl+E | Ir al inicio / final de la línea |
Ctrl+W | Borrar la palabra anterior |
Ctrl+U | Borrar toda la línea |
Alt+. | Insertar el último argumento del comando anterior |
↑ / ↓ | Navegar el historial |
💡
Ctrl+Res el atajo que separa al novato del veterano. PulsaCtrl+R, escribedockery va apareciendo el último comando con docker que usaste. PulsaCtrl+Rotra vez para ir hacia atrás. Enter ejecuta, flecha derecha lo edita. Nunca más vuelvas a reescribir un comando largo.
history | grep docker # ver todos los comandos con "docker"
!! # repetir el último comando
!504 # repetir el comando nº 504 del historialT1.12 · Personalizar tu entorno
Tu shell lee un archivo de configuración al arrancar: ~/.bashrc (Bash) o ~/.zshrc (Zsh).
# ~/.bashrc
# Alias: atajos para comandos largos
alias ll='ls -lah'
alias gs='git status'
alias dc='docker compose'
alias ports='sudo ss -tulpn'
# Variables de entorno
export EDITOR=nano
export PATH="$HOME/.local/bin:$PATH" # añadir una carpeta a los ejecutables
# Función propia (más potente que un alias: acepta argumentos)
mkcd() { mkdir -p "$1" && cd "$1"; }source ~/.bashrc # aplicar los cambios sin cerrar la terminal🧠
PATHexplicado: cuando escribesgit, la shell busca un archivo llamadogiten cada carpeta listada en$PATH, por orden, y ejecuta el primero que encuentre. Por eso "command not found" casi nunca significa "no está instalado": significa "no está en el PATH". Compruébalo conecho $PATHywhich git.
T1.13 · Los cinco errores más comunes (y qué significan de verdad)
| Mensaje | Qué pasa realmente | Cómo arreglarlo |
|---|---|---|
command not found | No está instalado o no está en $PATH | which cmd, echo $PATH, instalar |
Permission denied | Tu usuario no tiene el permiso, o falta x | ls -l, chmod +x, ¿necesita sudo? |
No such file or directory | Ruta mal escrita, o estás en otra carpeta | pwd, ls, usa Tab para autocompletar |
Address already in use | Otro proceso ya ocupa ese puerto | sudo ss -tulpn | grep :8080 → kill PID |
No space left on device | Disco lleno | df -h, du -sh /var/log/*, limpiar |
💡 Cómo leer un error de verdad: lee la última línea (suele ser la causa) y la primera (suele ser el contexto). Lo del medio es la pila de llamadas. Y si el mensaje menciona una ruta o un número de línea, ve directo allí antes de buscar en Google.
✅ Ejercicio del capítulo
Vas a montar un pequeño "servidor" simulado en tu propia máquina y a diagnosticarlo. Todo esto funciona en Linux, macOS y WSL2.
Parte 1 — El terreno de juego
- Crea esta estructura con un solo comando (pista:
mkdir -pacepta llaves{}):~/practica-t1/ ├── app/ ├── config/ └── logs/ - Crea
config/app.confcon el textopuerto=8080(usaechoy redirección>). - Dale a
config/app.confpermisos600y comprueba conls -lque pone-rw-------. - Explica en un comentario por qué un archivo de configuración con credenciales debería ser
600y no644.
Parte 2 — Procesos y puertos
- Levanta un servidor web de una línea en el puerto 8080 y déjalo corriendo en segundo plano:bash
python3 -m http.server 8080 > ~/practica-t1/logs/servidor.log 2>&1 & - Averigua su PID de dos formas distintas (una con
ps, otra conssopgrep). - Comprueba que responde:
curl -I localhost:8080. ¿Qué código de estado devuelve? - Ahora intenta levantar otro servidor en el mismo puerto 8080. Anota el error exacto.
- Mata el primero con
kill(sin-9) y verifica que el puerto quedó libre.
Parte 3 — Diagnóstico
- Escribe los comandos que ejecutarías, en orden, para diagnosticar cada situación:
- "La web da 502 Bad Gateway."
- "El despliegue falló y no sé por qué."
- "El servidor va lentísimo desde ayer."
- Genera 1000 líneas de log falso y quédate solo con las que contengan
ERROR:bashCuenta cuántas son (for i in $(seq 1 1000); do echo "linea $i $( [ $((i % 7)) -eq 0 ] && echo ERROR || echo INFO )"; done > logs/app.loggrep -c), y muestra las 5 últimas.
Parte 4 — Tu entorno
- Añade a tu
~/.bashrc(o~/.zshrc) tres alias que de verdad vayas a usar y una funciónpuerto()que reciba un número y muestre qué proceso lo ocupa.
💡 Pistas de la solución (abre solo si te atascas)
- Punto 1:
mkdir -p ~/practica-t1/{app,config,logs}— la expansión de llaves es de Bash, no del comandomkdir; la shell la convierte en tres rutas antes de ejecutar nada. - Punto 4: con
644cualquier usuario del sistema puede leer el archivo. En un servidor compartido, o si un proceso web se ve comprometido, eso es tu contraseña de base de datos regalada.600limita la lectura al dueño. - Punto 6:
ps aux | grep http.serverda el PID en la segunda columna;sudo ss -tulpn | grep :8080lo da al final entre paréntesis (users:(("python3",pid=12345,...))). - Punto 8: el error es
OSError: [Errno 98] Address already in use. Es exactamente el mismo problema que verás con Docker, Nginx o tu API — y siempre se resuelve igual. - Punto 10, orden mental correcto: proceso vivo → puerto escuchando → responde en local → responde desde fuera. Para "va lentísimo":
uptime(carga),df -h(disco),free -h(RAM),htop(quién consume). - Punto 12: la función puede ser
puerto() { sudo ss -tulpn | grep ":$1"; }— con"$1"entre comillas, siempre.
🧠 Autoevaluación
Porque en un directorio x no significa "ejecutar" sino atravesar: entrar en él y acceder a lo que contiene. Sin x no puedes hacer cd ni abrir un archivo de dentro, aunque tengas r (que solo te deja listar los nombres). Por eso los directorios son 755 y no 644: quitarle la x a un directorio lo vuelve inaccesible aunque parezca legible.
- La app escucha solo en
127.0.0.1en vez de0.0.0.0. Compruébalo conss -tulpn: si la dirección es127.0.0.1:8080, solo acepta conexiones locales por diseño. - El firewall bloquea el puerto (
sudo ufw status, o el grupo de seguridad del proveedor cloud, que es un firewall externo que no ves desde la máquina). - El proxy inverso no está bien configurado: Nginx recibe en el 80/443 pero no reenvía al 8080, o apunta a un
proxy_passequivocado.
El orden importa: se descarta de dentro hacia fuera, porque cada capa depende de la anterior.
kill envía SIGTERM, una petición de terminar que el programa puede interceptar: cierra conexiones a la base de datos, termina las peticiones HTTP en curso, hace flush de los buffers y sale limpio (graceful shutdown). kill -9 envía SIGKILL, que el kernel ejecuta directamente y el programa no puede interceptar: se corta en seco.
En producción kill -9 puede dejar transacciones a medias, archivos temporales huérfanos, peticiones de usuarios cortadas y bloqueos sin liberar. Se usa kill primero y solo se recurre al -9 si el proceso no responde tras unos segundos — que es exactamente lo que hace docker stop.
Lo "arregla" porque elimina toda restricción: sea cual sea el usuario que ejecuta el proceso, podrá leer, escribir y ejecutar. Pero eso no resuelve el problema real (el proceso corre con el usuario equivocado), solo lo esconde — y abre el archivo a cualquier proceso de la máquina, incluido código malicioso que haya entrado por otra vía. La pregunta correcta siempre es: "¿con qué usuario corre este proceso y qué necesita exactamente?", y la respuesta suele ser un chown al usuario adecuado, no un chmod abierto a todos.
rsync, sin dudarlo. Si la transferencia se corta, rsync -avz --partial continúa donde se quedó, mientras que scp vuelve a empezar desde cero. Además rsync compara origen y destino y transfiere solo lo que ha cambiado, lo que en despliegues repetidos supone la diferencia entre segundos y minutos. Y -z comprime durante el envío.
Un túnel (ssh -L 5433:localhost:5432 servidor) crea un puerto en tu máquina que se conecta, a través de la conexión SSH cifrada, a un puerto del servidor. Resuelve un problema concreto: tu base de datos de producción no debería estar expuesta a internet (el puerto 5432 abierto es un objetivo constante de escaneos automáticos). Con un túnel, la base de datos solo escucha en localhost del servidor y tú accedes reutilizando la autenticación de SSH, que ya tienes con claves. Cero puertos nuevos abiertos.
df -h. Un disco lleno provoca fallos en cascada que no se parecen a un problema de disco: PostgreSQL rechaza escrituras, Nginx no puede escribir logs, Docker no arranca contenedores, las sesiones no se guardan. Como cada componente reporta su propio error genérico, se pierde mucho tiempo investigando la aplicación cuando la causa es una partición al 100%. Los sospechosos habituales son /var/log sin rotación e imágenes de Docker sin limpiar.
Siguiente: 02-bash-scripting.md — automatizar todo esto en scripts que no exploten.