SSH for WordPress developers: 10 commands that will save your life

SSH for WordPress developers: 10 commands that will save your life

Last verified: October 14, 2026
16 min read
Guide
Full-stack developer

As a WordPress Developer, you probably spend a lot of time in an FTP client (FileZilla) or hosting panel. That’s a mistake. What takes 15 minutes in FTP (e.g., deleting a cache folder with 100,000 files) takes 2 seconds in the SSH terminal.

In this guide, I will show you a set of commands that senior developers cannot imagine working without.


#How to set up SSH key login instead of a password

Everything below assumes you are already on the server, and how you get there decides whether you will actually use any of it. A password prompt on every connection is enough friction to send you back to the FTP client, and a password is also what brute force bots grind against all day. Generate a pair with ssh-keygen -t ed25519, push the public half with ssh-copy-id user@server, and the login becomes one word. If the host blocks ssh-copy-id, paste the contents of ~/.ssh/id_ed25519.pub into ~/.ssh/authorized_keys yourself, then check that the file is 600 and the .ssh directory 700. Sshd refuses keys that sit in a world writable directory and it does not tell you why, it simply falls back to asking for the password again. While you are in there, give the host an alias in ~/.ssh/config, so the rest of this guide reads ssh prod instead of a line you retype from memory every time.

#How to save server logins in ~/.ssh/config

Typing ssh -p 2222 -i ~/.ssh/client_ed25519 [email protected] every time is how ports and keys get mixed up between clients. The OpenSSH client reads a per-user file, ~/.ssh/config, where each server gets a short name (ssh_config(5)):

Host client-prod
    HostName s123.hosting.example
    User client
    Port 2222
    IdentityFile ~/.ssh/client_ed25519
    IdentitiesOnly yes

From then on ssh client-prod, scp, sftp and rsync pick up those values on their own. Host covers every line below it up to the next Host or Match block, HostName is the real server address, and Port overrides the default of 22. One rule from the manual explains most surprises: for each option the first value found wins, and ssh reads command-line options first, then your user file, then the system-wide /etc/ssh/ssh_config. Put specific hosts at the top of the file and a catch-all Host * block at the bottom. On macOS, GitHub’s guide to SSH keys adds AddKeysToAgent yes and UseKeychain yes to the same block, so you type the key’s passphrase once and the system keychain holds it (GitHub Docs).


#How to fix the SSH error too many authentication failures

The message Too many authentication failures usually hits people who hold keys for many servers. The client offers the server every key ssh-agent holds, one after another, and the server counts each rejected attempt. The limit is the server-side MaxAuthTries option, which defaults to 6 (sshd_config(5)). If the right key sits further back than sixth place in the agent, the server closes the connection before it ever sees it. The fix is in the block from the previous section: IdentitiesOnly yes tells ssh to use only the configured key, even when the agent offers more (ssh_config(5)). For a one-off test, ssh -o IdentitiesOnly=yes -i ~/.ssh/client_ed25519 [email protected] is enough, and ssh -v shows in its Offering public key lines which keys actually reached the server.

From the same family comes the session that freezes on a shaky connection. ServerAliveInterval 15 in the host block makes the client ask the server for a response through the encrypted channel every 15 seconds. With the default ServerAliveCountMax of 3, the manual says ssh disconnects after roughly 45 seconds of silence instead of leaving you with a frozen terminal.


#1. How to check disk usage on a Linux server

When hosting screams “Quota Exceeded”, FileZilla won’t help. Use this:

#du (disk usage)

## How to list folders in the current directory sorted by size over SSH
du -h --max-depth=1 | sort -hr

#ncdu (ncurses disk usage)

If you can, run ncdu. It’s an interactive manager you navigate with arrows. On a noisy server nothing beats seeing what actually eats disk before you purge caches and logs. Reach for du when you want a number to paste into a ticket, and for ncdu when you do not yet know what you are looking for. Both walk the entire tree, which is not free: on a large uploads directory they keep the disk busy for a minute, and on shared hosting that shows up as slower page loads while they run. Point them at a subdirectory rather than the account root if the site is under traffic. Neither tells you whether the space is safe to reclaim, and on a typical WordPress install the honest answer is that archives left behind by a backup plugin and old year folders under wp-content/uploads usually are, while anything inside wp-includes never is.


