Módulo 5: Máquinas remotas, redes y scripting

5. Mover archivos y sesiones que sobreviven: scp, rsync y tmux

Descripción

Al final de esta lección vas a poder mover archivos hacia y desde una máquina remota con la herramienta correcta para cada caso — una copia puntual con scp, una sincronización real con rsync que solo mueve lo que cambió — y vas a poder lanzar un proceso que dura horas en un servidor remoto y dejarlo corriendo aunque cierres la laptop, apagues el wifi o se caiga tu conexión a internet.

Esto no es un detalle de comodidad. Es, probablemente, el primer susto operativo real que vas a tener trabajando con una máquina remota: conectas por SSH (ya sabes hacerlo desde la lección anterior), lanzas un entrenamiento, una migración de base de datos o un rsync de varios gigabytes, cierras la laptop para ir a almorzar, y cuando vuelves el proceso está muerto a la mitad — sin ningún mensaje de error que lo explique, porque desde el punto de vista del proceso, simplemente le cortaron la electricidad. Casi cualquier persona que trabaja con servidores remotos descubre esto una vez, generalmente perdiendo horas de trabajo, y no lo vuelve a olvidar.

Conexión con el módulo: ya sabes conectarte por SSH con un alias de ~/.ssh/config y autenticación por llave. Esta lección asume esa conexión resuelta y se enfoca en dos preguntas que vienen justo después: ¿cómo mueves archivos entre tu máquina y la remota sin reinventar cat a mano?, y ¿cómo haces que un proceso remoto sobreviva a que te desconectes? La siguiente lección da el paso natural que sigue: convertir los comandos que ya dominas en un archivo ejecutable — tu primer script.


Copiar una vez: scp

Piensa en scp como una fotocopiadora: metes una hoja, sacas una copia idéntica del otro lado, sin preguntar nada. Si vuelves a correr el mismo comando cinco minutos después, vuelve a copiar todo desde cero — no le importa que el archivo de destino ya exista y sea idéntico, ni que solo haya cambiado una línea de un archivo de dos gigabytes. scp (secure copy) usa el mismo canal cifrado que ya conoces de ssh, y su sintaxis es casi literal: donde en cp escribirías origen y destino en tu propia máquina, en scp cualquiera de los dos —o ambos— puede llevar el prefijo usuario@host:.

Nota técnica que vale la pena conocer: desde OpenSSH 9.0 (2022), scp usa por defecto el protocolo SFTP para transferir los archivos, no el protocolo scp original de los años ochenta — un cambio transparente en la sintaxis que usas, pero que corrige varias debilidades de seguridad del protocolo viejo. Si alguna vez ves el flag -O en un script antiguo, es para forzar el protocolo legado contra un servidor que todavía no soporta SFTP; en el trabajo normal de este módulo nunca lo vas a necesitar.

Ejemplo trabajado

Copia un archivo local hacia tu servidor, usando el alias que configuraste en ~/.ssh/config en la lección anterior:

scp report.csv myserver:/home/alex/reports/

Qué esperar:

report.csv                                    100%  350KB   4.1MB/s   00:00

Para traer algo de vuelta, el origen remoto y el destino local simplemente cambian de lugar:

scp myserver:/home/alex/reports/summary.csv ./

Y para una carpeta completa, agrega -r (recursivo) — igual que harías con cp:

scp -r myserver:/home/alex/reports ./local-reports

Qué esperar (una línea de progreso por cada archivo dentro de la carpeta):

sales.csv                                     100%   82KB   3.9MB/s   00:00
inventory.csv                                 100%  128KB   4.0MB/s   00:00

scp es perfecto para esto: "necesito este archivo específico, ahora, una sola vez". Para un proyecto completo que vas a actualizar una y otra vez —un directorio de código, un sitio estático, una carpeta de backups que crece cada día— scp se vuelve lento y torpe, porque siempre vuelve a copiar todo. Ahí es donde entra rsync.


Sincronizar en serio: rsync

Si scp es una fotocopiadora, rsync es un contador reconciliando dos libros de cuentas: antes de mover un solo byte, compara qué existe en el destino contra qué existe en el origen —nombre por nombre, y dentro de cada archivo, bloque por bloque— y solo transmite la diferencia. Un archivo de dos gigabytes donde cambiaron cuatro líneas se sincroniza en segundos, no porque rsync sea "más rápido" en abstracto, sino porque literalmente mueve menos datos. Esa es la razón por la que rsync (remote sync) es la herramienta real de trabajo para cualquier cosa que no sea una copia de una sola vez: código, backups, directorios de datos, sitios completos.

