Pick the right tool
SFTP is a file-transfer session over SSH, ideal for browsing the server and moving a few files by hand, with command-line or graphical clients. rsync copies only what changed, preserves permissions and times, can delete files that no longer exist locally and can be scripted. Use SFTP for one-off jobs, and rsync to publish a whole site or keep two folders in step. Both use your SSH keys, so no extra service is needed.
SFTP from the command line
The sftp command opens an interactive session. Use get and put to move files, and lcd and cd for the local and remote folders. Add -r to copy folders. Quit with exit.
Create /home/deploy/staging and the intended download directories on the server first. Set your custom SSH port in ~/.ssh/config (Port YOUR_PORT), or use sftp -P YOUR_PORT and scp -P YOUR_PORT. Rsync uses the SSH configuration or an -e "ssh -p YOUR_PORT" transport.
mkdir -p ~/staging ~/logssftp deploy@YOUR_SERVER
# inside the session:
pwd
ls
lcd ~/site
cd /home/deploy/staging
put index.html
put -r assets
get -r logs
exitscp -r ./site/ deploy@YOUR_SERVER:/home/deploy/staging/
sftp -b - deploy@YOUR_SERVER <<'EOF'
cd /home/deploy/staging
put index.html
EOFGraphical clients
FileZilla, WinSCP (Windows), Cyberduck and Transmit all speak SFTP. Choose SFTP over SSH, not FTP, enter your server address, port 22 and your user name, and pick your private key file instead of typing a password. Never use plain FTP, which sends passwords without encryption.
Sync a site with rsync
The basic form copies a folder to the server. The flags: -a keeps permissions and times and recurses, -v lists files, -z compresses in transit, and --info=progress2 shows overall progress. A trailing slash on the source means "the contents of this folder"; without it, the folder itself is copied into the destination. Getting that wrong is the classic rsync mistake.
rsync -avz --info=progress2 ./site/ deploy@YOUR_SERVER:/home/deploy/staging/
# ./site/ copies the contents into staging/
# ./site would create staging/site/Always do a dry run first, especially with --delete
The --delete flag removes files on the destination that no longer exist in the source, which makes the server an exact copy, and also makes a wrong path destructive. Run with -n (dry run) first and read the list. Exclude folders that should live only on the server, such as uploads and configuration files.
rsync -avzn --delete ./site/ deploy@YOUR_SERVER:/home/deploy/staging/
rsync -avz --delete \
--exclude '.git' --exclude 'node_modules' --exclude '.env' --exclude 'uploads/' \
./site/ deploy@YOUR_SERVER:/home/deploy/staging/Get ownership and permissions right
Files uploaded as your user are owned by your user, but a web server runs as www-data on Ubuntu or nginx on AlmaLinux, and needs to read them. The safe pattern is to upload to a staging folder you own, then use sudo rsync to copy into the web root and set the owner. Do not make the web root world-writable.
sudo rsync -a --delete --chown=www-data:www-data /home/deploy/staging/ /var/www/example.com/sudo rsync -a --delete --chown=nginx:nginx /home/deploy/staging/ /var/www/example.com/
sudo restorecon -Rv /var/www/example.comrsync -avz --delete --rsync-path="sudo rsync" --chown=www-data:www-data ./site/ deploy@YOUR_SERVER:/var/www/example.com/Make it reliable
Use a host alias in ~/.ssh/config so commands stay short and use the right key. For large uploads over a flaky link, --partial keeps half-sent files so a restart resumes. --checksum compares file contents instead of times, which is slower and useful when times are unreliable. --bwlimit stops an upload from using all your bandwidth. Put the command in a small script so everyone deploys the same way, or use the Git-based deployment guide for releases with rollback.
Host myserver
HostName YOUR_SERVER
User deploy
Port YOUR_PORT
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yesrsync -avz --partial --info=progress2 --bwlimit=5000 ./site/ myserver:/home/deploy/staging/On Windows
Modern Windows includes an OpenSSH client with sftp and scp. For rsync, use it inside WSL, the Windows Subsystem for Linux, where it behaves as on Linux. WinSCP also has a synchronise feature with a preview of the changes, which is the same idea as a dry run.
A locked-down SFTP-only account for a client or contractor
To let someone upload files without giving them a shell, create an account that can only use SFTP and is confined to one folder. The chroot folder itself must be owned by root and not writable by anyone else, so create a writable subfolder inside it for the uploads. Test with a second session before closing the first, as with any SSH change.
The account home is /upload inside the jail. The jail and its parents stay root-owned; only upload belongs to client1. Store the public key outside the writable jail and end the drop-in with Match all so later SSH configuration is not accidentally scoped to this group.
sudo groupadd sftponly
sudo useradd -g sftponly -s /usr/sbin/nologin -M -d /upload client1
sudo mkdir -p /srv/sftp/client1/upload
sudo chown root:root /srv/sftp /srv/sftp/client1
sudo chmod 755 /srv/sftp /srv/sftp/client1
sudo chown client1:sftponly /srv/sftp/client1/upload
sudo install -d -m 755 /etc/ssh/authorized_keys
sudoedit /etc/ssh/authorized_keys/client1
# Paste this client account’s public key, then:
sudo chown root:root /etc/ssh/authorized_keys/client1
sudo chmod 644 /etc/ssh/authorized_keys/client1Match Group sftponly
ChrootDirectory /srv/sftp/%u
AuthorizedKeysFile /etc/ssh/authorized_keys/%u
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
Match allsudo dnf install policycoreutils-python-utils
sudo semanage fcontext -a -t ssh_home_t "/etc/ssh/authorized_keys(/.*)?"
sudo restorecon -Rv /etc/ssh/authorized_keyssudo sshd -t && echo "Configuration OK"
sudo systemctl reload ssh 2>/dev/null || sudo systemctl reload sshd
sftp client1@YOUR_SERVEROfficial documentation
These upstream references provide more detail on the commands and workflows in this guide.