Skip to content
  • CS-10
  • Reference
  • Last checked November 8, 2026

Linux Server and SSH Commands Cheat Sheet for Site Owners

The Linux commands site owners need over SSH: find and grep files, check disk and inodes, back up and migrate with tar and rsync, fix permissions, read logs, analyze traffic, set up keys and cron.

Run the health check

Most website emergencies on a Linux server come down to a few questions: what changed, what is full, what is running, and what do the logs say. This sheet groups the commands by those jobs. Everything here works over SSH on a typical Linux host, including most shared hosting plans that offer SSH (commands that need root are marked). Run man COMMAND for the full options of any command.

Placeholders: USERNAME is your SSH or site user, 203.0.113.10 stands for your server’s IP address (a documentation address, replace it), /PATH/TO/SITE is the site’s document root (often ~/public_html or /var/www/SITE), DOMAIN is your domain and PATTERN is the text you are searching for. Safety: commands that delete or overwrite are flagged. Run the read-only version first (list before delete, --dry-run before sync) and keep a backup.

Finding files

Command What it does
ls -la All files including hidden ones (.htaccess, .user.ini), with permissions, owner, size and modification time.
ls -laht Newest first, human-readable sizes.
find . -name 'wp-config.php' Find by exact name from the current directory down. -iname ignores case.
find . -type f -name '*.php' -type f files only, -type d directories only. Quote wildcards so the shell does not expand them.
find . -type f -size +100M Files over 100 MB (k, M, G). Sizes round up to whole units, so -size -1k matches only empty files; use -size -1024c for files under 1 KB.
find . -type f -mtime -2 Content modified in the last 2 days (in 24-hour units). +30 means more than 30 days ago.
find . -type f -mmin -60 Modified in the last 60 minutes.
find . -type f -ctime -2 Status changed (content, permissions or owner) in the last 2 days. ctime is change time, not creation time. Attackers can fake mtime with touch; ctime cannot be set that way, so it is harder (though not impossible for root) to disguise.
find . -type f -newer wp-config.php Files modified more recently than a reference file.
find . -type f -newermt '2026-11-01' Modified after a date (GNU find).
find wp-content/uploads -type f -name '*.php' PHP files in uploads, which should normally contain none. A classic malware check.
find . -type f -empty Zero-byte files.
find . -name '*.log' -mtime +30 -print then -delete Deletes. Always run with -print first to see the list, then swap in -delete.

Searching inside files: grep

Command What it does
grep -rnI 'PATTERN' . -r recursive, -n line numbers, -I skip binary files.
grep -rnIi 'PATTERN' . -i case-insensitive.
grep -rlI 'PATTERN' . -l lists matching file names only.
grep -rnI --include='*.php' 'eval(' . Only search PHP files. Repeat --include for more patterns.
grep -rnI --exclude-dir={node_modules,.git,cache} 'PATTERN' . Skip directories.
grep -rnIE 'base64_decode|gzinflate|str_rot13|eval\(' --include='*.php' wp-content -E extended regex with alternatives. Common obfuscation functions. Legitimate plugins use some of these too, so review each hit.
grep -rnI 'DB_NAME' wp-config.php Read one setting without opening the file.
grep -c 'PATTERN' FILE Count matching lines.
grep -v 'PATTERN' Lines that do not match. Useful in pipes.
grep -A 5 -B 2 'Fatal error' error.log Show 5 lines after and 2 before each match.

Disk space and inodes

Command What it does
df -h Free space per filesystem in human-readable units. 100% use breaks uploads, database writes, sessions and logins.
df -i Inode use. Every file uses one inode, so millions of tiny files (cache, sessions, mail) can “fill” a disk that still shows free space.
du -sh /PATH/TO/SITE Total size of one directory.
du -sh * | sort -h Size of each item in the current directory, smallest to largest. Hidden items are not included by *.
du -h --max-depth=1 | sort -hr | head -20 Largest subdirectories first (GNU). -d 1 is the short form.
find . -xdev -type f -size +500M -exec ls -lh {} + Very large files on this filesystem only.
for d in */; do echo "$(find "$d" | wc -l) $d"; done | sort -n File count per directory, to find what is eating inodes.
lsof +L1 Deleted files still held open by a process (root). Space is only freed when that process restarts.