Las banderas que vas a usar en casi cada invocación real son -avz: -a (archive) activa recursividad y preserva permisos, dueño, grupo, timestamps y enlaces simbólicos de un solo golpe — es el modo "cópialo tal como es, no una versión genérica"; -v (verbose) te muestra qué archivos se están transfiriendo; -z (compress) comprime los datos en tránsito, lo cual ayuda mucho en conexiones lentas y casi no cuesta nada en una red rápida.

Ejemplo trabajado

Antes de sincronizar cualquier cosa de verdad, corre siempre --dry-run (o su forma corta -n) primero — te muestra exactamente qué haría rsync sin mover un solo byte:

rsync -avz --dry-run project/ myserver:/var/www/project/

Qué esperar (una simulación completa, ninguna transferencia real todavía):

sending incremental file list
./
src/app.py
src/config.py
static/style.css

sent 1,204 bytes  received 84 bytes  2,576.00 bytes/sec
total size is 48,213  speedup is 37.44 (DRY RUN)

La línea (DRY RUN) al final es tu confirmación de que nada se movió. Si la lista de archivos es la que esperabas, quitas --dry-run y corres exactamente el mismo comando para que sí ocurra:

rsync -avz project/ myserver:/var/www/project/

Ahora, la parte que cambia todo el resultado sin cambiar una sola bandera: la barra final en la ruta de origen. Compara estos dos comandos, idénticos salvo por una barra:

rsync -avz project  myserver:/var/www/
rsync -avz project/ myserver:/var/www/

Qué esperar en cada caso, si /var/www/ estaba vacío antes de correr el comando:

# Sin barra final en el origen ("project"):
# rsync copia la CARPETA, con su nombre incluido
/var/www/project/src/app.py
/var/www/project/static/style.css

# Con barra final en el origen ("project/"):
# rsync copia el CONTENIDO de la carpeta, sin el nombre de la carpeta
/var/www/src/app.py
/var/www/static/style.css

La regla es esa, literal: una barra final en el origen le dice a rsync "copia lo que está adentro de esta carpeta"; sin ella, le dices "copia esta carpeta completa, tal como es, con nombre y todo". La barra final en el destino no cambia nada — solo la del origen importa.

Dos banderas más que vas a necesitar casi de inmediato. --exclude deja fuera de la sincronización lo que le indiques por patrón — típicamente carpetas pesadas y regenerables que no deberían viajar a ningún lado:

rsync -avz --exclude='.git' --exclude='node_modules' project/ myserver:/var/www/project/

Y --delete hace que el destino termine siendo un espejo exacto del origen: si un archivo existía en el destino pero ya no está en el origen, rsync lo borra. Es exactamente lo que quieres para un backup que debe reflejar el estado actual, y exactamente lo que no quieres si te equivocaste de dirección en el comando. --delete combinado con rutas invertidas por accidente —origen y destino cambiados de lugar— puede borrar en segundos una carpeta que te tomó meses construir, sin preguntar nada. Por eso la secuencia correcta siempre es: primero --dry-run con --delete incluido, leer con calma qué archivos aparecen bajo deleting, y solo después correr el comando real:

rsync -avz --delete --dry-run backups/ myserver:/data/current/

Qué esperar (los archivos que se borrarían aparecen marcados, sin que se haya borrado nada todavía):

deleting old-report-2024.csv
deleting cache/tmp_4471.dat
sent 412 bytes  received 96 bytes  1,016.00 bytes/sec
total size is 91,204  speedup is 179.53 (DRY RUN)

Si esa lista tiene sentido —son archivos que de verdad ya no deberían estar ahí— quitas --dry-run y corres el mismo comando para que se ejecute de verdad.


El corte que nadie ve venir: por qué tu proceso remoto muere cuando cierras la laptop

Imagina tu sesión de SSH como una llamada telefónica: mientras dura la llamada, todo lo que dices llega al otro lado. El proceso que lanzaste en el servidor remoto —un script largo, una compilación, ese rsync de varios gigabytes— nació dentro de esa llamada, atado a ella como una extensión del mismo teléfono. Cuando cuelgas —cierras la terminal, cierras la laptop, se cae el wifi— el sistema operativo remoto envía a ese proceso una señal llamada SIGHUP ("hang up", literalmente "se colgó la llamada"), y por defecto casi cualquier programa que reciba esa señal termina de inmediato. No es un error del proceso ni de la red: es el comportamiento esperado del sistema. El proceso nunca fue diseñado para sobrevivir sin la llamada que lo sostenía.

