Skip to content

🎯 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. bash es el estándar de facto en servidores.
  • Comandos = programas de verdad que viven en el disco (/usr/bin/ls es 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?

bash
echo $SHELL          # la shell configurada para tu usuario → /bin/bash o /bin/zsh
bash --version       # versión de bash

⚠️ macOS usa zsh por defecto desde 2019, y su bash es 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 con brew 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:

bash
ls        -la      /var/log

        └── ARGUMENTO: sobre qué actúa
          └─────────── OPCIONES / FLAGS: cómo actúa
└────────────────────── COMANDO: qué programa ejecutar

Las opciones vienen en dos sabores:

FormaEjemploNotas
Corta-lUna letra. Se pueden agrupar: -l -a -h = -lah
Larga--allMás legible. En scripts, usa siempre la larga
Con valor-p 8080 o --port=8080El 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:

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

🧠 man es un superpoder infravalorado. Dentro de man: / busca, n va a la siguiente coincidencia, q sale. man 5 crontab te 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/Nginx

Las cuatro carpetas que de verdad vas a pisar como backend:

CarpetaPara quéEjemplo real
/etcConfigurar servicios/etc/nginx/sites-available/miapp
/var/logInvestigar fallos/var/log/nginx/error.log
/home/usuarioTu código y tus claves~/.ssh/, ~/apps/miapp
/tmpBasura de usar y tirardescargas temporales de un script

Rutas absolutas vs relativas

bash
/var/log/nginx/error.log     # ABSOLUTA: empieza en / → siempre significa lo mismo
../logs/error.log            # RELATIVA: depende de dónde estés parado

Atajos que debes tener en los dedos:

SímboloSignifica
.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.yml no existirá allí. Los scripts usan rutas absolutas o calculan la suya:

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

bash
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/lo y 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

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

⚠️⚠️ rm NO tiene papelera. Lo que borras, se fue. Tres reglas que te ahorrarán un incidente:

  1. Nunca escribas rm -rf con una variable sin comprobar: si $DIR está vacía, rm -rf $DIR/ se convierte en rm -rf /. Escribe rm -rf "${DIR:?variable vacía}" — el :? aborta si está vacía.
  2. Antes de borrar con comodines, lista primero: cambia el rm por ls y mira qué sale. ls *.log → si la lista es la correcta, ahora sí rm *.log.
  3. 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

bash
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 -f es el comando que más vas a usar en tu vida profesional. Despliegas, ejecutas tail -f sobre el log, provocas la petición y ves el error aparecer en tiempo real. Se corta con Ctrl+C. Si el archivo rota (se renombra), usa tail -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 enlace

Cada bloque de tres son los mismos tres permisos:

LetraValorEn un ARCHIVOEn un DIRECTORIO
r4Leer el contenidoListar lo que hay dentro (ls)
w2Modificar el contenidoCrear/borrar archivos dentro
x1Ejecutarlo como programaEntrar en él (cd)

🧠 La trampa de x en directorios. En un directorio, x no es "ejecutar": es atravesar. Un directorio con r pero sin x te deja ver los nombres pero no abrir nada dentro. Uno con x pero sin r te deja abrir archivos si sabes el nombre exacto, pero no listarlos. Por eso los directorios casi siempre son 755 y los archivos 644.

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)
bash
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 777 es 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/www significa 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 sea 600, precisamente para protegerte.

Casos reales que te vas a encontrar

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

sudo: pedir permisos de administrador

bash
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 -rf mal escrito como usuario normal rompe tu carpeta; como root rompe el servidor. sudo existe 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).

bash
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

bash
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 kill y kill -9 importa 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 kill primero. Solo kill -9 si tras 30 segundos sigue vivo. Docker hace exactamente esto: docker stop envía SIGTERM, espera 10s y luego SIGKILL.

Trabajos en primer y segundo plano

bash
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

⚠️ & y nohup no 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

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

El flujo mental cuando "no funciona":

mermaid
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 -->|| 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 -->|| F{"¿Responde en local?<br/>curl localhost:8080"}
    F -->|No| G["El fallo está DENTRO de la app"]
    F -->|| H{"¿Responde desde fuera?"}
    H -->|No| I["Firewall o proxy:<br/>ufw status · config de Nginx"]
    H -->|| 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 en 0.0.0.0. En ss -tulpn lo ves en la columna de dirección: 127.0.0.1:8080 es privado; 0.0.0.0:8080 o *:8080 acepta 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.

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

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

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

Y a partir de ahí: ssh produccion. También funciona con scp produccion:... y con Git.

Copiar archivos entre máquinas

bash
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

💡 rsync sobre scp siempre que sea una carpeta. Si se corta la transferencia, rsync continúa donde iba; scp empieza de cero. Y -z comprime 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.

