Web hostingWritten by GLWS14 min readUbuntu 24.04 LTS / AlmaLinux 9
Commands run on your server unless a section says otherwise. Replace example usernames, addresses and paths with your own values. Keep a working session open when changing remote access.

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.

On the server: prepare an owned staging directory
mkdir -p ~/staging ~/logs
On your computer
sftp deploy@YOUR_SERVER
# inside the session:
pwd
ls
lcd ~/site
cd /home/deploy/staging
put index.html
put -r assets
get -r logs
exit
One-shot, without an interactive session
scp -r ./site/ deploy@YOUR_SERVER:/home/deploy/staging/
sftp -b - deploy@YOUR_SERVER <<'EOF'
cd /home/deploy/staging
put index.html
EOF

Graphical 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.

On your computer
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.

On your computer
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/
If you ever type the destination wrongly, --delete can wipe a real folder. Keep backups and check the dry-run output every time.

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.

On the server
sudo rsync -a --delete --chown=www-data:www-data /home/deploy/staging/ /var/www/example.com/
AlmaLinux: owner is nginx
sudo rsync -a --delete --chown=nginx:nginx /home/deploy/staging/ /var/www/example.com/
sudo restorecon -Rv /var/www/example.com
Or in one step from your computer, if your user has passwordless sudo for rsync
rsync -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.

On your computer: ~/.ssh/config
Host myserver
    HostName YOUR_SERVER
    User deploy
    Port YOUR_PORT
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
Then
rsync -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.

Both systems
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/client1
Add to a drop-in file such as /etc/ssh/sshd_config.d/10-sftp.conf, then test and reload
Match Group sftponly
    ChrootDirectory /srv/sftp/%u
    AuthorizedKeysFile /etc/ssh/authorized_keys/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
Match all
AlmaLinux: label the public-key directory for SSH
sudo dnf install policycoreutils-python-utils
sudo semanage fcontext -a -t ssh_home_t "/etc/ssh/authorized_keys(/.*)?"
sudo restorecon -Rv /etc/ssh/authorized_keys
Test and reload
sudo sshd -t && echo "Configuration OK"
sudo systemctl reload ssh 2>/dev/null || sudo systemctl reload sshd
sftp client1@YOUR_SERVER
Give the user a key (authorized_keys under their home, which is inside the chroot) or a strong password, and check that password login is not accidentally disabled for them.

Official documentation

These upstream references provide more detail on the commands and workflows in this guide.