Usual suspects: old backups inside the web root, cache folders, error_log files in every directory, debug.log, and mailboxes.

Backup and restore: tar and zip

Command What it does
tar -czf site-$(date +%F).tar.gz -C /PATH/TO public_html Create (c) a gzip-compressed (z) archive file (f). -C changes directory first so paths inside the archive are relative.
tar -czf site.tar.gz --exclude='public_html/wp-content/cache' -C /PATH/TO public_html Exclude a path. Put --exclude before the paths being archived.
tar -tzf site.tar.gz | head List contents without extracting. Check the path layout before restoring.
tar -xzf site.tar.gz -C /PATH/TO/RESTORE Overwrites files with the same names in the target. Extract to an empty folder first if unsure.
tar -xzf site.tar.gz public_html/wp-config.php Extract a single file.
zip -r site.zip public_html Zip a folder recursively. -x 'public_html/wp-content/cache/*' excludes.
unzip -l site.zip List contents.
unzip site.zip -d /PATH/TO/RESTORE Extract into a folder. Prompts before overwriting unless -o is given.

A file backup is only half a WordPress backup. Export the database too (wp db export, see CS-09), and store copies off the server.

Copying between servers: rsync and scp

Command What it does
rsync -avz --dry-run -e ssh /PATH/TO/SITE/ USERNAME@203.0.113.10:/PATH/TO/SITE/ Preview a push to a new server. Nothing is copied.
rsync -avz -e ssh /PATH/TO/SITE/ USERNAME@203.0.113.10:/PATH/TO/SITE/ The real copy. Rerun it any time; only changed files transfer.
rsync -avz -e 'ssh -p 2222' SRC/ USERNAME@203.0.113.10:DEST/ SSH on a non-standard port.
rsync -avz USERNAME@203.0.113.10:/PATH/TO/SITE/ ./site/ Pull from a remote server to here.
rsync -avz --delete --dry-run SRC/ DEST/ --delete removes files in DEST that are not in SRC. Always dry-run first and check the list.
rsync -avz --exclude='wp-content/cache/' --exclude='*.log' SRC/ DEST/ Skip paths. Exclude patterns are relative to the source.
rsync -avzP SRC/ DEST/ -P shows progress and keeps partial files so large transfers can resume.
scp FILE USERNAME@203.0.113.10:/PATH/TO/ Copy one file. scp -r for folders.
scp -P 2222 USERNAME@203.0.113.10:/PATH/TO/FILE . Download a file. Note the capital -P for the port in scp (rsync and ssh use different syntax).
Flag Meaning
-a Archive: recursive, keeps symlinks, permissions, timestamps, group and owner (owner only when run as root).
-v Verbose: list files as they transfer.
-z Compress in transit. Helps over slow links, wastes CPU on fast ones or with already compressed files.
-n / --dry-run Show what would happen.
Trailing slash on the source SRC/ copies the contents of SRC into DEST. SRC without the slash creates DEST/SRC. The most common rsync mistake.

Permissions and ownership

What the numbers mean

Digit Permissions On a file On a directory
4 r (read) Read contents List names
2 w (write) Change contents Create, delete and rename entries
1 x (execute) Run as a program Enter and reach files inside
0 none

Add the values for each of the three positions: owner, group, others. So 644 is owner read and write (4+2), group read (4), others read (4), shown by ls -l as -rw-r--r--. 755 is rwxr-xr-x.

