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.
Before the ten commands: log in with a key, not 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.
1. Disk analysis: What’s eating my space?
When hosting screams “Quota Exceeded”, FileZilla won’t help. Use this:
Du (disk usage)
## Show folders IN current directory, sorted by size
du -h --max-depth=1 | sort -hrNcdu (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. Logs: Real-time debugging
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.logNow 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. Searching files: Where is that code?!
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. Permissions: Fixing “403 forbidden”
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. Backups: Fast archive
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.gzArchiving 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. Database (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 resetWP-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. Mass file deletion
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. Syncing files between machines
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. Port forwarding: reaching a database behind a firewall
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
## Map local port 3307 to the server's MySQL, open no remote shell
ssh -N -L 3307:127.0.0.1:3306 user@serverLeave 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. Domain swap: a dry run before you rewrite the database
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-onlyYou 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.
When the host only gives you 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 and rsync over it beat any GUI client for moving files, 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.






