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.
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.
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.
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.exampleServerAliveInterval 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.
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-runfirst.
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.
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.
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 deployssh -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.
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 deployFor 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.
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.
You run: ssh prod tmux attach -t deploy and get the error 'not a terminal'. What is the fix?
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.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.