Chapter 16 concluded Part 2 of this series with shell customization, prompts, aliases, and portable dotfiles for daily desktop needs. Part 3 begins with this chapter, shifting focus significantly: from personal computers to servers, and from the User role to the Sysadmin role. Daily managed servers, whether a small VPS or a production cluster, almost always run Linux. Therefore, some commands discussed throughout Part 3 are specific to the Linux ecosystem and differ from their macOS counterparts, something that will be marked explicitly whenever relevant. Before moving into more complex topics like cron in Chapter 18 or logging in Chapter 19, a Sysadmin needs the most fundamental foundation first: how to read system conditions, manage running processes, and configure who is allowed to do what on the server. This chapter covers these three foundations: system information via uname, uptime, df, du, and free; process management via ps, top/htop, kill, and nice; user and permission management via useradd, chmod, chown, and sudo; concluded by a case study combining everything into a single server audit script.
17.1 System Information: uname, uptime, df, du, and free
This section covers the five most basic commands for reading system conditions: kernel identity, server uptime along with its workload, disk space usage, and memory usage. These five are the first commands typically run by a Sysadmin upon logging into an unfamiliar server, before investigating issues further.
17.1.1 uname: Kernel and Architecture Information
uname (Unix name), part of GNU coreutils, prints basic information about the operating system and running kernel. Without any options, uname only prints the kernel name (Linux); additional options are required for more useful details.
uname -aThe -a option (all) prints all information at once: kernel name, hostname, kernel release version, build date, machine architecture, and operating system name. The format is roughly Linux host-name 6.8.0-45-generic #45-Ubuntu SMP ... x86_64 GNU/Linux. In practice, the two most frequently needed parts are often called separately so that the output can easily be reused inside other scripts.
uname -r
uname -muname -r prints only the kernel release version (for example, 6.8.0-45-generic), which is important to check before installing kernel modules or drivers that require a specific minimum version. uname -m prints the machine architecture (x86_64, aarch64, and similar), which is crucial when downloading third-party binaries compiled per architecture; downloading an aarch64 binary by mistake for an x86_64 server results in an "Exec format error" that can be confusing if the root cause is not understood beforehand.
17.1.2 uptime: Load Average and Uptime Duration
uptime prints the current time, how long the system has been running, the number of currently logged-in users, and the load average: three numbers representing the average number of processes running or waiting for CPU time across three different time windows.
uptimeAn example output looks like 14:32:05 up 21 days, 3:14, 2 users, load average: 0.45, 0.62, 0.58. The last three numbers represent the average load over the past 1 minute, 5 minutes, and 15 minutes, respectively, calculated by the Linux kernel from /proc/loadavg. The load average numbers themselves mean nothing without the context of the available CPU core count; a load average of 4 on a server with 8 cores is still light, but the same number on a server with 2 cores means process queues are backing up. Check the core count via nproc for accurate interpretation.
nprocFor a concise and direct sentence-based reading, the -p option (pretty) displays only the active uptime duration without load average details or user count.
uptime -p17.1.3 df and du: Disk Usage
Section 12.2.3 briefly touched upon du through a wrapper script example using options -d and -a; this section discusses du alongside its companion, df, in greater depth. Both commands are part of GNU coreutils and deal with disk storage, but they answer different questions: df (disk free) reports available space per mounted filesystem, while du (disk usage) reports how much space a specific file or directory consumes.
df -hThe -h option (human-readable) displays sizes in easy-to-read units (K, M, G) instead of raw byte counts, and is supported by both GNU coreutils on Linux and default BSD df on macOS. The Use% column in the df -h output is the most frequently monitored metric by Sysadmins; a filesystem approaching 100% risks causing services to fail to write logs or databases to fail to commit transactions.
Once df -h shows that a filesystem is nearly full, the next question is which directory is consuming that space, and that is where du comes in.
du -sh /var/log
du -h -d 1 /var | sort -rh | headdu -sh (summarize, human-readable) prints a single line with total size for the given path, without listing internal contents. The -d 1 option (depth) limits traversal depth to just one level down, resulting in a list of sizes for each direct subdirectory within /var without digging deeper; chained with sort -rh from Chapter 9, the largest subdirectory appears right at the top. GNU du also provides --max-depth=1 as the long format for -d 1, but the short option -d is also recognized by default BSD/macOS du, whereas --max-depth is a GNU-specific extension not available there; adopting -d keeps habits portable across both platforms.
A common pitfall encountered in the field: df -h reports a full filesystem, but du -sh across all reasonable directories fails to find files of that size. The most frequent cause is a process still holding a file handle to a deleted file; as long as that process is not closed or restarted, the kernel keeps allocating its disk space even though the file no longer appears in the filesystem. lsof (if installed) helps track down such processes using lsof +L1, which lists files marked as deleted but still open, a topic covered again when discussing network and process troubleshooting in Chapter 22.
17.1.4 free: Memory and Swap Usage
free, part of the procps/procps-ng package available by default on almost all Linux distributions, outputs a summary of physical memory (RAM) and swap usage. This command is Linux-specific; macOS does not provide free at all and uses vm_stat instead, making this section purely applicable to the Linux servers that are the focus of Part 3.
free -hThe free -h output displays two main rows, Mem and Swap, with columns total, used, free, shared, buff/cache, and available. The most misunderstood column is free: this number is not the actual memory size available for new applications, but memory that has not been touched at all since boot. Linux intentionally utilizes idle RAM for page cache (buff/cache column) to speed up subsequent file read operations, and the kernel immediately releases this cache whenever another application needs memory. The available column is a much more accurate estimate of how much memory can actually be used without hitting swap, as it accounts for potential cache release. Sysadmins panicking upon seeing a small free column on a perfectly healthy server is a very common misunderstanding, especially among those managing Linux servers for the first time.
Combining all five commands above into a single quick server condition snapshot is a common habit practiced by Sysadmins at the start of a troubleshooting session.
echo "Host: $(uname -n) | Kernel: $(uname -r)"
uptime -p
df -h /
free -h17.2 Process Management: ps, top/htop, kill, and nice
This section covers how to view processes running on the server, monitor them in real time, terminate them safely, and adjust their CPU priority. These skills are essential, especially when a server feels slow and a Sysadmin needs to identify which process is responsible.
17.2.1 ps: Process Snapshot
ps (process status) prints a list of processes running at the moment the command is executed, unlike top in section 17.2.2 which continuously updates its display. ps has a long history featuring two distinct option styles: BSD style without hyphens and System V style with hyphens. The Linux implementation of ps (from the procps-ng package) supports both simultaneously.
ps aux
ps -efps aux (BSD style) displays all processes belonging to all users (a), including processes without a controlling terminal (x), in a user-oriented format (u) that includes %CPU and %MEM columns. ps -ef (System V style) displays all processes (-e) in full format (-f), highlighting the PPID (parent process ID) column, making it easier to trace which process spawned which. Both are equally valid; choose based on which columns are required at the time.
Finding the most CPU- or memory-hungry processes without opening top can also be done directly via ps using the --sort option.
ps aux --sort=-%cpu | head -n 6The minus sign before %cpu sorts from highest to lowest value (descending); without the minus sign, ordering is ascending. head -n 6 from Chapter 2.3 trims the output to the header line plus the top five processes, a pattern reused in the case study in section 17.4.
17.2.2 top and htop: Real-Time Monitoring
top, also part of procps-ng and installed by default on almost all Linux distros, displays a process list automatically updated every few seconds, complete with a summary of overall CPU and memory utilization at the top of the screen.
topSome of the most frequently used keyboard shortcuts inside top: P sorts by CPU utilization (default order), M sorts by memory utilization, k sends a signal to a process without exiting top (prompts for PID and signal number), and q exits the view. htop is a popular third-party alternative offering a colored interface, mouse support, and a process tree view that clarifies parent-child relationships. Unlike top, htop is almost never installed by default and needs manual installation via a package manager.
# Debian/Ubuntu
sudo apt install htop
# Fedora
sudo dnf install htop
# Arch Linux
sudo pacman -S htopIn practice, checking whether htop is available before a server loses network accessibility (for instance, during SSH issues) is standard procedure. However, top, always present without extra installation, remains the primary tool when time is constrained or package manager access is unavailable.
17.2.3 kill and Process Signals
kill does not directly "kill" a process; it sends a signal, a kernel-level interrupt, to the target process via its PID, leaving it up to the process itself to decide how to respond. The following table summarizes the signals most commonly used by Sysadmins daily.
| Signal | Number (common on x86/x86_64) | Purpose |
|---|---|---|
SIGHUP | 1 | Originally terminal disconnect signal; used by many daemons as a "reload configuration" command |
SIGINT | 2 | Same signal sent when pressing Ctrl+C |
SIGTERM | 15 | Request for graceful termination; default signal for kill when omitted |
SIGKILL | 9 | Forces immediate termination, cannot be caught or ignored by target process |
kill 4821
kill -9 4821
kill -lkill 4821 without a signal number option sends SIGTERM by default, giving the process an opportunity to close file handles, complete pending transactions, and clean up before stopping. kill -9 4821 sends SIGKILL, forced to terminate immediately by the kernel without the target process getting a chance to clean up. kill -l prints a full list of signal names recognized by the system along with their numbers. Signal numbers shown in the table above are consistent across most architectures, but POSIX specifications note that certain signal numbers (especially around SIGSTOP) may vary on less common architectures. Using signal names (kill -TERM, kill -KILL) is safer and more portable than memorizing numbers, except for -9 and -15, which are universal conventions among Sysadmins.
The principle of caution must be emphasized here: SIGKILL is effective at stopping completely frozen processes that fail to respond to SIGTERM, but it carries real risks, especially for processes currently writing to disk files or databases. The proper habit is to always try SIGTERM first, wait a few seconds, and only switch to SIGKILL if the process fails to terminate. To search for and terminate processes by name without manually retrieving PIDs via ps first, pkill process_name and killall process_name serve as shortcuts; pkill matches process name patterns, while killall requires an exact name match.
17.2.4 nice and renice: Process Priority
Sometimes a CPU-hungry process is not a rogue process needing termination, but a legitimate job that simply needs to yield resources so critical services remain unaffected. nice (GNU coreutils) executes a new process with a specific priority level, defined by a niceness value ranging from -20 (highest priority) to 19 (lowest priority), with 0 as the default.
nice -n 10 ./backup-besar.shThe command above runs backup-besar.sh with a niceness of 10, making it yield CPU time to other processes when system usage is high. This is suitable for heavy tasks like backups or archive compression (Chapter 21) that do not require high-speed completion as long as main services run unhindered. Decreasing niceness (increasing priority) below 0 requires root privileges, as it carries the risk of allowing a process to hog CPU allocation from other system processes.
For a process already running without nice, renice (part of the util-linux package) modifies its niceness without restarting or killing the process.
renice -n 15 -p 4821This provides a milder alternative to issuing a kill command on processes that must complete but are interfering with other tasks; lowering priority via renice grants breathing room to other services without sacrificing running tasks.
17.3 User and Permission Management
This section covers creating new users, managing file permissions and ownership, and delegating administrative privileges via sudo without sharing the root password directly. Servers managed by multiple people or hosting multiple services almost always require distinct user separation for security and accountability, a topic explored further regarding security in Chapter 23.
17.3.1 useradd and adduser: Creating New Users
useradd is a low-level utility for creating new users, part of the shadow-utils tool suite (upstream project name "shadow") available across almost all Linux distributions. The wrapper package name in package managers may differ per distro, so avoid assuming a universal package name when searching for it; verify via dpkg -S $(command -v useradd) on Debian/Ubuntu or rpm -qf $(command -v useradd) on RHEL/Fedora when confirmation is needed on managed systems. Default behavior also varies across distributions, requiring explicit verification rather than assuming uniformity.
sudo useradd -m -s /bin/bash deploy
sudo passwd deployThe -m option (create home) creates the directory /home/deploy along with default skeleton contents from /etc/skel, and -s specifies the login shell for that user. passwd deploy is executed separately afterward to set the password, as useradd does not configure a password by default. The -G option adds the user to one or more secondary groups during creation, separated by commas.
sudo useradd -m -s /bin/bash -G docker,sudo deployAn important detail frequently missed: whether -m must be explicitly passed depends on the contents of /etc/login.defs on the distribution in use. The table below summarizes default behavior differences across major distribution families; when uncertain, inspect CREATE_HOME in /etc/login.defs on the managed server directly.
| Distro | useradd without -m | adduser |
|---|---|---|
| Debian/Ubuntu | Home directory is not automatically created | Separate interactive Perl script, automatically creates home + prompts for password |
| RHEL/CentOS/Fedora | Home directory is automatically created (CREATE_HOME yes default) | Direct symlink to useradd, not a separate tool |
On Debian and Ubuntu, adduser is a Perl-based wrapper far friendlier for interactive usage: running sudo adduser deploy automatically creates the home directory, copies skeleton files from /etc/skel, and prompts for a password alongside additional details. Due to its interactive nature, Debian/Ubuntu adduser is less suited for automated scripts compared to non-interactive useradd; standard practice uses adduser for manual user creation and useradd with explicit options for server provisioning and automation (Chapter 24).
17.3.2 chmod and chown: Permissions and Ownership
Every file and directory in Linux has three permission tiers: read (r), write (w), and execute (x), applied separately across three scopes: owner (file owner), group (owning group), and others (all remaining users). chmod (change mode) modifies these permission combinations via two notations: numeric and symbolic.
chmod 755 deploy.sh
chmod u+x deploy.shNumeric notation adds values for each permission: r=4, w=2, x=1 for each of the three scopes in sequence (owner, group, others). The following table summarizes common permission modes.
| Numeric notation | Meaning | Common usage |
|---|---|---|
644 | rw-r--r-- | Regular files; only owner can write |
600 | rw------- | Sensitive files such as private SSH keys; only owner can read/write |
755 | rwxr-xr-x | Executable scripts or binaries; writable only by owner |
700 | rwx------ | Private directory; access and listing limited to owner |
Symbolic notation uses target specifiers (u=user/owner, g=group, o=others, a=all), operators (+ adds, - revokes, = sets exact), and permission flags (r, w, x). This notation works best when adjusting a specific permission without altering existing settings for other scopes. For instance, chmod u+x deploy.sh adds execute permissions for the owner while leaving group and other permissions unchanged, unlike numeric notation which sets all three tiers simultaneously from scratch.
sudo chown deploy:deploy /var/www/aplikasi
sudo chown -R deploy:www-data /var/www/aplikasichown (change owner) modifies file ownership using the format user:group. Omitting the group segment (e.g., chown deploy file) changes only user ownership while retaining the existing group. The -R option (recursive) applies changes across an entire directory structure and its subdirectories, commonly used to grant service users ownership over application folders. Incorrect ownership or permission settings represent a primary reason services fail to start on servers, typically manifesting as Permission denied errors in logs. Quick verification can be performed using ls -l from Chapter 2.2 to check permissions and ownership at once.
17.3.3 sudo: Delegating Root Access
sudo (superuser do) enables non-root users to execute specific commands with root privileges without requiring direct access to or knowledge of the root password. Rules dictating execution privileges are defined in /etc/sudoers, but this file must not be edited directly with nano or standard text editors; visudo must be used instead.
sudo visudovisudo locks /etc/sudoers during editing and validates syntax before committing changes. A minor syntax error introduced via a standard editor can cause sudo to fail for all users on the server, preventing privilege recovery. visudo prevents this scenario by rejecting invalid syntax files.
For standard administrative tasks, adding users to predefined administrative groups configured within /etc/sudoers is safer and more common than editing /etc/sudoers directly every time a new user is created. Administrative group names vary across Linux distributions.
# Debian/Ubuntu
sudo usermod -aG sudo deploy
# RHEL/CentOS/Fedora
sudo usermod -aG wheel deployThe -aG flag combination in usermod is vital: -a (append) ensures the user is added to the specified group without removing them from existing group memberships. Omitting -a causes -G to overwrite all existing secondary group memberships, which may accidentally revoke access to required resources.
Configuration rules inside /etc/sudoers can also exempt specific users from password prompts when invoking sudo by using the NOPASSWD directive.
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart aplikasiThe configuration line above grants user deploy access to run a single command, systemctl restart aplikasi, without prompting for a password. This illustrates the principle of least privilege: granting users access only to specific commands strictly necessary for their duties, rather than full root access. Configuring deploy ALL=(ALL) NOPASSWD: ALL across all commands might seem convenient, but it carries significant risk as it equates to granting full unauthenticated root privileges. Access hardening topics are covered in depth in Chapter 23. Allowed commands for the currently logged-in user can be audited using sudo -l without opening /etc/sudoers directly.
sudo -l17.4 Case Study: Server Condition Audit Script
This concluding case study combines commands from sections 17.1 to 17.3 into a unified audit script that outputs server health metrics concisely: system details, disk usage with threshold alerts, memory usage, top CPU-consuming processes, and currently logged-in users. Such scripts serve as standard first steps before automation via cron in Chapter 18, alerting integration in Chapter 20, or expansion into security toolkits in Chapter 23.
17.4.1 Script Design
The script accepts two options via getopts, following the pattern discussed in section 12.2.3: -t to specify the disk percentage alert threshold (defaulting to 80), and -o to output results to a file instead of displaying them on screen. The logic is divided into functions following Chapter 8 practices, ensuring individual check routines remain readable and extensible. The script returns an exit status of 1 if any filesystem exceeds the specified threshold, adhering to Chapter 6.4 exit status conventions to enable direct integration with automated alert triggers in Chapter 20 without text re-parsing.
17.4.2 Script Implementation
#!/bin/bash
THRESHOLD=80
OUTPUT=""
usage() {
printf 'Usage: %s [-t threshold_percent] [-o output_file]\n' "$0" >&2
exit 1
}
while getopts ":t:o:h" opt; do
case "$opt" in
t) THRESHOLD="$OPTARG" ;;
o) OUTPUT="$OPTARG" ;;
h) usage ;;
:) echo "Error: option -$OPTARG requires an argument" >&2; exit 1 ;;
\?) echo "Error: unknown option: -$OPTARG" >&2; exit 1 ;;
esac
done
check_system_info() {
printf '=== System Info ===\n'
printf 'Hostname : %s\n' "$(uname -n)"
printf 'Kernel : %s\n' "$(uname -r)"
printf 'Uptime : %s\n' "$(uptime -p)"
printf 'Logged Users: %s\n' "$(who | wc -l)"
}
check_disk() {
local status=0
local fs pct target numeric_pct
printf '\n=== Disk Usage (threshold %s%%) ===\n' "$THRESHOLD"
while read -r fs pct target; do
[[ "$fs" == "Filesystem" ]] && continue
numeric_pct=${pct%\%}
if (( numeric_pct >= THRESHOLD )); then
printf '[WARNING] %-20s %s%% (%s)\n' "$fs" "$numeric_pct" "$target"
status=1
else
printf '[OK] %-20s %s%% (%s)\n' "$fs" "$numeric_pct" "$target"
fi
done < <(df -h --output=source,pcent,target)
return "$status"
}
check_memory() {
printf '\n=== Memory Usage ===\n'
free -h
}
check_top_processes() {
printf '\n=== Top 5 Processes (CPU) ===\n'
ps aux --sort=-%cpu | head -n 6
}
generate_report() {
check_system_info
check_disk
local disk_status=$?
check_memory
check_top_processes
return "$disk_status"
}
if [[ -n "$OUTPUT" ]]; then
generate_report > "$OUTPUT"
exit_code=$?
echo "Report saved to: $OUTPUT"
else
generate_report
exit_code=$?
fi
exit "$exit_code"check_disk uses df -h --output=source,pcent,target. The --output flag selects specific columns, making output parsing via read safer than parsing default df -h output where column positions shift based on filesystem name lengths. Note that --output is a GNU coreutils extension unavailable in default BSD/macOS df; this segment is written specifically for Linux servers, consistent with Part 3. The done < <(df -h ...) pattern employs process substitution introduced in section 7.4. It is chosen deliberately over standard piping (df ... | while read ...) because standard pipes execute the while loop inside a subshell, preventing variable changes to status from persisting outside the loop.
The generate_report function explicitly saves the exit status of check_disk into disk_status before invoking remaining functions, as $? captures only the exit status of the immediately preceding command. If evaluated after check_memory and check_top_processes, the stored exit code would reflect those routines rather than check_disk.
17.4.3 Verification and Testing
Grant execution permissions first, following procedures established in section 1.4.1, then execute without flags to verify terminal output.
chmod +x server-audit.sh
./server-audit.shTest with a low threshold to trigger [WARNING] lines, ensuring conditional comparison logic functions correctly prior to production deployment.
./server-audit.sh -t 1Save output to a log file via the -o option, then inspect the exit code separately.
./server-audit.sh -t 80 -o /tmp/audit-$(date +%Y%m%d).log
echo "Exit status: $?"An exit status of 0 indicates all filesystems remain below the set threshold, while 1 indicates at least one filesystem requires attention. If an invalid flag is passed, such as ./server-audit.sh -x, execution terminates immediately, sending Error: unknown option: -x to stderr, consistent with silent mode getopts error handling discussed in section 12.2.3. This basic script forms a working foundation for daily server checks; Chapter 18 builds on this foundation by automating execution schedules via cron and systemd timer routines.

