This guide provides step-by-step instructions for manually deploying a WordPress site using a Trellis-inspired symlink-based deployment strategy, which enables zero-downtime deployments and easy rollbacks onto shared hosting such as site ground or other hosts that provide composer and ssh access to your website.
- Prerequisites
- Part 1: Provisioning
- Part 2: Deployment
- Rollback Process
- Final Directory Structure
- Troubleshooting
Before starting, ensure you have:
- SSH access to your remote server
- Git repository with your WordPress project
- Database credentials for your WordPress site
- Composer installed on your remote server
Provisioning is a one-time process that converts a traditional WordPress installation into a structure that supports Trellis-style deployments.
[Run on LOCAL]
# Set timestamp variable for consistent backup directory naming
TIMESTAMP=$(date +%Y%m%d%H%M%S)
# Create a local backup directory
mkdir -p ./backups/$TIMESTAMP
# Backup the entire WordPress installation
rsync -avz user@host:~/public_html/ ./backups/$TIMESTAMP/full_backup/
# Specifically backup critical files
rsync -avz user@host:~/public_html/wp-config.php ./backups/$TIMESTAMP/
rsync -avz user@host:~/public_html/.htaccess ./backups/$TIMESTAMP/[Run on LOCAL]
# Extract and display DB credentials
grep "define.*DB_" ./backups/$TIMESTAMP/wp-config.phpSave these credentials, as you'll need them for the .env file during deployment.
[Run on REMOTE]
ssh user@host
# Create main deployment structure
mkdir -p ~/site/releases
mkdir -p ~/site/shared/uploads[Run on REMOTE]
# Copy existing uploads to shared directory
if [ -d ~/public_html/wp-content/uploads ]; then
cp -r ~/public_html/wp-content/uploads/* ~/site/shared/uploads/
echo "Uploads preserved"
fi[Run on REMOTE]
# Remove core WordPress files (careful with this command: Make sure you have a backup.)
rm -rf ~/public_html[Run on REMOTE]
# Create the initial "current" symlink
# This will be updated with each deployment
mkdir -p ~/site/releases/initial/web
ln -sfn ~/site/releases/initial ~/site/current
# Create the public_html symlink that points to the current release's web directory
# This only needs to be done once during provisioning
ln -sf ~/site/current/web ~/public_htmlDeployment is the process of pushing your Git repository to the server and making it live. This can be repeated for each update.
[Run on REMOTE]
ssh user@host
# Set timestamp for this release
TIMESTAMP=$(date +%Y%m%d%H%M%S)
mkdir -p ~/site/releases/$TIMESTAMP[Run on REMOTE]
# Clone repository to release directory
git clone [email protected]:your-org/site-repo.git ~/site/releases/$TIMESTAMP
cd ~/site/releases/$TIMESTAMP[Run on LOCAL]
# Copy the .env.production file from local to the remote server
scp .env.production user@host:~/site/releases/$TIMESTAMP/.env[Run on REMOTE]
# Secure the .env file
chmod 600 ~/site/releases/$TIMESTAMP/.env[Run on REMOTE]
# Remove the uploads directory in the release and symlink to shared
rm -rf ~/site/releases/$TIMESTAMP/web/app/uploads
ln -sf ~/site/shared/uploads ~/site/releases/$TIMESTAMP/web/app/uploads[Run on REMOTE]
# Install Composer dependencies
cd ~/site/releases/$TIMESTAMP
composer install --no-dev --optimize-autoloader[Run on REMOTE]
# Update the current symlink to point to the new release
# This is the key step that makes the deployment "atomic" and zero-downtime
ln -sfn ~/site/releases/$TIMESTAMP ~/site/current[Run on REMOTE]
# Remove old releases (keep last 5)
cd ~/site/releases
ls -1t | tail -n +6 | xargs -r rm -rfIf you need to roll back to a previous release:
[Run on REMOTE]
# List available releases
ls -la ~/site/releases
# Update current symlink to point to the previous release
ln -sfn ~/site/releases/PREVIOUS_TIMESTAMP ~/site/current
# No need to update the public_html symlink as it always points to ~/site/current/webAfter completing both provisioning and deployment, your directory structure will look like:
~/
├── public_html -> ~/site/current/web (symlink)
└── site/
├── current -> ~/site/releases/20250920123456 (symlink to latest release)
├── releases/
│ ├── 20250920123456/ (timestamped release directory)
│ │ ├── .env
│ │ ├── app/
│ │ │ └── uploads -> ~/site/shared/uploads (symlink)
│ │ ├── composer.json
│ │ ├── web/
│ │ └── ...
│ ├── older_release_1/
│ └── older_release_2/
└── shared/
└── uploads/ (persistent uploads directory)
This structure achieves:
- Zero-downtime deployments - The public_html symlink is switched atomically
- Rollback capability - Previous releases are preserved
- Persistent data - Uploads and other shared data persists across deployments
- Clean separation - Code and content are properly separated
- Verify SSH keys are set up correctly on the remote server
- Test the git clone command manually to see specific errors
- Double-check the database credentials in your .env file
- Ensure the database server allows connections from your hosting environment
- Check if composer is installed and in the PATH
- Try running composer with the
--no-devflag to skip development dependencies - Increase memory limit if needed:
php -d memory_limit=-1 /path/to/composer install