Value Use for
755 Directories
644 Files
640 or 600 wp-config.php, where the host’s setup allows it (PHP must still be able to read it; some hosts recommend 440 or 400)
700 / 600 ~/.ssh and the files inside it
777 / 666 Never on a web server. Anyone on the system can write to it.
Command What it does
find /PATH/TO/SITE -type d -exec chmod 755 {} + All directories to 755. The + passes many files per chmod call, which is much faster than \;.
find /PATH/TO/SITE -type f -exec chmod 644 {} + All files to 644.
chmod 600 wp-config.php One file.
chmod -R 755 DIR Avoid: sets files and directories to the same value, making every file executable.
stat -c '%a %U:%G %n' FILE Numeric permissions and owner of a file (GNU).
chown -R USERNAME:USERNAME /PATH/TO/SITE Give the site user ownership of every file (root). Fixes files left owned by root after a migration or extraction.
find /PATH/TO/SITE ! -user USERNAME -ls Find files not owned by the site user before changing anything.

When not to chown -R: on shared hosting (you cannot, and the host manages ownership); across a whole home directory, which can also hold mail and SSH files with deliberate owners; to the web server user (www-data, apache, nginx) on a server where PHP runs as the site user, because it breaks updates or lets a compromised PHP process rewrite every file. Check how PHP runs and who owns a fresh install before changing ownership.

Processes and load

Command What it does
uptime Load averages for 1, 5 and 15 minutes. Compare with the CPU count from nproc: load well above it means a queue of work.
free -h Memory and swap. Look at the “available” column, not “free”. Heavy swap use slows everything.
top Live process list. Keys: P sort by CPU, M by memory, c full command, 1 per-CPU, k kill, q quit.
htop Friendlier version of top, if installed. F6 to sort, F9 to kill.
ps aux --sort=-%cpu | head -15 Top CPU users (GNU ps). The minus sign means descending.
ps aux --sort=-%mem | head -15 Top memory users.
ps -u USERNAME -f Processes of one user.
pgrep -af php Process IDs and full commands matching a name.
kill PID Asks the process to stop (SIGTERM, signal 15) so it can clean up.
kill -9 PID Forces it (SIGKILL). Last resort: no cleanup, risk of corrupted files or tables. Never on a database server process.
pkill -u USERNAME -f 'wp-cron' Kill by pattern. Check the match with pgrep -af first.
ss -tunap Open connections and listening ports (root to see all process names).

Reading logs

Command What it does
tail -n 100 FILE Last 100 lines.
tail -f FILE Follow new lines live while you reproduce the problem. Ctrl+C to stop. tail -F keeps following after log rotation.
less +F FILE Follow mode in less. Ctrl+C stops following so you can scroll and search with /; F resumes; q quits.
grep 'PATTERN' FILE | tail -20 The 20 most recent matches.
zgrep 'PATTERN' access.log.*.gz Search rotated, compressed logs without unpacking. zcat and zless also exist.
journalctl -u nginx --since '1 hour ago' Logs of a systemd service (root or the adm or systemd-journal group). -f follows, -p err shows errors only.

Typical log locations

These are common defaults. Your server may differ: check the web server config (ErrorLog, CustomLog, error_log, access_log directives) or your control panel.

Log Debian / Ubuntu RHEL / AlmaLinux / Rocky
Apache /var/log/apache2/access.log, error.log /var/log/httpd/access_log, error_log
Nginx /var/log/nginx/access.log, error.log Same
PHP-FPM /var/log/phpVERSION-fpm.log /var/log/php-fpm/error.log, www-error.log
LiteSpeed /usr/local/lsws/logs/error.log, access.log Same
Mail /var/log/mail.log /var/log/maillog
Auth and SSH logins /var/log/auth.log /var/log/secure
Control panel or app Typical location
cPanel Per-domain access logs under /usr/local/apache/domlogs/ (root), raw logs in ~/access-logs/ for the user, PHP errors in error_log files in the site’s directories, Exim mail in /var/log/exim_mainlog
Plesk /var/www/vhosts/system/DOMAIN/logs/
WordPress wp-content/debug.log when WP_DEBUG_LOG is on
PHP set by the site Wherever error_log in php.ini or .user.ini points. Check with php -i | grep error_log, noting the CLI and web PHP can differ.

