Chapter 7: Rsync Deployment
Rsync copies files from your workstation to the server. It's fast, reliable, and you control exactly what gets deployed. This chapter covers the essentials: how rsync works, what to exclude, automating deploys with a script, and handling post-deployment tasks.
7.1 How Rsync Works
Rsync compares source and destination, then copies only what changed. First deploy copies everything. Subsequent deploys are fast because most files haven't changed.
Basic syntax:
rsync [options] source/ destination/
The trailing slash matters:
rsync source/ dest/copies contents of source into destrsync source dest/copies the source directory itself into dest
Always use source/ with the trailing slash for deploying.
7.2 Essential Rsync Options
rsync -avz --delete ./ deploy@server:/var/www/myapp/
-a (archive): Preserves permissions, timestamps, symlinks. What you want for deployments.
-v (verbose): Shows what's being transferred. Helpful for debugging.
-z (compress): Compresses data during transfer. Faster over slow connections.
--delete: Removes files on server that don't exist locally. Keeps the deployment clean. Without this, deleted files linger forever.
Other Useful Options
--dry-run: Shows what would happen without doing it. Test before deploying.
rsync -avz --delete --dry-run ./ server:/var/www/myapp/
-P (progress): Shows transfer progress. Nice for large uploads.
-e: Specify SSH options (non-standard port, specific key).
rsync -avz -e "ssh -p 2222" ./ server:/var/www/myapp/
7.3 Exclude Patterns
You don't want to upload everything. Development files, IDE configs, and local artifacts should stay on your workstation.
Using --exclude
rsync -avz --delete \
--exclude='.git' \
--exclude='.idea' \
--exclude='node_modules' \
--exclude='.DS_Store' \
./ server:/var/www/myapp/
Using an Exclude File
For complex patterns, put them in a file. Create rsync-excludes.txt:
# Version control
.git
.gitignore
.gitattributes
# IDE and editor
.idea
.vscode
*.swp
*~
# OS files
.DS_Store
Thumbs.db
# Development
node_modules
tests
phpunit.xml
phpunit.xml.dist
.phpunit.result.cache
# Documentation (unless serving it)
README.md
CHANGELOG.md
docs
# Env files that stay on workstation (deploy script handles .env)
.env.prod
.env.staging
.env.local
# Server-side runtime files (don't overwrite)
storage/logs/*
storage/cache/*
storage/uploads/*
public/uploads/*
.env isn't in the exclude list because it carries production values and needs to reach the server with the code. See section 7.10 for details.
Use it with:
rsync -avz --delete --exclude-from='rsync-excludes.txt' ./ server:/var/www/myapp/
What to Exclude
Always exclude:
.git/- Repository history doesn't belong on server- IDE directories (
.idea/,.vscode/) - Test files and PHPUnit config
node_modules/- Unless you're not building assets locally.env.prod,.env.staging,.env.local- Deploy script handles these separately
Server-side runtime files (don't delete):
storage/logs/*,storage/cache/*- Runtime filespublic/uploads/*- User uploads
The /* pattern excludes contents but keeps the directory.
7.4 The Deploy Script
Wrap rsync and post-deploy commands in a script. Create deploy.sh in your project root:
#!/bin/bash
set -e
# Configuration
SERVER="myserver" # SSH host from ~/.ssh/config
REMOTE_PATH="/var/www/myapp"
EXCLUDE_FILE="rsync-excludes.txt"
PHP_SERVICE="php8.3-fpm"
echo "Deploying to $SERVER..."
# Sync files
rsync -avz --delete \
--exclude-from="$EXCLUDE_FILE" \
./ "$SERVER:$REMOTE_PATH/"
# Run post-deploy commands on server
ssh "$SERVER" bash -c "'
cd $REMOTE_PATH
# Install/update dependencies
composer install --no-dev --optimize-autoloader --no-interaction
# Fix permissions
sudo chown -R deploy:www-data .
chmod -R 775 storage/
# Run database migrations
php scripts/migrate.php
# Clear application cache (framework-specific, uncomment if needed)
# php artisan cache:clear && php artisan config:clear # Laravel
# php bin/console cache:clear # Symfony
# Reload PHP-FPM to clear opcache
sudo systemctl reload $PHP_SERVICE
echo \"Deploy complete on server\"
'"
echo "Done!"
Make it executable:
chmod +x deploy.sh
Deploy with:
./deploy.sh
What set -e Does
The set -e at the top exits immediately if any command fails. Without it, the script continues after errors, potentially leaving things broken.
SSH Config Requirement
The script uses $SERVER as an SSH host alias. You need this in ~/.ssh/config:
Host myserver
HostName your-server-ip
User deploy
IdentityFile ~/.ssh/id_ed25519
With this entry, both ssh myserver and rsync ... myserver: use the correct connection details.
7.5 Why Reload PHP-FPM?
PHP caches compiled scripts in memory (opcache) for performance. After rsync updates files on disk, PHP-FPM workers still have old bytecode cached.
In production, opcache.revalidate_freq is typically 0, so PHP never checks if files changed. Without a reload, PHP serves old code indefinitely.
systemctl reload gracefully restarts workers: existing requests finish, then new workers start with empty opcache, so connections are preserved.
7.6 Post-Deploy Steps Explained
The deploy script runs several commands on the server after syncing files:
Composer Install
composer install --no-dev --optimize-autoloader --no-interaction
--no-dev: Skip development dependencies (PHPUnit, etc.)--optimize-autoloader: Build faster classmap--no-interaction: Don't prompt (automated deploy)
Run this after every deploy in case composer.lock changed.
Permissions
sudo chown -R deploy:www-data .
chmod -R 775 storage/
Ensures web server can read files and write to storage directories.
Migrations
php scripts/migrate.php
Runs pending database migrations. See Chapter 8 for the migration script.
Cache Clear
Framework-specific. Laravel, Symfony, and others cache configs and views. Clear after deploy so cached data matches new code.
PHP-FPM Reload
sudo systemctl reload php8.3-fpm
Clears opcache, activates new code. Always the last step.
7.7 Deploying Vendor Directory
Two approaches for Composer dependencies:
Option A: Run Composer on Server (Recommended)
Server runs composer install during deploy. Dependencies install fresh.
Pros:
- Smaller rsync transfer
- Server always has correct platform-specific packages
Cons:
- Composer must be on server
- Can fail if packagist is down
- Memory-hungry on small VPSes
This is what the deploy script above does.
Option B: Deploy Vendor from Workstation
Rsync the vendor/ directory directly. Remove it from your exclude file.
Before deploying:
composer install --no-dev --optimize-autoloader
./deploy.sh
Pros:
- No composer on server
- Works offline
- No memory issues on small VPSes
Cons:
- Larger transfer (vendor can be 50-100MB)
- Platform-specific packages might differ (rare with pure PHP)
Choose based on your server's constraints. Option A is simpler when it works.
The rest of this chapter
The remaining sections cover:
- 7.8 Checking What Will Change
- 7.9 Atomic Deploys with Symlinks
- 7.10 Configuration Files
- 7.11 Deploying from PHPStorm
- 7.12 Windows Users
- 7.13 Deployment Checklist
This is one of 28 chapters. The complete guide covers everything from initial server setup to monitoring, backups, and disaster recovery.
Get the Guide