CI/CD Automation with Rsync
Rsync is a lightweight, efficient deployment tool for CI/CD pipelines. Instead of building Docker images or using complex orchestration, you can deploy applications by rsyncing build artifacts directly to your servers.
Why Rsync for CI/CD?
| Approach | Complexity | Speed | Best For |
|---|---|---|---|
| Rsync | Low | Fast (only changed files) | VPS/dedicated, traditional hosting |
| Docker | Medium-High | Moderate | Container-based infrastructure |
| Kubernetes | High | Variable | Large-scale microservices |
| FTP/SFTP | Low | Slow (full upload) | Legacy hosting only |
Rsync is ideal when:
- You're deploying to VPS or dedicated servers
- You want fast, incremental deployments
- You don't need container orchestration
- You want simple, auditable deployment scripts
SSH Key Setup for CI/CD
Every CI/CD system needs SSH access to your server. Create a dedicated deployment key:
# Generate deployment key (no passphrase for automation)
ssh-keygen -t ed25519 -C "ci-deploy-key" -f deploy_key -N ""
# Copy public key to server
ssh-copy-id -i deploy_key.pub deployuser@production-server
Store the Private Key as a CI Secret
- GitHub Actions: Settings → Secrets →
SSH_PRIVATE_KEY - GitLab CI: Settings → CI/CD → Variables →
SSH_PRIVATE_KEY - Jenkins: Credentials → SSH Username with private key
GitHub Actions
Basic Deployment
.github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
- name: Deploy via rsync
run: |
rsync -avz --delete \
--exclude='.git/' \
--exclude='.github/' \
--exclude='node_modules/' \
--exclude='.env' \
--exclude='storage/logs/' \
-e "ssh -i ~/.ssh/deploy_key" \
./ ${{ secrets.DEPLOY_USER }}@${{ secrets.SERVER_HOST }}:/var/www/html/
With Build Step
.github/workflows/build-and-deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
- name: Deploy build artifacts
run: |
rsync -avz --delete \
-e "ssh -i ~/.ssh/deploy_key" \
./dist/ ${{ secrets.DEPLOY_USER }}@${{ secrets.SERVER_HOST }}:/var/www/html/
GitLab CI/CD
.gitlab-ci.yml
stages:
- build
- deploy
build:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache rsync openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
- chmod 600 ~/.ssh/deploy_key
- ssh-keyscan -H "$SERVER_HOST" >> ~/.ssh/known_hosts
script:
- rsync -avz --delete
-e "ssh -i ~/.ssh/deploy_key"
./dist/
$DEPLOY_USER@$SERVER_HOST:/var/www/html/
only:
- main
environment:
name: production
url: https://example.com
Zero-Downtime Deployment
Deploy to a new directory, then swap the symlink — no interruption for visitors:
#!/bin/bash
# zero-downtime-deploy.sh
set -euo pipefail
SERVER="deployuser@production"
DEPLOY_DIR="/var/www/releases/$(date +%F_%H%M)"
CURRENT_LINK="/var/www/html"
# Step 1: Create new release directory
ssh "$SERVER" "mkdir -p $DEPLOY_DIR"
# Step 2: Rsync to new directory
rsync -avz --delete \
--exclude='.git/' \
--exclude='node_modules/' \
--exclude='.env' \
./ "$SERVER:$DEPLOY_DIR/"
# Step 3: Copy environment file from current release
ssh "$SERVER" "cp $CURRENT_LINK/.env $DEPLOY_DIR/.env 2>/dev/null || true"
# Step 4: Swap symlink (atomic operation)
ssh "$SERVER" "ln -sfn $DEPLOY_DIR $CURRENT_LINK"
# Step 5: Cleanup old releases (keep last 5)
ssh "$SERVER" "cd /var/www/releases && ls -t | tail -n +6 | xargs -r rm -rf"
echo " Deployed: $DEPLOY_DIR"
Exclude File for Deployments
.rsync-exclude
# Version control
.git/
.gitignore
.github/
# Dependencies (install on server instead)
node_modules/
vendor/
# Environment and secrets
.env
.env.*
*.pem
*.key
# Development files
tests/
*.test.js
*.spec.js
*.md
LICENSE
# Build tools
webpack.config.js
vite.config.js
tsconfig.json
.eslintrc*
.prettierrc*
# OS files
.DS_Store
Thumbs.db
Use in rsync:
rsync -avz --delete --exclude-from='.rsync-exclude' \
./ user@server:/var/www/html/
Post-Deploy Tasks
After rsync completes, run setup commands on the server:
# After deployment, run setup commands via SSH
ssh deployuser@production << 'EOF'
cd /var/www/html
# Install dependencies
composer install --no-dev --optimize-autoloader 2>/dev/null || true
npm ci --production 2>/dev/null || true
# Clear caches
php artisan cache:clear 2>/dev/null || true
php artisan config:cache 2>/dev/null || true
# Fix permissions
sudo chown -R www-data:www-data /var/www/html
# Reload web server
sudo systemctl reload nginx
echo " Post-deploy tasks complete"
EOF
Common Pitfalls
| Pitfall | Consequence | Prevention |
|---|---|---|
Deploying .env to server | Overwrites production credentials | Always exclude .env |
Deploying node_modules/ | Massive, slow transfer | Exclude and npm ci on server |
No --delete in deployment | Old files accumulate | Use --delete for clean deployments |
| Plaintext SSH key in CI config | Key exposed in logs | Store as encrypted secret |
| No build step before deploy | Source code deployed, not built assets | Build first, deploy dist/ |
| No rollback plan | Broken deploy with no way back | Use symlink swap or keep releases |
| Deploying without dry-run first | Wrong files synced to production | Run --dry-run in CI before real deploy |
Quick Reference
# Basic CI deployment
rsync -avz --delete --exclude-from='.rsync-exclude' \
-e "ssh -i deploy_key" \
./ user@server:/var/www/html/
# Deploy build artifacts only
rsync -avz --delete \
-e "ssh -i deploy_key" \
./dist/ user@server:/var/www/html/
# Zero-downtime with symlink swap
rsync -avz ./ user@server:/var/www/releases/$(date +%F)/
ssh user@server "ln -sfn /var/www/releases/$(date +%F) /var/www/html"
What's Next
- Cron Automation — Schedule automated backups
- Secure Transfer — SSH key management and hardening
- Dry Run and Testing — Validate deployments before executing