Hay dos formas distintas de resolver esto, y elegir entre ellas es la habilidad real de esta sección — no memorizar los comandos, sino saber cuál corresponde a cada situación.

tmux crea una sala que existe independientemente de la llamada. En vez de que tu proceso viva dentro de tu sesión SSH, vive dentro de una sesión de tmux que corre en el servidor remoto, y tu sesión SSH solo se conecta a esa sala como quien entra a mirar una pantalla — puedes salir de la sala (desconectarte, o en la jerga de tmux, detach) y la sala sigue ahí, con todo corriendo adentro, exista o no tu conexión SSH. Vuelves cuando quieras, te reconectas a la misma sala (attach), y ves la pantalla exactamente como la dejaste, con el proceso corriendo o ya terminado.

nohup (más &) le dice al proceso "ignora la señal de colgado". Es más liviano que tmux porque no crea ninguna sala ni pantalla persistente: solo le indica al proceso que, si le llega SIGHUP, la ignore y siga corriendo. El costo es que no tienes forma de "volver a mirar" ese proceso en vivo como si fuera una terminal — solo puedes revisar su archivo de salida con algo como tail -f, o comprobar con ps que sigue vivo.

Ejemplo trabajado

Crea una sesión nueva de tmux con un nombre —dale siempre un nombre, no dejes que use el número por defecto, porque un nombre lo hace reconocible semanas después:

tmux new -s deploy

Dentro de esa sesión —que para el sistema es una terminal normal— lanzas el proceso largo como lo harías siempre:

rsync -avz big-dataset/ myserver:/data/archive/

Mientras corre, te desconectas sin matar nada: presiona Ctrl-b y suelta, luego presiona d (el prefijo de tmux es Ctrl-b; casi todo comando de tmux empieza soltando esas dos teclas y presionando una tercera). Vuelves a tu shell normal:

[detached (from session deploy)]

Cierra la laptop si quieres — la sesión sigue viva en el servidor. Cuando vuelvas a conectarte por SSH, primero confirma qué sesiones existen:

tmux ls

Qué esperar:

deploy: 1 windows (created Mon Jul 20 14:02:31 2026)

Y te reconectas a esa sesión específica por su nombre:

tmux attach -t deploy

Vuelves a ver exactamente la misma pantalla, con el rsync ya terminado o todavía corriendo. Cuando el trabajo esté hecho y ya no necesites la sesión, la cierras explícitamente (no basta con salir del shell de adentro; eso solo cierra la ventana, no necesariamente la sesión si tiene más de una):

tmux kill-session -t deploy

Si tu servidor no tiene tmux instalado y no quieres instalarlo para una tarea puntual, nohup junto con & (que ya conoces del módulo de procesos) resuelve el mismo problema sin ninguna sesión persistente:

nohup rsync -avz big-dataset/ myserver:/data/archive/ &

Qué esperar (el número entre corchetes es el PID del proceso en segundo plano):

[1] 48213
nohup: ignoring input and appending output to 'nohup.out'

Ese archivo nohup.out se crea en el directorio donde estabas parado, con toda la salida —normal y de error— mezclada adentro. Para no depender de ese nombre genérico ni tener que buscarlo después, redirige tú mismo la salida a un archivo con nombre propio; en ese caso nohup ni siquiera necesita crear nada, porque ya no hay salida sin destino:

nohup rsync -avz big-dataset/ myserver:/data/archive/ > sync.log 2>&1 &

Qué esperar: ninguna línea de aviso — la salida completa, estándar y de error (gracias a 2>&1), queda directamente en sync.log, no en nohup.out.

Puedes cerrar la sesión SSH con tranquilidad; el proceso sigue corriendo en el servidor, ajeno a la señal de colgado. Para ver su progreso más tarde, sin nada que "reabrir":

tail -f sync.log

screen existe como alternativa más antigua a tmux — resuelve el mismo problema, sesiones que sobreviven a la desconexión, con una sintaxis distinta (screen -S nombre para crear, Ctrl-a d para desconectar, screen -r nombre para reconectar) y viene preinstalado en más sistemas viejos. Si ya sabes tmux, no hay razón práctica para aprender screen también; si te conectas a un servidor donde tmux no existe y no puedes instalarlo, screen suele estar ahí como respaldo.

