Efficient React App Deployment Using rsync: A Practical Guide

Efficient React App Deployment Using rsync: A Practical Guide
Photo by Lautaro Andreani / Unsplash

When it comes to deploying a React application, many developers reach for tools like FTP, scp, or Docker. While those methods have their place, if you need a fast, reliable, and incremental way to sync your build directory (usually dist/ or build/), there’s arguably no better tool than rsync.

This article dives into why rsync is ideal for syncing React builds, how to use it effectively, and the gotchas and best practices you’ll want to be aware of — especially when integrating into a CI/CD pipeline like Drone CI.


⚡ Why Use rsync?

Here are a few reasons rsync stands out for syncing front-end build files:

  • Incremental transfer: Only modified files are sent over the wire.
  • Built-in deletion logic: Old files can be removed automatically.
  • SSH support: Secure transmission using private keys.
  • Cross-platform: Works on Linux, macOS, and even Windows via WSL or Git Bash.
  • Script-friendly: Perfect for use in CI/CD pipelines.

📁 The Problem With Hashed Files

Most React/Vite/Webpack builds generate content-hashed filenames like:

app.18e21a0d.js
vendor.8734dsf8.js
style.abcd123.css

Every build produces new filenames, and the old ones are obsolete. If you deploy without cleaning the destination directory, your users might receive stale or conflicting assets, especially when caching is involved.

This is where --delete becomes critical in your rsync command.


rsync -avzh --delete --info=progress2 ./dist/ user@your-server:/var/www/your-app/

Let’s break this down:

Flag Description
-a Archive mode: preserves file permissions, timestamps, symbolic links, etc.
-v Verbose: prints what’s being transferred. Useful for logs.
-z Compress file data during transfer — helps speed on slower networks.
-h Human-readable output for file sizes (e.g., 4.2M instead of 4378923).
--delete Deletes files on the target that are no longer present in source (essential for hashed builds).
--info=progress2 Displays total progress instead of per-file progress (cleaner for large builds).
./dist/ The trailing slash ensures only the contents of dist are synced, not the folder itself.
user@your-server:/.../ SSH target and destination directory.

🧪 Optional: Per-file Progress

If you prefer to see progress for each individual file, replace --info=progress2 with --progress:

rsync -avzh --delete --progress ./dist/ user@host:/path/

This is more verbose but useful during debugging or manual deployment.


🔐 SSH Key Considerations

When using rsync over SSH (which is most common), you’ll want to ensure:

  • Passwordless login is set up using a private key.
  • The remote user (e.g., www-data, deployer, ubuntu, etc.) has write permissions to the target directory.

In CI, use:

-e "ssh -i /path/to/key -o StrictHostKeyChecking=no"

You can even mount the key from a Drone secret or environment variable and assign the correct permissions with chmod 600.


🤖 Integrating with Drone CI

Here's a minimal Drone CI pipeline step using rsync:

steps:
  - name: deploy-to-staging
    image: alpine
    commands:
      - apk add --no-cache openssh rsync
      - chmod 600 /home/drone/staging_key
      - echo "Syncing dist to staging server..."
      - rsync -avzh --delete --info=progress2 \
          -e "ssh -i /home/drone/staging_key -o StrictHostKeyChecking=no" \
          /var/www/html/my-app/dist/ \
          user@staging-server:/var/www/html/your-app/

Be sure to:

  • Add staging_key as a secret.
  • Use the correct user and path.
  • Ensure the dist/ folder is already built before this step runs.

🚨 Things to Watch Out For

Issue Recommendation
Stale Files Remain Always use --delete if your build uses hashed filenames.
Wrong folder structure Use a trailing / on the source path to sync contents, not the directory itself.
Permission denied Double-check remote write permissions for the SSH user.
Missing tools Install rsync and ssh inside CI containers if not present (e.g., Alpine uses apk add).
CI log spam Use --info=progress2 instead of --progress to reduce noise.

🔁 Alternative: scp vs rsync

Tool Behavior
scp Always copies everything — no diffing, no deletion, slower over time.
rsync Smart diffing, can delete, and more efficient. Better for frequent and incremental deploys.

🏁 Finally

Using rsync for syncing your React app’s dist/ folder is a clean, fast, and robust method. It works great locally and scales well to CI/CD environments, especially when you want deterministic builds and clean deployments without stale artifacts.

If you’re deploying hashed frontend builds and care about efficiency and cache hygiene, rsync --delete is not optional — it’s essential.

Support Us