Access log analysis one-liners

These assume the common “combined” log format, where field 1 is the IP, field 4 the time, field 7 the URL, field 9 the status and the user agent is the sixth quote-delimited field. Check a line with head -1 access.log and adjust field numbers if your format differs (cPanel, Plesk and CDN logs often do).

Question Command
Top 20 IPs awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20
Top 20 URLs awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -20
Status code counts awk '{print $9}' access.log | sort | uniq -c | sort -rn
Requests per minute awk '{print substr($4, 2, 17)}' access.log | sort | uniq -c | tail -30
Requests per hour awk '{print substr($4, 2, 14)}' access.log | sort | uniq -c
Top 404 URLs awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -20
5xx errors with time and URL awk '$9 ~ /^5/ {print $4, $9, $7}' access.log | tail -30
Top user agents awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head -20
Login and XML-RPC attacks by IP grep -E 'POST /(wp-login|xmlrpc)\.php' access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
What one IP requested grep '^203\.0\.113\.10 ' access.log | awk '{print $4, $7, $9}' | tail -50
Unique visitors (IPs) today grep "$(date +%d/%b/%Y)" access.log | awk '{print $1}' | sort -u | wc -l
Same, across rotated logs zcat -f access.log* | awk '{print $1}' | sort | uniq -c | sort -rn | head
Just the path, without query strings awk '{print $7}' access.log | cut -d'?' -f1 | sort | uniq -c | sort -rn | head

Behind a CDN or proxy, field 1 is the proxy’s IP unless the server logs the real client IP (Apache mod_remoteip, Nginx realip module). uniq -c only counts adjacent duplicates, which is why every pipeline sorts first.

SSH keys and config

Command What it does
ssh-keygen -t ed25519 -C 'you@laptop' Create a key pair: private ~/.ssh/id_ed25519, public ~/.ssh/id_ed25519.pub. Set a passphrase. The -C comment just labels the key.
ssh-keygen -t rsa -b 4096 Only for old servers that do not accept Ed25519.
ssh-copy-id -i ~/.ssh/id_ed25519.pub USERNAME@203.0.113.10 Appends your public key to the server’s ~/.ssh/authorized_keys and fixes permissions. -p 2222 for another port.
Manual install cat ~/.ssh/id_ed25519.pub | ssh USERNAME@203.0.113.10 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
Permissions on the server chmod 700 ~/.ssh and chmod 600 ~/.ssh/authorized_keys. The home directory must not be group- or world-writable, or the SSH server ignores the key.
Permissions on your computer chmod 600 ~/.ssh/id_ed25519. SSH refuses a private key that others can read.
ssh -v USERNAME@203.0.113.10 Verbose: shows which keys are offered and why authentication fails.
ssh-add ~/.ssh/id_ed25519 Load the key into the agent so you type the passphrase once per session.
ssh-keygen -R 203.0.113.10 Remove an old host key after a server rebuild, to clear the “REMOTE HOST IDENTIFICATION HAS CHANGED” warning. Only do this if you know the server really changed.

Host aliases in ~/.ssh/config (permissions 600) let you type ssh mysite, and also work with scp and rsync:

Host mysite
    HostName 203.0.113.10
    User USERNAME
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Host staging
    HostName staging.example.com
    User USERNAME

IdentitiesOnly yes stops SSH offering every key in the agent, which can trigger “Too many authentication failures”. Windows 10 and 11 include the OpenSSH client (ssh, scp, ssh-keygen) but not ssh-copy-id; use the manual install line from PowerShell with type in place of cat.

Cron

Edit your jobs with crontab -e, list them with crontab -l. Each line is five time fields and a command.

Field Allowed values
Minute 0 to 59
Hour 0 to 23
Day of month 1 to 31
Month 1 to 12 (or jan to dec)
Day of week 0 to 7, where 0 and 7 are Sunday (or sun to sat)

* means every value, */15 every 15th, 1-5 a range, 1,15 a list.