El criterio para elegir, en una frase: si necesitas ver el proceso en vivo, interactuar con él, o vas a organizar varias cosas a la vez en pantallas separadas, usa tmux; si es un comando de una sola vez, no necesitas verlo correr, y solo quieres revisar el resultado al final en un archivo de log, nohup ... & es más rápido y no depende de que tmux esté instalado.


Errores comunes

"Cerré la laptop cinco minutos, y el rsync de dos horas quedó a la mitad, sin ningún error." Lo que pasa: el proceso corría directamente dentro de tu sesión SSH, sin tmux ni nohup de por medio, así que cuando la conexión se cortó, el sistema le mandó SIGHUP y el proceso murió con ella. Por qué ocurre: por defecto, un proceso está atado al ciclo de vida de la sesión que lo lanzó; sobrevivir a la desconexión es la excepción, no la regla, y hay que pedirlo explícitamente con tmux o nohup. Cómo detectarlo: revisa con ps aux | grep rsync en el servidor apenas te reconectes — si el proceso ya no aparece y el archivo de destino quedó incompleto, esa es la firma. Cómo corregirlo: lanza cualquier proceso que dure más de un par de minutos dentro de una sesión de tmux, o antepón nohup — nunca directo en la sesión SSH sin protección.

"Corrí rsync -avz proyecto myserver:/var/www/ y ahora tengo /var/www/proyecto/proyecto/ duplicado." Lo que pasa: el origen no llevaba barra final, así que rsync copió la carpeta completa —con su nombre— dentro de un destino que ya tenía una carpeta con ese mismo nombre de una corrida anterior. Por qué ocurre: la barra final en el origen decide si rsync trata la ruta como "el contenido" o como "la carpeta completa", y es fácil no notar esa diferencia entre dos comandos casi idénticos, sobre todo si copiaste uno de un ejemplo distinto. Cómo detectarlo: antes de correr cualquier rsync hacia un destino que ya tiene contenido, corre primero con --dry-run y lee la lista de rutas que se crearían — si ves el nombre de tu carpeta repetido en la ruta resultante, ahí está la señal. Cómo corregirlo: decide con intención si quieres el contenido (barra final en el origen) o la carpeta completa (sin barra), y si ya quedó duplicado, entra al destino y mueve el contenido un nivel arriba con mv antes de seguir sincronizando.

"Usé --delete para 'limpiar' el destino y ahora faltan archivos que sí necesitaba." Lo que pasa: --delete borró en el destino archivos que ya no estaban en el origen en ese momento —una carpeta local incompleta, un --exclude de más, o simplemente origen y destino invertidos por error de tipeo— y esos archivos "sobrantes" desaparecieron sin previo aviso. Por qué ocurre: --delete no distingue "archivo que ya no debería estar" de "archivo que falta por error tuyo"; para rsync, cualquier cosa que no esté en el origen en ese instante es candidata a borrarse en el destino, sin excepciones. Cómo detectarlo: si corriste --delete sin --dry-run primero, la única forma de notarlo es después del hecho, revisando qué falta — que es exactamente lo que quieres evitar. Cómo corregirlo hacia adelante: --delete nunca se corre por primera vez sin --dry-run delante, sin excepción; lees la lista completa de líneas deleting antes de quitar la bandera, y si el destino es algo que no puedes permitirte perder, mantén además una copia de respaldo aparte del rsync mismo.


Ejercicios

1. Lee la barra final

Tu carpeta local site/ contiene index.html y styles.css. El servidor remoto tiene una carpeta vacía en /var/www/. Corres:

rsync -avz site myserver:/var/www/

¿Qué ruta exacta va a tener index.html en el servidor al terminar? Y si en cambio hubieras corrido rsync -avz site/ myserver:/var/www/, ¿cuál habría sido la ruta?

Ver solución

Sin barra final en el origen (site), rsync copia la carpeta completa dentro del destino: la ruta final es /var/www/site/index.html.

Con barra final en el origen (site/), rsync copia solo el contenido: la ruta final es /var/www/index.html, sin ninguna carpeta site de por medio.

Por qué funciona: la barra final en la ruta de origen le indica a rsync que trate esa ruta como "lo que está adentro", no como "la carpeta en sí" — es la única diferencia entre los dos comandos, y cambia por completo la estructura resultante.

