open atlas
↑ Back to track
Command line CLI · 10 · 03

Remote ops capstone

Tie ssh, rsync, and tmux into a complete remote-ops workflow: sync a directory to a remote server, start a long-running job in a detached tmux session, and reconnect to it later — surviving disconnects without losing work.

CLI Senior ◷ 35 min
Level
FoundationsJuniorMiddleSenior

You have a data-processing job that takes four hours. You ssh into the server, start it, and forty minutes later your laptop’s WiFi drops. The job is gone. Or: you scp a 3 GB dataset to the server, it fails at 2.8 GB, and you start over from zero. These are the two most common remote-ops pain points, and both have clean solutions that are already installed on every Linux server. This lesson ties together ssh, rsync, and tmux into a repeatable deploy-and-run workflow that survives disconnects, resumes partial transfers, and lets you check in on long jobs without keeping a terminal open.

Goal

After this lesson you can sync a local directory to a remote server with rsync -avz --delete (with --dry-run first), start a long-running job in a named tmux session so it survives SSH disconnects, reconnect to that session from any terminal, and wire the whole workflow into a deploy script using ~/.ssh/config for ergonomic host aliases.

1

Configure ~/.ssh/config for ergonomic host aliases. Raw ssh commands are verbose and error-prone to retype. A config entry gives you a short alias, fixes the username, key file, and port once, and enables ProxyJump for bastion-host patterns.

# ~/.ssh/config
Host prod
  HostName server.example
  User deploy
  IdentityFile ~/.ssh/id_ed25519
  ServerAliveInterval 60
  ServerAliveCountMax 3

Host staging
  HostName 192.0.2.100
  User deploy
  IdentityFile ~/.ssh/id_ed25519
  ProxyJump bastion.example

ServerAliveInterval 60 / ServerAliveCountMax 3 sends a keepalive every 60 seconds and gives up after 3 missed responses — prevents the SSH connection from hanging silently when the network path goes dead. ProxyJump (OpenSSH 7.3+) replaces the older -o ProxyCommand pattern for bastion access.

After saving, ssh prod connects to deploy@server.example using the right key — no flags required.

2

Sync files with rsync -avz --dry-run first, then without. rsync transfers only the bytes that changed (delta encoding via rolling checksums). This makes re-running a sync after a partial transfer fast — it resumes from where it left off.

# Dry run: see what WOULD change, nothing transferred
rsync -avz --dry-run --delete ./dist/ prod:/var/app/current/

# Real run: transfer the diff
rsync -avz --delete ./dist/ prod:/var/app/current/

Flag breakdown:

  • -a — archive mode: recursive, preserve permissions, timestamps, symlinks, owner, group.
  • -v — verbose: print each file transferred.
  • -z — compress data in transit (useful on slow connections; skip for LAN or already-compressed files like .gz).
  • --delete — remove files on the remote that are absent locally. This is destructive. Always run --dry-run first.

The trailing slash on ./dist/ means “copy the contents of dist”, not the directory itself. prod:/var/app/current/ is the rsync remote path, resolved via the ~/.ssh/config alias.

3

Start a long job in a named tmux session. tmux multiplexes the terminal: a session keeps running on the server after you disconnect. Use named sessions so you can reattach reliably.

# On the remote: start a named session
ssh prod tmux new-session -d -s deploy -x 220 -y 50

# Send a command into the session (non-interactive)
ssh prod tmux send-keys -t deploy 'cd /var/app/current && ./run-job.sh 2>&1 | tee /var/log/job.log' Enter

# Attach to watch live output (interactive)
ssh prod
tmux attach -t deploy

-d starts the session detached (no terminal attached). -s deploy names it. -x 220 -y 50 sets the virtual terminal size to avoid layout issues when attaching from different terminal sizes.

send-keys -t deploy '...' Enter injects keystrokes into the session — this lets you start the job non-interactively in a one-liner from your local machine.

4

Manage tmux sessions: list, attach, kill. Three commands cover 90% of daily tmux ops:

# List all sessions on the remote
ssh prod tmux ls

# Attach to the deploy session
ssh prod -t tmux attach -t deploy