#2. How to view the WordPress debug.log in real time

Instead of downloading debug.log, opening it with Notepad, and searching for errors… watch it live!

#tail -f

## Follow the last lines of the file IN real-time
tail -f wp-content/debug.log

Now refresh the page in your browser, and errors will appear on the screen. Exit with Ctrl+C. Live tailing is the right tool while you can still reproduce the problem on demand. When you cannot, the line you want has already scrolled past, so open with tail -n 200 wp-content/debug.log and read backwards instead. Two things trip people up here. The file only exists when WP_DEBUG_LOG is enabled in wp-config.php, so an empty terminal usually means logging is off rather than that the site is healthy. And nothing rotates that file on its own: a site throwing one notice per request will grow it until it is the largest object in wp-content, which is exactly how a debug log ends up consuming the quota you were diagnosing in the previous section.


#3. How to find a function in WordPress files with grep

Looking for where add_image_size was used? Don’t download the whole project.

#grep

## Search for the phrase "add_image_size" IN all PHP files recursively
grep -r "add_image_size" .

If you just want a list of files (without content):

grep -rl "add_image_size" .

Use the full form when you know the string and want to read the surrounding code, and -l when you only want to know which plugin owns a behaviour. The search costs almost nothing inside a theme and a great deal inside wp-content/plugins, where it crawls every vendor directory it finds, so --include='*.php' and --exclude-dir=node_modules earn their keystrokes. Verify a hit by opening the file at that line rather than trusting the count. The same function name turns up in a plugin’s own code, in its documentation block and in a translation file, and only one of those three is the definition you were after.


#4. How to fix 403 forbidden errors with file permissions

Often after migration, files have wrong permissions. Remember the rule:

  • Directories: 755
  • Files: 644

#find + chmod

Don’t do it manually. Automate it:

## Set 755 for all directories
find . -type d -exec chmod 755 {} \;

## Set 644 for all files
find . -type f -exec chmod 644 {} \;

Run this when you have a symptom, a 403 or an upload that fails, and not as routine hygiene. A blanket chmod flattens every deliberate exception on the install, and wp-config.php is the one that matters, because it carries the database credentials and belongs at 640 or tighter rather than the 644 the loop just handed it. The loop also only touches what your shell user owns, so on a host where the web server runs as a different account it can fix nothing while reporting no error at all. Confirm with ls -l wp-config.php and by loading the page that was failing, never by running the same find a second time.


#5. How to back up a WordPress site with tar over SSH

Want a quick backup before an update? Don’t copy via FTP (takes ages). Zip it on the server.

#tar

## Create archive backup.tar.gz of current directory
tar -czf backup.tar.gz .

Unzipping:

tar -xzf backup.tar.gz

Archiving on the server is the right move before a plugin update, because the file never crosses your connection. It is not a backup strategy though: the archive sits on the same disk as the site, so it survives a bad update and not a failed volume. Exclude the noise with --exclude='wp-content/cache' unless you enjoy archiving the thing you were about to delete. Check it with tar -tzf backup.tar.gz | head before you trust it, because an archive cut short by a full disk still looks like a perfectly ordinary file in a directory listing.


#6. How to export and import a WordPress database with WP-CLI

If you have WP-CLI (and you should), you don’t need phpMyAdmin.

## Export db
wp db export backup.sql

## Import db
wp db import backup.sql

## Reset db (careful!)
wp db reset

WP-CLI wins the moment the database is large enough that the browser gives up in the middle of an export. It runs as your shell user rather than through the web server, so PHP upload limits and execution timeouts stop applying. What it cannot ignore is the site being live: wp db import drops and recreates tables while visitors are reading pages, so a maintenance window is part of the command, not an optional extra. Verify the result with a single wp option get siteurl, which fails loudly and immediately if the import only went in halfway.


