Every deploy ends up asking the same questions of the target server. Is there enough disk? Did the old release get cleaned up? Why is memory pinned at 95%? Which process is holding port 3000? Where do the logs live for the thing that just crashed?
This guide is the working set of Linux commands that come up in those moments — disk and memory inspection, process and port inspection, log reading, and a few file-system tools that earn their keep on every deploy. It's the practical companion to Linux distros for deployment and checking your Ubuntu version — once you know what server you've got, these are the commands you'll actually type.
Disk space — the most common pre-deploy check
A deploy that runs out of disk fails in unpredictable ways. The first thing to check on any server you don't fully trust:
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 38G 10G 79% /
tmpfs 2.0G 12M 2.0G 1% /run
/dev/sda2 200G 142G 49G 75% /var
-h prints sizes in human-readable form (G, M, K). The Use% column is the one to watch — anything over 80% is worth investigating before pushing a deploy that copies a few hundred MB of release files.
For an inode check (separate from disk space — large numbers of small files can exhaust inodes before bytes run out):
df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 3276800 245031 3031769 8% /
If IUse% is high but Use% is low, you have an inode shortage — usually a sign that a directory is filling up with millions of small files (sessions, cache fragments, runaway log rotation).
Find what's taking up the space
df tells you the partition is 79% full. du tells you which directory is responsible:
sudo du -h --max-depth=1 /var | sort -hr | head -20
142G /var
112G /var/log
24G /var/lib/docker
3.2G /var/cache
1.8G /var/lib/postgresql
--max-depth=1 keeps the output manageable; sort -hr puts the biggest items first. Drill down one level at a time:
sudo du -h --max-depth=1 /var/log | sort -hr | head -10
The pattern repeats — re-run with deeper paths until you find the actual offender. The most common culprits on a deploy server: old release directories that weren't cleaned up, oversized application log files, Docker images and overlay layers, and database backup directories that haven't rotated.
For a one-shot find the biggest files anywhere on disk
:
sudo find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -hr | head -20
The 2>/dev/null suppresses permission-denied noise from system directories you can't read.
Memory — what's using RAM right now
free -h
total used free shared buff/cache available
Mem: 7.7Gi 4.2Gi 312Mi 88Mi 3.2Gi 3.1Gi
Swap: 2.0Gi 0B 2.0Gi
The column to watch is available, not free. free excludes the buff/cache memory that Linux uses as a disk cache — that memory is available to applications when needed; it just isn't sitting idle. available is the honest number.
If available is small and swap is being used, the server is genuinely memory-pressured. If available is small but swap is untouched, the kernel is using RAM for cache the way it should be.
For a per-process view of memory:
ps aux --sort=-%mem | head -10
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
postgres 843 0.1 18.4 8120324 1466240 ? Ss Apr12 12:34 postgres: main
www-data 1402 2.1 12.1 1543216 963548 ? S 14:23 2:31 php-fpm: pool www
mongodb 2103 0.8 8.7 4321076 697432 ? Ssl Mar01 84:12 mongod
RSS (Resident Set Size) is the actual physical memory the process is holding, in KB. %MEM is that as a percentage of total RAM. The processes at the top of this list are the ones to investigate first when memory is tight.
CPU and processes
top is the live view; htop (sudo apt-get install -y htop) is the friendlier version. Both update in real time and show CPU per process.
For a one-shot snapshot in a script:
ps aux --sort=-%cpu | head -10
To find a specific process by name:
ps -ef | grep nginx | grep -v grep
www-data 1812 1810 0 14:20 ? 00:00:01 nginx: worker process
www-data 1813 1810 0 14:20 ? 00:00:01 nginx: worker process
root 1810 1 0 14:20 ? 00:00:00 nginx: master process /usr/sbin/nginx
grep -v grep filters out the grep command itself, which would otherwise match its own search pattern.
For just the PIDs (useful in scripts):
pgrep -f "nginx: worker process"
To kill a process by name:
sudo pkill -TERM nginx # graceful — sends SIGTERM
sudo pkill -KILL nginx # ungraceful — sends SIGKILL (use only after TERM doesn't work)
Ports and network connections
Knowing what's listening on which port is the second-most-common deploy-time question after disk space. The modern command is ss:
sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234))
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1810))
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1810))
LISTEN 0 128 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=2342))
LISTEN 0 128 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=843))
Flags: -t for TCP, -l for listening, -n for numeric ports (skip DNS lookups), -p for process name (requires root).
If a deploy fails with EADDRINUSE, this is the command to identify what's holding the port:
sudo ss -tlnp | grep :3000
The older netstat -tlnp does the same thing but is deprecated on most modern distros — ss is the right default in 2026.
For active (not listening) connections — useful when debugging why is the database CPU pinned
:
sudo ss -tn state established
Logs — where they live and how to read them
On any systemd-based distro (every modern Linux distribution covered in our distro guide), system logs live in the journal, queried with journalctl:
sudo journalctl -u nginx # all logs for the nginx service
sudo journalctl -u nginx -f # follow (tail -f equivalent)
sudo journalctl -u nginx --since "10 min ago"
sudo journalctl -u nginx -p err # error level and above only
sudo journalctl -u nginx -n 100 # last 100 lines
sudo journalctl --since today # everything since midnight
sudo journalctl --since "2026-05-23 14:00" # specific timestamp
Application logs that don't go through systemd still typically land in /var/log/:
tail -n 100 /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
For application-specific log directories (WordPress, Rails, Laravel, Node), check /var/log/<appname>/ or the application's working directory. A useful one-liner to find log files modified in the last day:
sudo find /var/log -type f -mtime -1 -name "*.log"
To search across many log files for an error pattern:
sudo grep -r --include="*.log" "FATAL" /var/log/ | tail -20
File system — finding and inspecting files
find is the workhorse:
sudo find /var/www -name "*.php" -mtime -7 # PHP files modified in the last 7 days
sudo find / -name "config.yml" 2>/dev/null # locate a config across the filesystem
sudo find /tmp -type f -mtime +30 -delete # delete files older than 30 days in /tmp
-mtime is in days; positive numbers mean older than
, negative numbers mean newer than
. find ... -delete is destructive — always run without -delete first to see what would be affected.
To inspect file permissions and ownership:
ls -la /var/www/
stat /var/www/index.php
stat is the more detailed view — it shows owner, group, mode in both numeric and symbolic form, plus access/modify/change timestamps.
To find what's holding a file open (useful when an unmount fails or a log file can't be rotated):
sudo lsof | grep "/var/log/nginx/access.log"
sudo lsof -p 1810 # everything open by PID 1810
sudo lsof -i :3000 # what has port 3000 open
A pre-deploy health-check script
Put the commands together as a sanity check that runs before any deploy that matters:
#!/usr/bin/env bash
# Pre-deploy health check — fail the deploy if any of these thresholds are breached.
set -euo pipefail
# 1. Disk space — abort if any mounted filesystem is over 90% full
high_disk=$(df -h --output=pcent,target | tail -n +2 | awk '{gsub("%",""); if ($1 > 90) print $0}')
if [ -n "$high_disk" ]; then
echo "Disk space warning:"
echo "$high_disk"
exit 1
fi
# 2. Memory — abort if available memory under 200 MB
avail_mb=$(free -m | awk '/^Mem:/{print $7}')
if [ "$avail_mb" -lt 200 ]; then
echo "Memory critical: ${avail_mb}MB available"
exit 1
fi
# 3. Confirm the application port is currently free
if sudo ss -tln | grep -q ":3000 "; then
echo "Port 3000 already in use:"
sudo ss -tlnp | grep :3000
exit 1
fi
# 4. Confirm the previous release directory exists and is readable
test -d /var/www/current || { echo "No /var/www/current symlink"; exit 1; }
# 5. Check for recent errors in the application log (last 5 min)
err_count=$(sudo journalctl -u myapp --since "5 min ago" -p err --no-pager | wc -l)
if [ "$err_count" -gt 10 ]; then
echo "App has ${err_count} errors in the last 5 minutes — investigate before deploying"
exit 1
fi
echo "All pre-deploy checks passed."
Wire this as a pre-deploy SSH command in your deploy tool of choice. Anything that fails the script aborts the deploy before any code changes — so a stuck server, a half-mounted disk, or a runaway process can't be silently buried under a fresh release.
Related guides
- Installing cPanel on Ubuntu 22.04 — many of the disk-space and memory checks above are required pre-flight for cPanel installs.
- Deploying WordPress on a VPS — same server-ops vocabulary, applied to a WordPress stack.
- Installing Keycloak on a VPS — Keycloak in particular is memory-hungry;
free -handps aux --sort=-%memcome up regularly.
For automating these checks across a fleet of servers, DeployHQ's build pipelines and SSH-based deploys let you run the health-check script as a pre-deploy hook — the deploy aborts cleanly before changing anything if the script exits non-zero. Start a free trial to wire health checks into a Git-driven deploy pipeline.
Need help? Email support@deployhq.com or follow @deployhq on X.