2. Decide si el --dry-run es seguro para correr de verdad

Corriste esto y obtuviste la salida que sigue:

rsync -avz --delete --dry-run current-build/ myserver:/var/www/production/
deleting old-vendor-bundle.js
deleting debug.log
sending incremental file list
src/main.js
src/styles.css

sent 890 bytes  received 210 bytes  2,200.00 bytes/sec
total size is 210,442  speedup is 191.31 (DRY RUN)

¿Correrías este comando quitando --dry-run? Justifica con lo que ves en la salida.

Ver solución

Sí, en principio es seguro: los dos archivos marcados con deleting (old-vendor-bundle.js y debug.log) son exactamente el tipo de archivo que uno esperaría eliminar en una sincronización de producción — un bundle viejo ya reemplazado y un log de depuración que no debería estar en el servidor. Si en cambio la lista de deleting incluyera algo como config/database.yml o una carpeta completa de datos de usuarios, sería una señal clara de detenerse y revisar si el origen y el destino están invertidos, o si falta algo en el directorio local antes de sincronizar.

Por qué funciona: el propósito exacto de --dry-run junto con --delete es darte esta lista para revisar antes de que ocurra algo irreversible — la decisión de correrlo de verdad depende de leer cada línea deleting, no de asumir que está bien.

3. Elige la herramienta correcta

Tienes tres situaciones. Para cada una, decide si usarías tmux o nohup ... &, y en una frase, justifica por qué:

a) Un script de backup de quince minutos que corres una vez al mes, sin necesidad de mirarlo mientras corre. b) Un proceso de entrenamiento de varias horas donde quieres poder revisar el progreso en vivo de vez en cuando, y quizás lanzar un segundo comando en paralelo mientras el primero sigue corriendo. c) Un servidor de pruebas donde ni tmux ni screen están instalados, y no tienes permisos para instalar nada.

Ver solución

a) nohup ... &: es un comando de una sola vez, no necesitas verlo correr en vivo, y no vale la pena crear una sesión persistente para algo que solo revisas al final en un log.

b) tmux: necesitas mirar el proceso en vivo y potencialmente organizar más de una cosa a la vez en pantallas separadas — exactamente el caso donde una sesión persistente con interfaz vale la pena.

c) nohup ... &: sin permisos para instalar nada, no tienes forma de conseguir tmux ni screen; nohup viene con prácticamente cualquier sistema Unix por defecto, sin instalación adicional.

Por qué funciona: el criterio no es "cuál es mejor" en abstracto, sino si necesitas volver a ver el proceso corriendo (tmux) o solo necesitas que sobreviva sin que lo mires (nohup), y qué está disponible en el servidor donde estás parado.

4. Reconstruye la secuencia de tmux

Escribe, en orden, los comandos para: crear una sesión de tmux llamada import, salir de ella sin matarla, confirmar que sigue existiendo, volver a entrar, y finalmente destruirla por completo.

Ver solución
tmux new -s import           # crea la sesión
# dentro de la sesión: Ctrl-b, soltar, luego d   -> desconecta sin matar nada
tmux ls                      # confirma que "import" sigue en la lista
tmux attach -t import        # vuelve a entrar a la misma sesión
tmux kill-session -t import  # la destruye por completo

Por qué funciona: desconectar (Ctrl-b d) y matar (kill-session) son operaciones distintas — desconectar solo te saca de la vista, la sesión sigue viva en el servidor; matarla la termina de verdad, junto con cualquier proceso que estuviera corriendo adentro.


Resumen y siguiente paso

Antes de avanzar deberías poder:

  • elegir entre scp (copia puntual) y rsync (sincronización real que solo mueve la diferencia) según si vas a repetir la operación o no;
  • explicar qué cambia la barra final en el origen de un rsync, y correr --dry-run antes de cualquier --delete sin que nadie te lo recuerde;
  • explicar por qué un proceso remoto muere cuando se corta tu sesión SSH (SIGHUP), y resolverlo con tmux (crear, desconectar, reconectar, listar, matar sesiones) o con nohup ... & según si necesitas ver el proceso en vivo o no.

Hoy tu "script" siguió siendo una secuencia de comandos que tecleaste uno por uno, aunque ya corrieran dentro de una sesión que sobrevive a tu desconexión. El siguiente paso es dejar de teclear esa secuencia cada vez: convertirla en un archivo que se ejecuta solo, con sus propios argumentos — tu primer script de shell.


Recursos