#7. How to delete a large cache folder fast

Deleting a plugin cache folder containing a million small files via FTP can take an hour.

#rm

## Delete folder and everything inside (no undo!)
rm -rf wp-content/cache/

Time taken: 0.5 seconds. This is the correct tool for a cache directory and almost never the correct tool for anything else, because there is no confirmation step and no recovery afterwards. The habit worth building is to run ls against the exact same path first and to delete only once the listing matches what you had in mind. Watch the end of the path especially, since a stray space turns one target into two and the second one is usually the parent. On a busy site the plugin rebuilds its cache on the next request, so what you pay is one slow page load rather than downtime.


#8. How to sync WordPress uploads with rsync

FTP uploads everything again every time. rsync compares the two sides first and sends only the difference, which turns a nightly uploads sync from a coffee break into a few seconds.

#rsync

## Dry run first: print what would be copied, change nothing
rsync -avzn --exclude 'cache/' ./wp-content/uploads/ user@server:/var/www/example.com/wp-content/uploads/

Drop the n once the file list looks right. Two habits are worth keeping. The trailing slash on the source means “the contents of this directory”; without it you nest a folder inside itself and end up with uploads/uploads. And --delete mirrors deletions, so it removes anything on the target that is gone locally. Run that against a live uploads directory from a stale local copy and you have deleted the media library, not synced it.


#9. How to connect to remote MySQL through an SSH tunnel

Managed hosts normally close MySQL to the outside world. Port 3306 answers on localhost and nowhere else, so a desktop client times out and the panel only gives you phpMyAdmin. You do not need the port opened, you need a tunnel.

#ssh -L

## How to map a local port to remote MySQL over SSH without a shell
ssh -N -L 3307:127.0.0.1:3306 user@server

Leave that terminal running and point TablePlus, Sequel Ace or the plain mysql client at 127.0.0.1:3307. -N means “no remote command”, so the session does nothing but forward. The same move reaches anything bound to localhost on the box: Redis on 6379, a staging app on 8080, a metrics endpoint that was never meant to be public. Pick a local port that is actually free, because ssh reports the failed bind and then keeps the session open anyway, which reads as a working tunnel until the first query hangs.


#10. How to change the WordPress domain with WP-CLI search-replace

After a migration the database still carries the old domain, and a good part of it sits inside serialized PHP arrays where each string is prefixed with its own length. A plain SQL find and replace changes the text and leaves the length wrong, so widgets, theme options and page builder layouts come back empty. WP-CLI understands serialization, and it will tell you what it intends to touch before it touches anything.

#wp search-replace --dry-run

## Count what would change, write nothing
wp search-replace 'https://staging.example.com' 'https://example.com' --dry-run --all-tables-with-prefix --report-changed-only

You get a table of tables and columns with a count per row. When those counts match what you expect, run the same command without --dry-run. Add --precise if the site has nested serialized data, it forces the slower PHP path instead of the SQL shortcut, and --recurse-objects when serialized objects are involved. Take a wp db export first either way: search-replace has no undo.


#How to monitor CPU, memory and processes on the server

#Resource usage in real time

# Show the processes using the most resources on the WordPress server
top

# Improved version (if available)
htop

# Show memory usage
free -h

# Show disk usage per partition
df -h

# Show active network connections
netstat -tuln

#How to find heavy PHP processes

# Show active PHP processes
ps aux | grep php

# Show the queries MySQL is running right now
mysqladmin -u root -p processlist

These commands answer a different question from the disk one: not what takes up space, but what is using the processor right now. When several PHP processes sit at the top of the list, the culprit is almost never WordPress itself but a slow query, or a cron job overlapping with itself because the previous run has not finished yet. Check that hypothesis with wp cron event list before you touch the server. And keep in mind that on shared hosting you see processes from the whole machine, so a high load may have nothing to do with your site.


#How to create aliases and maintenance scripts for WordPress

Create aliases for commands you use often:

# Add to ~/.bashrc or ~/.zshrc
alias wplog='tail -f wp-content/debug.log'
alias wpsize='du -h --max-depth=1 | sort -hr'
alias wpcache='rm -rf wp-content/cache/*'
alias wpdb='wp db export backup-$(date +%Y%m%d).sql'
alias wpcheck='wp core verify-checksums && wp plugin list --update=available'

#Automated maintenance script

#!/bin/bash
# Daily WordPress maintenance script
echo "=== WordPress maintenance $(date) ==="

# Database backup
wp db export /backups/db-$(date +%Y%m%d).sql
echo "Database backed up"

# Verify core integrity
wp core verify-checksums
echo "Core verified"

# List available updates
echo "=== Available updates ==="
wp plugin list --update=available
wp theme list --update=available
wp core check-update

# Clean expired transients
wp transient delete --expired
echo "Transients cleaned"

# Optimise the database
wp db optimize
echo "Database optimised"

echo "=== Maintenance complete ==="

#How to run WP-CLI on a remote server from your own machine

WP-CLI can run a command on another machine over SSH, so you do not have to open a shell first. The WP-CLI handbook documents the global --ssh parameter in the form [<scheme>:][<user>@]<host>[:<port>][<path>] (Running commands remotely):

wp [email protected]:2222/home/client/public_html plugin list

The path follows the port directly. If you give only the host, the user is your local user name, the port is 22 and the path is the SSH user’s home directory. A Host alias from ~/.ssh/config works too, so wp --ssh=client-prod inherits the port and key from the section on that file. For projects you return to often, register aliases in the project’s wp-cli.yml or in the global ~/.wp-cli/config.yml:

@prod:
  ssh: [email protected]/home/client/public_html
@staging:
  ssh: [email protected]/home/client/staging
@both:
  - @prod
  - @staging

After that, wp @prod core version queries production, and the @both group runs wp @both core check-update on both installs in one go. The condition most shared hosts trip over: WP-CLI has to be installed on the server and reachable as wp on its $PATH. An SSH session started this way does not read ~/.bashrc, so a wp you set up there as an alias will not be found. The handbook suggests placing the executable at $HOME/bin/wp. Only the command and its output cross your connection, which is what makes it so easy to run the wp search-replace --dry-run from the previous section against a live site.


#What to do when hosting only allows SFTP

Some shared plans advertise SSH and deliver SFTP, which is file transfer over the same protocol with no shell waiting behind it. You find out the moment ssh user@host 'ls' answers with exec request failed on channel 0. Nothing in this guide that executes on the server survives that: no du, no tar, no WP-CLI, because there is no process to run them in. The transfer layer still works, so sftp beats any GUI client for moving files (rsync does not, because it has to start its own process on the far side), and the database side of the job goes back into the browser through phpMyAdmin and a backup plugin. Before you accept the limitation, check whether the provider offers a shell on a different port or switches it on when asked, because with several of them it is a setting rather than a product boundary. If it truly is not available on that plan, that is a concrete reason to price a move, since every workflow above is closed to you.


#Summary

The SSH terminal doesn’t bite. It allows you to work at the speed of the server’s disk, not your internet connection speed. Start with ncdu and tail -f, you won’t want to go back to mouse clicking.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Frequently asked questions

Practical answers to apply the topic in real execution.

Is SSH difficult to learn for WordPress developers#
A handful of commands is enough to get value from it. Start with ls, cd, du and tail -f, which only read and change nothing on the server.
What is the difference between SSH and FTP#
SSH gives you an encrypted shell, so commands run on the server itself. Plain FTP only transfers files and sends the user name and password unencrypted.
Do I need SSH for WordPress development#
Not for editing content. For debugging, backups, WP-CLI and migrations on a live server it is the fastest route, and a host that offers only SFTP rules all of those out.
What are the most essential SSH commands for WordPress#
du for disk usage, tail -f for the debug.log, grep for code search, tar for archives, rsync for syncing and WP-CLI for the database.
Is SSH safe to use#
Yes, when you log in with a passphrase-protected key and password-only login is switched off on the server.

Need an FAQ tailored to your industry and market? I can build one aligned with your business goals.

Let’s discuss

Related Articles