If you are still dragging PHP files into FileZilla and praying your internet connection holds, you are running a deployment strategy from 2008. At wppoland.com, we have migrated dozens of client sites away from FTP-based workflows, and the difference in reliability, security, and developer sanity is night and day. This guide walks you through every layer of a modern WordPress deployment pipeline - from SSH key authentication to fully automated CI/CD - with real commands you can run today.
FTP is dead, here is what replaced it
FTP (File Transfer Protocol) transmits credentials and file contents in plain text. Anyone sitting on the same network - a coffee shop, a shared hosting control panel, a compromised router - can intercept your username, password, and every file you upload. Beyond the security nightmare, FTP has no concept of version history, no rollback mechanism, and no way to coordinate work between multiple developers.
SFTP and FTPS patched the encryption gap, but they did not fix the fundamental workflow problems. You are still manually selecting files, hoping you remembered which ones changed, and overwriting production code with no safety net. One interrupted upload of functions.php and your site returns a white screen.
The modern replacement is a three-layer stack: SSH for secure server access, Git for version control and collaboration, and CI/CD pipelines for automated testing and deployment. Together, these tools give you encrypted connections, full change history, instant rollback, and zero-downtime releases. Every professional WordPress agency in 2026 - including wppoland.com - treats this stack as baseline infrastructure, not a luxury.
SSH key authentication setup
Password-based SSH authentication is vulnerable to brute-force attacks. Key-based authentication eliminates that risk entirely. The server only grants access to clients that possess the matching private key - no password ever crosses the network.
Generate an ED25519 key pair
ED25519 keys are shorter, faster, and more secure than older RSA keys. Generate one with a descriptive comment:
ssh-keygen -t ed25519 -C "deploy@wppoland.com"
This creates two files: ~/.ssh/id_ed25519 (private key - never share this) and ~/.ssh/id_ed25519.pub (public key - safe to distribute).
Copy your public key to the server
The quickest method is ssh-copy-id, which appends your public key to the server’s ~/.ssh/authorized_keys file:
ssh-copy-id user@server.example.com
After this, SSH connections to that server will authenticate automatically without a password prompt.
Configure ssh-agent and the config file
Start ssh-agent so you do not have to enter your key passphrase repeatedly:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Then create or edit ~/.ssh/config for convenient aliases:
Host wppoland-prod
HostName 198.51.100.42
User deploy
IdentityFile ~/.ssh/id_ed25519
Port 22
Host wppoland-staging
HostName 198.51.100.43
User deploy
IdentityFile ~/.ssh/id_ed25519
Port 22
Now ssh wppoland-prod connects you instantly. No IP addresses to remember, no passwords to type. Every developer on your team should have their own key pair - never share private keys between people.
Git-based WordPress workflow
Git transforms WordPress development from “edit files on the server and hope for the best” into a structured, auditable process with full change history.
Repository structure
Not everything in a WordPress installation belongs in Git. Track your custom code; ignore everything that can be reinstalled:
wordpress-project/
├── .gitignore
├── composer.json
├── wp-content/
│ ├── themes/
│ │ └── wppoland-theme/ # tracked
│ ├── plugins/
│ │ └── wppoland-custom/ # tracked
│ └── mu-plugins/ # tracked
└── wp-config.php # NOT tracked
Your .gitignore should exclude WordPress core files, uploads, and sensitive configuration:
/wp-admin/
/wp-includes/
/wp-content/uploads/
/wp-content/upgrade/
wp-config.php
.env
*.log
node_modules/
vendor/
Branching strategy
Keep it simple. A main branch represents production. Feature branches (feature/new-header, fix/contact-form) branch off main and merge back through pull requests. For client projects at wppoland.com, we add a staging branch that auto-deploys to the staging server for client review before merging to main.
Composer for dependency management
Use Composer to manage WordPress plugins and even WordPress core as dependencies. This makes installations reproducible and eliminates the need to commit third-party code:
{
"require": {
"wpackagist-plugin/advanced-custom-fields": "^6.0",
"wpackagist-plugin/wordpress-seo": "^22.0",
"wpackagist-plugin/wp-super-cache": "^1.9"
},
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
}
]
}
Run composer install on the server (or during CI) and every environment gets identical plugin versions.
Deploying with rsync over SSH
While Git handles version control, rsync is the workhorse for transferring built files to production. It only sends the differences between source and destination, making it dramatically faster than uploading everything via FTP or SCP.
Why rsync beats the alternatives
SCP copies entire files every time. FTP has no delta transfer. rsync calculates checksums and transfers only changed blocks. It also supports atomic operations - if the transfer is interrupted, you can resume exactly where it stopped.
The deployment command
Always run a dry-run first to preview what will change:
rsync -avz --dry-run --delete \
--exclude='.git' \
--exclude='wp-config.php' \
--exclude='wp-content/uploads' \
--exclude='.env' \
--exclude='node_modules' \
./dist/ user@server:/var/www/html/
Review the output. When satisfied, remove --dry-run to execute:
rsync -avz --delete \
--exclude='.git' \
--exclude='wp-config.php' \
--exclude='wp-content/uploads' \
--exclude='.env' \
--exclude='node_modules' \
./dist/ user@server:/var/www/html/
The --delete flag removes files from the server that no longer exist in your local build, keeping the deployment clean. The exclude patterns protect uploads, environment config, and version control metadata from being overwritten or deleted.
CI/CD pipelines for WordPress
Continuous Integration and Continuous Deployment automate the entire chain: lint your code, run tests, build assets, and deploy - triggered by a single git push.
GitHub Actions workflow
Create .github/workflows/deploy.yml in your repository:
name: Deploy WordPress
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run PHP linting
run: vendor/bin/phpcs --standard=WordPress wp-content/themes/wppoland-theme/
- name: Build frontend assets
run: |
npm ci
npm run build
- name: Deploy via rsync
uses: burnett01/rsync-deployments@6.0.0
with:
switches: -avz --delete --exclude='.git' --exclude='wp-config.php' --exclude='wp-content/uploads'
path: ./
remote_path: /var/www/html/
remote_host: ${{ secrets.SSH_HOST }}
remote_user: ${{ secrets.SSH_USER }}
remote_key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Clear cache
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: wp cache flush --path=/var/www/html
Store your SSH credentials in GitHub repository secrets - never hardcode them. Every push to main now triggers a full lint, build, and deploy cycle. If linting fails, the deployment never happens. If rsync fails, you still have your previous working version on the server.
WP-CLI, the command-line power tool
WP-CLI lets you manage every aspect of WordPress from the terminal. Combined with SSH, you can administer remote sites without ever opening a browser.
Essential commands
# Update WordPress core
wp core update
wp core update-db
# Plugin management
wp plugin install wordpress-seo --activate
wp plugin update --all
wp plugin deactivate wordfence
# User management
wp user create editor editor@wppoland.com --role=editor
wp user update 1 --user_pass=new_secure_password
# Database operations
wp db optimize
wp db repair
# Search and replace (critical for migrations)
wp search-replace 'https://staging.wppoland.com' 'https://wppoland.com' --all-tables --precise
# Generate a salts refresh
wp config shuffle-salts
# Maintenance mode
wp maintenance-mode activate
Remote WP-CLI via SSH
Run WP-CLI commands on your production server without logging in first:
ssh wppoland-prod "cd /var/www/html && wp plugin status"
ssh wppoland-prod "cd /var/www/html && wp cron event run --due-now"
You can also configure WP-CLI aliases in ~/.wp-cli/config.yml:
@prod:
ssh: deploy@198.51.100.42/var/www/html
@staging:
ssh: deploy@198.51.100.43/var/www/html
Then run wp @prod plugin list from your local machine.
Database migration and sync
The database is the trickiest part of WordPress deployment because it contains serialized data with hardcoded URLs. A simple find-and-replace in SQL will break serialized strings. WP-CLI handles this correctly.
Export and import
# Export from staging
wp @staging db export staging-backup.sql
# Download it
scp wppoland-staging:/var/www/html/staging-backup.sql ./
# Import to production (after backup)
wp @prod db export production-backup-$(date +%Y%m%d).sql
wp @prod db import staging-backup.sql
# Fix URLs with serialization-safe replacement
wp @prod search-replace 'https://staging.wppoland.com' 'https://wppoland.com' --all-tables --precise
Staging to production workflow
At wppoland.com, our database migration workflow follows a strict sequence: export production database as backup, import the staging database, run search-replace for URLs, flush all caches, and verify critical pages. We script the entire process so it runs in under two minutes with a single command. For larger sites, consider the WP Migrate plugin, which handles serialized data, media files, and table selection through a GUI - useful when clients need to trigger their own migrations.
Environment management
Professional WordPress development requires at least three environments: local development, staging for client review, and production. Each environment needs its own database, its own URL, and its own configuration - but the same codebase.
wp-config.php per environment
Use environment detection in wp-config.php to load the correct settings:
<?php
$environment = getenv('WP_ENVIRONMENT_TYPE') ?: 'production';
switch ($environment) {
case 'local':
define('DB_NAME', 'wppoland_local');
define('DB_HOST', 'localhost');
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
break;
case 'staging':
define('DB_NAME', 'wppoland_staging');
define('DB_HOST', 'db.internal');
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
break;
default: // production
define('DB_NAME', 'wppoland_prod');
define('DB_HOST', 'db.internal');
define('WP_DEBUG', false);
break;
}
Using .env files with phpdotenv
A cleaner approach keeps secrets out of wp-config.php entirely. Install vlucas/phpdotenv via Composer and load environment variables from a .env file:
DB_NAME=wppoland_prod
DB_USER=wp_user
DB_PASSWORD=strong_random_password
DB_HOST=localhost
WP_HOME=https://wppoland.com
WP_SITEURL=https://wppoland.com
Each environment gets its own .env file (never committed to Git), while wp-config.php reads from $_ENV and stays identical across all servers. This pattern, popularized by Bedrock from Roots.io, is now standard practice for WordPress agencies that manage multiple environments per project.
Security hardening your deployment
A modern deployment pipeline is only as secure as its weakest link. Once you have SSH keys and automated deploys in place, lock down everything else.
Disable XML-RPC
XML-RPC is a legacy API that enables brute-force amplification attacks. Unless you specifically need it (Jetpack, some mobile apps), disable it:
add_filter('xmlrpc_enabled', '__return_false');
Set correct file permissions
WordPress files should never be world-writable:
# Files: 644 (owner read/write, group and others read)
find /var/www/html -type f -exec chmod 644 {} \;
# Directories: 755 (owner full, group and others read/execute)
find /var/www/html -type d -exec chmod 755 {} \;
# wp-config.php: extra restrictive
chmod 600 /var/www/html/wp-config.php
Move wp-config.php above webroot
WordPress automatically checks one directory above the webroot for wp-config.php. Moving it there prevents direct access via URL:
mv /var/www/html/wp-config.php /var/www/wp-config.php
Protect SSH with fail2ban
Install fail2ban to automatically ban IPs that fail SSH authentication repeatedly:
sudo apt install fail2ban
sudo systemctl enable fail2ban
The default configuration bans an IP for 10 minutes after 5 failed attempts. For production servers at wppoland.com, we increase the ban time to 1 hour and reduce the retry threshold to 3 attempts.
The modern deployment checklist
Moving from FTP to a professional deployment workflow does not happen overnight. Use this checklist to migrate incrementally, one step at a time.
Phase 1 - Foundation (day 1)
- Generate ED25519 SSH keys for every developer
- Disable password authentication on the server (
PasswordAuthentication noinsshd_config) - Install WP-CLI on the server
- Configure
~/.ssh/configfor all environments
Phase 2 - Version control (week 1)
- Initialize a Git repository for the project
- Create a proper
.gitignore(exclude core, uploads, config) - Move plugin/theme management to Composer where possible
- Establish a branching strategy (
main+ feature branches)
Phase 3 - Automated deployment (week 2)
- Set up a staging environment that mirrors production
- Write an rsync deployment script with proper excludes
- Create a GitHub Actions workflow for automated deploys
- Add PHP linting and coding standards checks to the pipeline
Phase 4 - Hardening (week 3)
- Configure per-environment
wp-config.phpor.envfiles - Set file permissions (644 files, 755 directories)
- Install and configure fail2ban
- Disable XML-RPC and unused REST API endpoints
- Document the entire workflow for your team
Every site we manage at wppoland.com follows this progression. Clients who started with FileZilla now have push-to-deploy pipelines with automatic rollback. The upfront investment is a few hours; the return is years of faster, safer, and less stressful deployments.
Learn more about our WordPress development services at WPPoland.

