How to Use rsync for Fast, Safe VPS Deployments
Ship code and static assets to a VPS with a workflow that is quicker than full re-uploads and safer than dragging files around manually.
How rsync works, how to do dry runs, how to exclude junk files, and how to deploy into a release directory without unnecessary risk.
Static sites, small web apps, build artifacts, config bundles, and teams who want a simple deployment habit without a full CI/CD platform.
rsync --delete is powerful and dangerous. It can clean the target or wipe the wrong directory if you are careless.
Before you begin
- A local project directory you want to deploy.
- SSH access to your VPS.
rsyncinstalled locally and available on the VPS.- A destination path that you understand clearly before you touch
--delete.
rsync is one of the most practical deployment tools for small stacks because it sends only the differences between source and destination. That makes it fast, bandwidth-friendly, and simple to automate. It also gives you more control than copying files manually in an editor or FTP client, which is how many brittle deployments begin.
Why this deployment habit matters
A lot of small server deployments fail for boring reasons. Hidden build files get uploaded, obsolete assets never get removed, permissions drift, or someone edits production directly and later forgets what changed. rsync helps because it encourages repeatability. You can define a command, inspect a dry run, and ship the same way every time.
Step 1: Learn the basic rsync command
Here is a solid starting command for many deployments:
rsync -avz --progress ./dist/ ubuntu@your-vps:/var/www/myapp/What the flags mean:
-akeeps a useful archive-style behavior for files and directories.-vshows what is happening.-zcompresses data during transfer, which often helps over the network.--progressshows transfer progress for larger files.
The trailing slash matters. ./dist/ means “copy the contents of this directory.” Without the slash, rsync may create an extra nested directory on the destination. That detail trips up beginners constantly.
Before doing anything destructive, use a dry run:
rsync -avzn --delete ./dist/ ubuntu@your-vps:/var/www/myapp/The -n flag means no changes are made. You can inspect what would be uploaded or deleted first.
Step 2: Use a safer VPS deployment pattern
For real deployments, it is often better to sync into a release directory instead of writing straight into the live path. A simple structure looks like this:
/var/www/myapp/
├── current -> /var/www/myapp/releases/2026-04-22-1600
├── releases/
└── shared/Create the directories once on the VPS:
ssh ubuntu@your-vps 'mkdir -p /var/www/myapp/releases /var/www/myapp/shared'Deploy a new release from your local machine:
RELEASE=$(date -u +%Y-%m-%d-%H%M)
ssh ubuntu@your-vps "mkdir -p /var/www/myapp/releases/$RELEASE"
rsync -avz --delete \
--exclude '.git/' \
--exclude 'node_modules/' \
--exclude '.env' \
--exclude '.DS_Store' \
./dist/ ubuntu@your-vps:/var/www/myapp/releases/$RELEASE/
ssh ubuntu@your-vps "ln -sfn /var/www/myapp/releases/$RELEASE /var/www/myapp/current"This pattern gives you a cleaner rollback story. If the new release is bad, you can repoint current to the previous release rather than rebuilding from memory.
If your app needs a restart after deployment, do that explicitly after the files arrive:
ssh ubuntu@your-vps 'sudo systemctl restart myapp'Or, for a Docker Compose app that uses local source or built assets:
ssh ubuntu@your-vps 'cd /opt/myapp && docker compose up -d'Do not let rsync overwrite persistent data directories or secret files unless you fully intend it. Keep deployable app files separate from runtime data whenever possible.
Step 3: Verify the deployment actually worked
After sync and restart, check the site from the outside and the service from the inside:
curl -I https://example.com
ssh ubuntu@your-vps 'ls -lah /var/www/myapp/current'
ssh ubuntu@your-vps 'sudo journalctl -u myapp -n 50 --no-pager'For a static site behind Nginx, confirm the expected build files are present and that old ones were removed when appropriate. For application stacks, verify the app process, container, or service restarted cleanly.
When everything is healthy, you should see:
- The new files in the intended release directory.
- The
currentsymlink pointing to the new release. - The public site serving the updated content.
- No unexplained 403, 404, 500, or permission errors in logs.
Troubleshooting common rsync problems
Files landed in the wrong path.
Check your trailing slashes. This is the most common rsync mistake.
Too many files uploaded.
Add excludes for local-only directories like .git/, node_modules/, build caches, and secret files.
Permission denied on the VPS.
Your remote user may not own the deployment path. Fix ownership or deploy to a path your user controls and then move or reload as needed.
--delete wants to remove important files.
Stop and inspect the dry run. This usually means your source directory is missing files you expected, or your destination path is broader than it should be.
The deploy finished, but the website did not change.
You may have synced into the wrong directory, forgotten to update the live symlink, or left an app service using old cached assets or an old container.
--delete against a path you have not listed and double-checked. One wrong destination string can turn a cleanup flag into a disaster.What to do next
Once deployments are repeatable, the next skill is reading logs fast when something still goes wrong. Continue with How to Read Linux Logs with journalctl and tail.