bash
ssh -L 5433:localhost:5432 produccion
#      │    │         │
#      │    │         └── puerto en el SERVIDOR (Postgres remoto)
#      │    └──────────── "localhost" visto DESDE el servidor
#      └───────────────── puerto en TU máquina

Ahora 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

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

systemd gestiona los servicios (programas que corren siempre):

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

💡 reload vs restart: restart mata el proceso y lo levanta (hay unos segundos de caída). reload le dice que relea su configuración sin soltar las conexiones abiertas. Para cambios de config de Nginx, siempre reload — y antes, sudo nginx -t para 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"

bash
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 -h es siempre el primer comando de cualquier investigación. El culpable habitual: /var/log con logs sin rotar, o imágenes viejas de Docker (docker system prune -a libera gigas).


T1.11 · Atajos de teclado que te hacen 3× más rápido

AtajoQué hace
TabAutocompletar (¡úsalo siempre!)
Ctrl+CMatar el comando actual
Ctrl+DFin de entrada / cerrar sesión (como exit)
Ctrl+LLimpiar pantalla (igual que clear)
Ctrl+R📌 Buscar en el historial — escribe parte del comando
Ctrl+A / Ctrl+EIr al inicio / final de la línea
Ctrl+WBorrar la palabra anterior
Ctrl+UBorrar toda la línea
Alt+.Insertar el último argumento del comando anterior
/ Navegar el historial

💡 Ctrl+R es el atajo que separa al novato del veterano. Pulsa Ctrl+R, escribe docker y va apareciendo el último comando con docker que usaste. Pulsa Ctrl+R otra vez para ir hacia atrás. Enter ejecuta, flecha derecha lo edita. Nunca más vuelvas a reescribir un comando largo.

bash
history | grep docker        # ver todos los comandos con "docker"
!!                           # repetir el último comando
!504                         # repetir el comando nº 504 del historial

T1.12 · Personalizar tu entorno

Tu shell lee un archivo de configuración al arrancar: ~/.bashrc (Bash) o ~/.zshrc (Zsh).

bash
# ~/.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"; }
bash
source ~/.bashrc      # aplicar los cambios sin cerrar la terminal

🧠 PATH explicado: cuando escribes git, la shell busca un archivo llamado git en 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 con echo $PATH y which git.


T1.13 · Los cinco errores más comunes (y qué significan de verdad)

MensajeQué pasa realmenteCómo arreglarlo
command not foundNo está instalado o no está en $PATHwhich cmd, echo $PATH, instalar
Permission deniedTu usuario no tiene el permiso, o falta xls -l, chmod +x, ¿necesita sudo?
No such file or directoryRuta mal escrita, o estás en otra carpetapwd, ls, usa Tab para autocompletar
Address already in useOtro proceso ya ocupa ese puertosudo ss -tulpn | grep :8080kill PID
No space left on deviceDisco llenodf -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

  1. Crea esta estructura con un solo comando (pista: mkdir -p acepta llaves {}):
    ~/practica-t1/
    ├── app/
    ├── config/
    └── logs/
  2. Crea config/app.conf con el texto puerto=8080 (usa echo y redirección >).
  3. Dale a config/app.conf permisos 600 y comprueba con ls -l que pone -rw-------.
  4. Explica en un comentario por qué un archivo de configuración con credenciales debería ser 600 y no 644.

Parte 2 — Procesos y puertos

  1. 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 &
  2. Averigua su PID de dos formas distintas (una con ps, otra con ss o pgrep).
  3. Comprueba que responde: curl -I localhost:8080. ¿Qué código de estado devuelve?
  4. Ahora intenta levantar otro servidor en el mismo puerto 8080. Anota el error exacto.
  5. Mata el primero con kill (sin -9) y verifica que el puerto quedó libre.

Parte 3 — Diagnóstico

  1. 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."
  2. Genera 1000 líneas de log falso y quédate solo con las que contengan ERROR:
    bash
    for i in $(seq 1 1000); do echo "linea $i $( [ $((i % 7)) -eq 0 ] && echo ERROR || echo INFO )"; done > logs/app.log
    Cuenta cuántas son (grep -c), y muestra las 5 últimas.

Parte 4 — Tu entorno

  1. Añade a tu ~/.bashrc (o ~/.zshrc) tres alias que de verdad vayas a usar y una función puerto() 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 comando mkdir; la shell la convierte en tres rutas antes de ejecutar nada.
  • Punto 4: con 644 cualquier 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. 600 limita la lectura al dueño.
  • Punto 6: ps aux | grep http.server da el PID en la segunda columna; sudo ss -tulpn | grep :8080 lo 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.

  1. La app escucha solo en 127.0.0.1 en vez de 0.0.0.0. Compruébalo con ss -tulpn: si la dirección es 127.0.0.1:8080, solo acepta conexiones locales por diseño.
  2. 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).
  3. El proxy inverso no está bien configurado: Nginx recibe en el 80/443 pero no reenvía al 8080, o apunta a un proxy_pass equivocado.

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.