← Back to Own Your Stack

Sample Chapter from the Guide

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:

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:

Server-side runtime files (don't delete):

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

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:

Cons:

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:

Cons:

Choose based on your server's constraints. Option A is simpler when it works.

The rest of this chapter

The remaining sections cover:

This is one of 28 chapters. The complete guide covers everything from initial server setup to monitoring, backups, and disaster recovery.

Get the Guide