← Back to Own Your Stack

Sample Chapter from the Guide

Chapter 7: Rsync Deployment

Rsync is the deployment transport in this book. It copies the files you choose from your workstation to the server, skips unchanged files, and keeps the deployment model visible. Later chapters can have Gitea Actions run the same scripts from a clean checkout, but the mechanism stays the same.

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/*

The main rsync excludes .env.prod, .env.staging, and .env.local. The deploy script copies the right one separately as .env for the target environment. 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. For this book, keep the source simple: deploy from the current checked-out directory on your workstation. Later, Gitea or GitLab can run the same script from a clean runner checkout without changing the deployment model. Create deploy.sh in your project root:

#!/bin/bash
set -euo pipefail

# Configuration
SERVER="myserver"  # SSH host from ~/.ssh/config
REMOTE_PATH="/var/www/myapp"
EXCLUDE_FILE="rsync-excludes.txt"
PHP_SERVICE="php8.3-fpm"
ENV_FILE=".env.prod"

echo "Deploying current working tree to $SERVER..."

# Sync files from the current project directory
rsync -avz --delete \
    --exclude-from="$EXCLUDE_FILE" \
    ./ "$SERVER:$REMOTE_PATH/"

# Copy the environment-specific config as .env
rsync "$ENV_FILE" "$SERVER:$REMOTE_PATH/.env"

# 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 -euo pipefail Does

The set -euo pipefail at the top exits on failures, unset variables, and pipeline errors. Without it, the script can continue after a failure and leave things half-deployed.

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