# Kill the session when done
ssh prod tmux kill-session -t deploy

ssh -t forces a pseudo-TTY allocation — required for interactive programs like tmux attach that need a real terminal. Without -t, tmux attach fails with “not a terminal” because SSH’s default mode does not allocate a TTY for commands.

If the session dies (the job finished or crashed), tmux attach -t deploy returns “no sessions”. Check the log: ssh prod tail -50 /var/log/job.log.

5

Handle the bastion (jump host) pattern. Many production environments require SSH through a bastion. With ProxyJump in ~/.ssh/config, rsync and ssh both tunnel through it transparently — you do not need separate flags.

# This works transparently through the bastion defined in ~/.ssh/config
rsync -avz --delete ./dist/ staging:/var/app/current/
ssh staging tmux new-session -d -s deploy

For one-off connections without a config entry, use -J:

ssh -J deploy@bastion.example deploy@192.0.2.100
rsync -avz -e 'ssh -J deploy@bastion.example' ./dist/ deploy@192.0.2.100:/var/app/

The -e flag to rsync overrides the SSH command. This is useful in CI environments where you cannot write ~/.ssh/config.

Worked example

Complete deploy script: sync, start job, tail log.

#!/usr/bin/env bash
set -euo pipefail

REMOTE="prod"
REMOTE_DIR="/var/app/current"
SESSION="deploy"
LOG="/var/log/app/job.log"

echo "==> dry run (review before transferring)"
rsync -avz --dry-run --delete ./dist/ "${REMOTE}:${REMOTE_DIR}/"

read -r -p "Proceed with real sync? [y/N] " answer
[[ "${answer,,}" != "y" ]] && { echo "aborted"; exit 0; }

echo "==> syncing"
rsync -avz --delete ./dist/ "${REMOTE}:${REMOTE_DIR}/"

echo "==> starting job in tmux session '${SESSION}'"
ssh "${REMOTE}" "tmux new-session -d -s ${SESSION} -x 220 -y 50 2>/dev/null || true"
ssh "${REMOTE}" "tmux send-keys -t ${SESSION} \
  'cd ${REMOTE_DIR} && ./run-job.sh 2>&1 | tee ${LOG}' Enter"

echo "==> tailing log (Ctrl-C to detach — job keeps running)"
ssh "${REMOTE}" "tail -f ${LOG}"

The || true after tmux new-session means “create the session if it doesn’t exist; if it already exists, that’s fine too.” This makes the script idempotent — re-running it attaches to the existing session rather than erroring.

To reconnect later from any machine: ssh prod -t tmux attach -t deploy.

Why this works

On macOS, the system ssh and rsync are older BSD versions. For ProxyJump support, ensure OpenSSH >= 7.3 (ssh -V). macOS ships with a very old rsync (2.x); install a current version via Homebrew (brew install rsync) for --info=progress2 and other modern flags. tmux behavior is identical on macOS and Linux — install via brew install tmux if not present.

Common mistake

rsync --delete without --dry-run first is a common source of data loss. If your local dist/ is empty or wrong, --delete wipes the remote directory to match. Always dry-run, read the output, confirm. A second mistake: forgetting -t when running tmux attach over SSH. Without it, SSH does not allocate a pseudo-TTY and tmux cannot claim the terminal — you get “not a terminal” and nothing attaches. Always ssh -t host tmux attach -t session.

Check yourself
Quiz

You run: ssh prod tmux attach -t deploy and get the error 'not a terminal'. What is the fix?

Recap

The remote-ops workflow ties three tools together: ~/.ssh/config eliminates repetitive flags and enables transparent ProxyJump for bastion hosts; rsync -avz --delete syncs only changed bytes and keeps the remote in exact parity with local — always preceded by --dry-run; tmux new-session -d -s NAME starts a detached session that outlives the SSH connection, and tmux attach -t NAME (via ssh -t) reconnects from any terminal at any time. The -t flag to ssh is mandatory for interactive programs that need a real TTY. Together these three tools let you deploy, start a long job, disconnect, and reconnect to check progress — the full remote-ops loop with zero data loss risk.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.