Schedule Meaning
*/5 * * * * Every 5 minutes
0 * * * * Every hour, on the hour
30 2 * * * Daily at 02:30 server time
0 3 * * 0 Sundays at 03:00
0 4 1 * * First day of each month at 04:00
0 9 * * 1-5 Weekdays at 09:00

Run WordPress cron from the system instead of page visits: add define( 'DISABLE_WP_CRON', true ); to wp-config.php, then:

*/5 * * * * cd /PATH/TO/SITE && /usr/local/bin/wp cron event run --due-now >/dev/null 2>&1

Or, without WP-CLI: */5 * * * * curl -s https://DOMAIN/wp-cron.php?doing_wp_cron >/dev/null 2>&1. Find the real WP-CLI path with which wp.

Cron rule Why
Use full paths Cron runs with a minimal PATH, so commands that work in your shell may not be found.
Escape % as \% In a crontab line, an unescaped % becomes a newline. date +%F must be written date +\%F.
Redirect output Otherwise cron emails every run’s output to the account (or MAILTO), if mail works.
Server time zone Schedules use the server’s time zone, which may not be yours. Check with date.

Services: systemctl

These need root or sudo, on a VPS or dedicated server. Shared hosting does not allow them: ask the host, or use the control panel’s restart option if it has one.

Command What it does
systemctl status nginx Running or not, recent log lines, and why it failed.
systemctl reload nginx Reread config without dropping connections. Test first with nginx -t.
systemctl restart nginx Full stop and start.
systemctl is-active SERVICE One-word answer, handy in scripts.
systemctl enable --now SERVICE Start now and at boot.
systemctl list-units --type=service --state=failed Every failed service.
Service Debian / Ubuntu name RHEL family name
Apache apache2 httpd
Nginx nginx nginx
PHP-FPM phpVERSION-fpm (for example php8.3-fpm) php-fpm
MySQL / MariaDB mysql or mariadb mysqld or mariadb
LiteSpeed lsws lsws
Redis redis-server redis

Common recipes

Task Command
What changed in the last day? find /PATH/TO/SITE -type f -mtime -1 -not -path '*/cache/*' -ls
Recently changed PHP files, newest first find /PATH/TO/SITE -name '*.php' -mtime -7 -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort -r | head -30
Disk full: find the culprit df -h, then du -h --max-depth=1 ~ | sort -hr | head, then repeat inside the biggest folder
Delete error_log files scattered through the site find /PATH/TO/SITE -name error_log -type f -print, review, then -delete (deletes)
Who is hammering the site right now? tail -n 5000 access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
Migrate a site rsync -avz --dry-run to check, the real rsync, wp db export and import, wp search-replace, then test with curl --resolve before switching DNS
Back up before a risky change tar -czf ~/before-change-$(date +%F-%H%M).tar.gz -C /PATH/TO public_html && wp db export ~/before-change.sql
Keep a long job running after you disconnect Run it inside screen or tmux, or prefix with nohup

Gotchas

Problem Explanation
rm -rf with a variable If the variable is empty, rm -rf "$DIR"/ targets /. Check paths with ls first and never run delete commands you have not read.
Disk shows free space but writes fail Out of inodes (df -i), a full quota, or a different filesystem (for example /tmp) is full.
Deleted a huge log but space did not come back A process still holds it open. Truncate instead with : > FILE, or restart the process.
Files owned by root after extraction or rsync You ran it as root. Fix with chown to the site user, or WordPress cannot update them.
A command works in your shell but not in cron Different PATH, environment, or an unescaped %.
Permission denied (publickey) Wrong user, key not installed, wrong permissions on ~/.ssh, or the server only allows certain keys. Debug with ssh -v.
Different PHP on the command line The php binary in your shell may not be the version the site runs. Check php -v against the control panel; hosts often provide versioned binaries.
Access log numbers seem too low A CDN or page cache answered most requests before they reached the server.

Need the full fix?

Cheat sheets are quick reference. The field guides walk you through each emergency step by step.

Open the Emergency Room

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close