Por qué Linux domina el mundo de los servidores
Entra en cualquier centro de datos, abre cualquier consola de nube o docker exec en cualquier contenedor y, casi con total seguridad, estarás sobre un kernel de Linux. Impulsa la mayoría de los servidores web, cada teléfono Android, la mayoría de los routers y la gran mayoría de las instancias en la nube. Las razones son antiguas y sólidas: es gratuito, se ejecuta en casi cualquier hardware, es lo suficientemente estable como para permanecer activo durante años y es automatizable mediante scripts de principio a fin.
Para un desarrollador, el punto práctico es más específico. En el momento en que despliegas, dejas de ser una persona que edita archivos en un editor para convertirte en alguien que escribe comandos en una shell, a menudo a través de SSH, en una máquina que no puedes ver. Las habilidades de esta guía son las que hacen que esa máquina sea comprensible en lugar de intimidante.
No necesitas convertirte en un administrador de sistemas. Solo necesitas la fluidez suficiente para moverte, leer logs, corregir permisos, reiniciar un servicio y entender qué proceso está escuchando en un puerto.
WSL y macOS: ya sabes más de lo que crees
Si usas macOS, ya estás en un sistema Unix. La terminal, la estructura del sistema de archivos y la mayoría de los comandos — ls, cd, grep, chmod, ssh — funcionan de la misma manera. Las diferencias radican principalmente en el gestor de paquetes y en algunos flags de BSD frente a GNU.
En Windows, WSL 2 te proporciona un kernel de Linux real dentro de una máquina virtual ligera. wsl --install te permite instalar Ubuntu con una shell, y tus archivos son accesibles desde ambos lados. Este es el camino recomendado porque es el mismo entorno en el que se ejecutará tu código.
Incluso si nunca instalas Linux directamente, el modelo mental se transfiere completamente a los contenedores, que no son más que procesos de userland de Linux con un disfraz muy convincente.
El sistema de archivos es un único árbol
A diferencia de Windows, donde las unidades son raíces separadas, Linux tiene un único árbol con raíz en /. Cada disco, recurso compartido de red y dispositivo se monta en algún lugar dentro de él. Un puñado de directorios tienen significados convencionales, y conocerlos significa que siempre sabrás dónde buscar.
/ the root of everything
├── bin essential user binaries (ls, cp, bash)
├── etc system-wide configuration files
├── home per-user home directories
├── opt optional third-party software
├── proc kernel and process state, as files
├── root the root user's home
├── tmp temporary files, cleared on reboot
├── usr installed programs and libraries
│ ├── bin local binaries
│ └── lib shared libraries
└── var variable data: logs, caches, databases
├── log system and application logs
└── www web content on Debian systems
Los dos que más visitarás son /etc para la configuración y /var/log para los logs. Cuando algo va mal, esos son los primeros lugares donde mirar. Cuando instalas una aplicación por tu cuenta, /opt o /usr/local evita que interfiera con el gestor de paquetes.
Las rutas son absolutas (comienzan con /, como /etc/hosts) o relativas (se resuelven desde tu directorio actual). ~ es un acceso directo a tu directorio personal, . es el directorio actual y .. es su directorio padre. Estos tres símbolos aparecen en casi cada comando que escribas.
Todo es un archivo
La idea más profunda de Unix es que casi todo se representa como un archivo. Un disco duro es /dev/sda. Una terminal es /dev/pts/0. La información de los procesos en ejecución reside en /proc/<pid>/. Los números aleatorios provienen de /dev/urandom. Un Unix domain socket también es un archivo.
Esta uniformidad es lo que hace que la shell sea tan potente. Los mismos cat, grep, less y operadores de redirección funcionan en un archivo de log, en el mapa de memoria de un proceso y en un dispositivo, porque todos son simplemente flujos de bytes detrás de una ruta. Cuando entiendes esto, /proc y /sys dejan de parecer magia y se vuelven inspeccionables.
cat /proc/cpuinfo | grep 'model name' | head -1
ls -l /dev/null # the bit bucket
Navegación y los comandos de archivos que usarás a diario
La shell es un bucle: lee una línea, la expande, busca el programa y lo ejecuta. Seis comandos cubren la mayor parte de la navegación.
pwd # print working directory
ls -lah # list, long format, human sizes, hidden files
cd /var/log # change directory
cd - # back to the previous directory
cd ~ # home
tree -L 2 # a readable tree, two levels deep
Crear, copiar, mover y eliminar archivos requiere un vocabulario igual de reducido. mkdir -p crea los directorios padres según sea necesario. cp -r copia directorios de forma recursiva. mv renombra dentro de un sistema de archivos y mueve entre ellos. rm -rf elimina un árbol completo, razón por la cual merece respeto: en la línea de comandos no existe la papelera de reciclaje.
mkdir -p app/{src,test,docs}
cp -r app app.bak
mv notes.txt docs/notes.txt
rm -rf build/
Dos hábitos te mantendrán a salvo. Usa ls o find para confirmar qué coincide con un glob antes de ejecutar rm con él, y prefiere mv hacia un directorio de respaldo antes de eliminar algo de lo que no estés seguro. El historial de la shell no es una red de seguridad una vez que presionas Enter.
Leer archivos sin abrir un editor
Rara vez necesitas un editor completo para inspeccionar algo en un servidor. Estas herramientas imprimen solo la parte que necesitas.
cat /etc/os-release # print the whole file
less /var/log/syslog # page through, / to search, q to quit
head -n 20 access.log # first 20 lines
tail -n 50 access.log # last 50 lines
tail -f app.log # follow a live log
wc -l access.log # count lines
tail -f es el comando más útil durante un incidente: transmite las líneas nuevas a medida que se escriben. Presiona Ctrl+C para detenerlo. less es más rápido que un editor y nunca modifica el archivo accidentalmente, lo cual es fundamental en producción.
Búsqueda con find y grep
find recorre el sistema de archivos y busca coincidencias en los metadatos; grep busca dentro del contenido de los archivos. Juntos, responden a la mayoría de las preguntas de “¿dónde está esto?”.
find /etc -name '*.conf' # by name
find . -type d -name node_modules -prune -o -type f -name '*.ts' -print
find /var/log -type f -mtime -1 # modified in the last day
find . -type f -size +50M # larger than 50 MB
find . -type f -name '*.tmp' -delete # delete matches
grep -RIn 'TODO' src/ # recursive, line numbers, ignore binary
grep -c 'ERROR' app.log # count matches
grep -E 'timeout|refused' app.log # extended regex
grep -v '^#' config.ini # invert: drop comments
Que grep no devuelva nada es un éxito, no un error, pero sale con el estado 1. Esto puede causar problemas en scripts bajo set -e; añade || true cuando un resultado vacío sea aceptable.
Permisos: rwx y la notación octal
Cada archivo tiene un propietario, un grupo y tres bits de permisos para cada uno: lectura (r), escritura (w) y ejecución (x). ls -l los muestra como una cadena de diez caracteres.
-rwxr-xr-- 1 deploy www-data 512 Sep 16 10:00 deploy.sh
El sistema octal es la forma abreviada. Cada triplete es la suma de 4 (lectura), 2 (escritura) y 1 (ejecución), por lo que rwx es 7, r-x es 5 y r-- es 4. Esto hace que 755 signifique rwxr-xr-x y 644 signifique rw-r--r--.
chmod 755 deploy.sh # scripts and directories
chmod 644 index.html # regular files
chmod 600 .env # secrets only the owner can read
chmod u+x,g-w,o-rwx script # symbolic form, easier to review
Los directorios necesitan el bit de ejecución para poder acceder a ellos, razón por la cual eliminar x de un directorio oculta su contenido incluso si el permiso de lectura sigue activo. Esta es una fuente sutil de errores de “permission denied” cuando el permiso de lectura parece estar correcto.
Propiedad, grupos y umask
chown cambia el propietario y el grupo; chgrp cambia solo el grupo. Utiliza el prefijo sudo ya que solo root puede transferir la propiedad de los archivos.
sudo chown deploy:www-data /var/www/app
sudo chgrp -R www-data /var/www/app/uploads
Los archivos nuevos obtienen sus permisos del umask, una máscara que se resta del valor predeterminado. Un umask de 022 resulta en 644 para archivos y 755 para directorios; 027 resulta en 640 y 750. Configúralo en el perfil de tu shell o en el archivo de unidad de un servicio cuando se requiera un valor predeterminado más estricto.
umask # show current, e.g. 0022
umask 027 # stricter for this shell
sudo y el hábito del privilegio mínimo
sudo ejecuta un comando como otro usuario, root por defecto, tras verificar /etc/sudoers. Es deliberadamente restrictivo: otorga los comandos específicos que una persona o servicio necesita en lugar de entregar una shell de root. Es preferible usar sudo -u www-data <cmd> que editar archivos como root y dejar que root sea el propietario de los mismos.
sudo systemctl restart my-api
sudo -u postgres psql -c '\l'
sudo -l # what am I allowed to run?
Una regla general: root es el propietario de la configuración del sistema, un usuario de servicio dedicado es el propietario de los archivos de la aplicación, y tu app nunca se ejecuta como root. Si un compromiso de tu app le diera a un atacante acceso root en el host, los permisos no te están sirviendo de nada.
Usuarios y grupos
Los usuarios se identifican mediante un nombre y un uid numérico; los grupos, mediante un nombre y un gid. El mapeo se encuentra en /etc/passwd y /etc/group, mientras que las contraseñas hasheadas residen en /etc/shadow, siendo legibles únicamente por root.
whoami # current user
id # uid, gid and groups
groups deploy # groups a user belongs to
sudo adduser appuser # create a user (Debian/Ubuntu)
sudo usermod -aG docker appuser # add to a group
sudo userdel -r appuser # remove the user and home
Las cuentas de servicio generalmente no tienen contraseña ni shell de inicio de sesión. Esto es intencional: existen para poseer archivos y ejecutar un único proceso, no para iniciar sesión en ellas.
Procesos: ps, top y señales
Cada programa en ejecución es un proceso con un pid, un propietario y un padre. ps toma una captura instantánea; top y htop se actualizan en tiempo real.
ps aux | grep node # find a process
ps -eo pid,ppid,user,%cpu,%mem,cmd --sort=-%cpu | head
top -o %MEM # interactive, sort by memory
pgrep -af node # pids and full command lines
Los procesos se comunican a través de señales. SIGTERM (15) solicita que un proceso finalice y es la opción predeterminada de kill; SIGINT (2) es Ctrl+C; SIGHUP (1) tradicionalmente significa recargar; SIGKILL (9) no puede ser interceptada y debe usarse como último recurso.
kill 1234 # SIGTERM, graceful
kill -HUP 1234 # reload config
kill -9 1234 # force, no cleanup
pkill -f 'node server.js'
Envía SIGTERM primero y dale al proceso unos segundos para cerrar conexiones y vaciar el estado. Escala a -9 solo cuando el proceso esté realmente bloqueado, ya que omite cualquier ruta de limpieza que tenga tu aplicación.
Trabajos en segundo plano, & y nohup
Añadir & ejecuta un comando en segundo plano; jobs, fg y bg permiten gestionarlo desde la shell actual. Sin embargo, un trabajo en segundo plano seguirá finalizando al cerrar la terminal, a menos que lo desvincules de la señal de hangup mediante nohup o un multiplexor de terminal.
long-task & # background in this shell
jobs -l # list background jobs
fg %1 # bring job 1 to the foreground
nohup ./worker.sh > worker.log 2>&1 &
disown # detach from the shell's job table
Para cualquier proceso de larga duración, utiliza tmux o screen. Estos mantienen la sesión activa a pesar de las desconexiones, lo cual es invaluable cuando un despliegue tarda más que tu conexión SSH.
tmux new -s deploy
# Ctrl+B, then D to detach
tmux attach -t deploy
Servicios de systemd con systemctl
En las distribuciones modernas, systemd es el sistema de inicio que arranca los servicios al bootear y los supervisa. Defines un servicio en un archivo de unidad bajo /etc/systemd/system/.
# /etc/systemd/system/my-api.service
[Unit]
Description=My API
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/my-api
Environment=NODE_ENV=production
EnvironmentFile=/opt/my-api/.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
Después de crear o editar una unidad, recarga el gestor y habilita el servicio para que se inicie al arrancar el sistema.
sudo systemctl daemon-reload
sudo systemctl enable --now my-api
systemctl status my-api --no-pager
sudo systemctl restart my-api
sudo systemctl reload my-api # if the service supports it
Restart=on-failure es lo que convierte un proceso caído en un servicio capaz de autorecuperarse. EnvironmentFile mantiene los secretos fuera de la unidad, la cual suele ser legible para cualquier usuario. Usa Type=notify solo si el programa realmente soporta el protocolo de notificación; simple es la opción segura por defecto.
Logs con journalctl
systemd captura el stdout y stderr de un servicio en el journal. journalctl lo consulta, y lo usarás constantemente.
journalctl -u my-api # all logs for one unit
journalctl -u my-api -n 100 # last 100 lines
journalctl -u my-api -f # follow live
journalctl -u my-api --since today
journalctl -u my-api --since '10 min ago' -p err
journalctl -b -p warning # this boot, warnings and worse
journalctl --disk-usage
sudo journalctl --vacuum-time=7d # keep a week
El flag -p filtra por prioridad, desde emerg hasta debug. Seguir una unidad con -f mientras haces una petición a un endpoint es la forma más rápida de entender el motivo de un error 500.
Variables de entorno y PATH
Las variables de entorno son cadenas de clave-valor que un proceso hereda de su padre. PATH es la más importante: enumera los directorios que el shell busca para encontrar el nombre de un comando, en orden.
echo "$PATH" | tr ':' '\n'
export NODE_ENV=production
export PATH="$HOME/.local/bin:$PATH"
printenv DATABASE_URL
Debido a que un servicio no hereda tu shell interactivo, define las variables en su archivo de unidad o en un EnvironmentFile. Cuando una variable funciona en tu terminal pero no bajo systemd, casi siempre se debe a esto. Para persistir tu propia configuración, añade las líneas export a ~/.bashrc o ~/.profile.
Gestores de paquetes: apt, dnf, apk
Cada familia de distribuciones tiene su propio gestor de paquetes, pero los verbos son similares: actualizar el índice, instalar, actualizar, eliminar y buscar.
# Debian / Ubuntu
sudo apt update && sudo apt upgrade -y
sudo apt install nginx
sudo apt remove --purge nginx
# Fedora / RHEL
sudo dnf install nginx
sudo dnf upgrade --refresh
# Alpine (common in containers)
sudo apk add nginx
sudo apk upgrade
Instala únicamente desde repositorios oficiales siempre que sea posible; estos están firmados y cuentan con parches. Recurre a un PPA o a un repositorio de terceros de forma deliberada, y fija las versiones en las imágenes de producción para que una reconstrucción no cambie silenciosamente lo que se está ejecutando.
Pipes, redireccionamiento y scripts sencillos
Un pipe envía la salida de un programa a la entrada de otro. El redireccionamiento la envía a un archivo o la lee desde uno. Juntos, convierten herramientas pequeñas en pipelines.
ls -l | wc -l # count files
grep ' 500 ' access.log | wc -l # count 500s
cmd > out.txt # stdout to file, overwrite
cmd >> out.txt # append
cmd 2> err.txt # stderr to file
cmd > all.txt 2>&1 # both streams together
cmd < input.txt # read stdin from a file
Cada proceso comienza con tres descriptores de archivo: stdin (0), stdout (1) y stderr (2). 2>&1 significa “envía stderr a donde va stdout”, y el orden es importante: debe ir después del redireccionamiento de stdout.
Envuelve una secuencia en un script y tendrás automatización. Dos líneas al principio hacen que los scripts sean mucho más seguros:
#!/usr/bin/env bash
set -euo pipefail
-e hace que el script termine en el primer comando que falle, -u genera un error si hay una variable no definida, y -o pipefail hace que un pipeline falle si cualquiera de sus etapas falla. Juntos, convierten los fallos parciales silenciosos en errores evidentes, que es exactamente lo que buscas en un script de despliegue.
SSH y claves
SSH es la forma de acceder a una máquina remota. Los inicios de sesión con contraseña funcionan, pero las claves son más seguras y permiten la automatización mediante scripts. Mantienes una clave privada en secreto y colocas la clave pública correspondiente en el servidor.
ssh-keygen -t ed25519 -C "you@laptop" # creates ~/.ssh/id_ed25519(.pub)
ssh-copy-id [email protected] # install the public key
ssh [email protected] # log in
Un alias de host en ~/.ssh/config evita tener que escribir todo el comando y permite fijar opciones útiles:
Host prod
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 30
Luego, ssh prod realiza la conexión. Una vez que confíes en las claves, deshabilita la autenticación por contraseña en /etc/ssh/sshd_config (PasswordAuthentication no) y reinicia sshd. scp y rsync reutilizan las mismas claves para copiar archivos; rsync -avz --delete es la forma habitual de sincronizar un directorio.
rsync -avz --exclude node_modules ./app/ prod:/opt/app/
ssh prod 'sudo systemctl restart my-api'
Disco y memoria: df, du, free
Los servidores rara vez fallan por la CPU; fallan porque un disco se llena o porque se agota la memoria. Estos comandos responden a “¿cuánto queda?” y “¿qué lo está consumiendo?”.
df -h # free space per filesystem
df -i # inode usage — a disk can be full of inodes
du -sh /var/log/* # size per entry
du -h --max-depth=1 / | sort -h
free -h # memory and swap
ls -lhS /var/log | head # biggest log files
Cuando /var está lleno, la causa casi siempre son los logs, la caché de paquetes o una base de datos descontrolada. Realiza la limpieza de forma deliberada — journalctl --vacuum-time, apt clean, rotación de logs — en lugar de borrar archivos que una aplicación necesite.
Networking: ss, curl, dig
ss muestra los sockets y los puertos en escucha, reemplazando al antiguo netstat. Responde a la pregunta “¿hay algo escuchando y quién está conectado?”.
ss -tulpn # TCP, UDP, listening, process names
curl -I https://example.com # response headers only
curl -sS localhost:3000/health
curl -X POST -H 'content-type: application/json' \
-d '{"name":"ada"}' localhost:3000/users
dig example.com +short # DNS resolution
dig @1.1.1.1 example.com MX
curl -I es la forma más rápida de confirmar que un servidor está respondiendo y qué estado devuelve. Si ss no muestra nada en un puerto pero la aplicación indica que se ha iniciado, es posible que esté vinculada a 127.0.0.1 en lugar de 0.0.0.0, lo cual es invisible desde fuera del host.
Programación con cron
cron ejecuta comandos según un horario programado. Cada usuario tiene un crontab con cinco campos de tiempo seguidos del comando.
# ┌ min (0-59)
# │ ┌ hour (0-23)
# │ │ ┌ day of month (1-31)
# │ │ │ ┌ month (1-12)
# │ │ │ │ ┌ day of week (0-6, Sun=0)
# * * * * * command
crontab -e # edit your crontab
crontab -l # list it
0 3 * * * /opt/app/backup.sh >> /var/log/backup.log 2>&1
*/5 * * * * curl -fsS localhost:3000/health > /dev/null
0 7 * * 1 /opt/app/weekly-report.sh
El entorno de cron es mínimo: tiene un PATH diferente, no posee un perfil de shell ni variables interactivas. Utiliza rutas absolutas, redirige la salida a un log para que los fallos sean visibles y recuerda que cron no reintenta una ejecución fallida. Para cualquier cosa más compleja, programa un timer de systemd o encola un job en su lugar.
Mejores prácticas
- Usa rutas absolutas en scripts y archivos de unidad; nunca dependas del entorno interactivo.
- Coloca
set -euo pipefailal principio de cada shell script que no sea trivial. - Aplica el principio de mínimo privilegio: usuarios de servicio dedicados,
sudopara comandos específicos y ninguna aplicación ejecutándose como root. - Prefiere
SIGTERMy permite que los servicios se cierren correctamente antes de recurrir akill -9. - Configura
Restart=on-failurey leejournalctl -u <unit>al depurar. - Mantén la configuración en el control de versiones y cambia una sola cosa a la vez en un host en producción.
- Rota los logs y monitorea
df -hydf -i; los discos se llenan silenciosamente. - Usa llaves SSH, no contraseñas, y mantén las llaves privadas fuera de los servidores.
- Aprende
find,grep,awky pipes; sustituyen a muchas herramientas de un solo uso. - Prueba una configuración antes de reiniciar el servicio que la utiliza.
Errores comunes
- Ejecutar aplicaciones como root y convertir cualquier bug en un compromiso total del sistema.
- Usar
chmod -R 777para “arreglar” los permisos y eliminar todas las protecciones entre usuarios. - Borrar archivos usando un comodín sin revisar y sin tener un backup.
- Enviar
kill -9primero y corromper el estado que un apagado controlado (graceful shutdown) habría vaciado. - Configurar variables de entorno en tu shell y esperar que systemd las reconozca.
- Asumir que un servicio está caído cuando solo está vinculado a
127.0.0.1. - Olvidar
2>&1en las entradas de cron y perder el error que explica por qué falló una tarea. - Editar la configuración directamente en un servidor sin control de versiones y sin forma de revertir los cambios.
- Ignorar el agotamiento de inodos porque
df -htodavía muestra espacio libre. - Dejar la autenticación por contraseña habilitada en un puerto SSH expuesto a internet.
Próximos pasos
Linux es la base de todo lo que despliegues. Una vez que sepas moverte por un servidor, leer sus logs y reiniciar sus servicios, la guía de Nginx te mostrará el servidor web que configurarás frente a tu aplicación, y Docker & Deployment convertirá estas primitivas en imágenes aisladas y repetibles. Para automatizar estos mismos comandos en cada push, lee sobre CI/CD, y para comprender el proceso que realmente ejecuta tu servicio, vuelve a consultar Node.js Basics.