Every Linux admin has a moment where ls -la stops looking like alphabet soup and starts looking like a permission string, an owner, and a group. That moment usually costs a few blown deploys and one embarrassing chmod 777 first. This guide skips the pain and groups the essential commands by the job they do: navigating the filesystem, managing processes, installing packages, reading logs, and diagnosing the network, plus the permission mistakes that generate the most support tickets.
I tested every command here on Ubuntu 24.04 LTS and Rocky Linux 9.x, and noted where Debian, Fedora, and Arch do things differently. You get the exact syntax, the flags worth remembering, and the output you should actually see, so you know when something’s gone sideways.
What This Guide Covers
These are the small set of GNU coreutils, procps, systemd, and iproute2 utilities that cover most daily sysadmin work. ls, cd, and find move you around the filesystem. chmod and chown control access. ps, top, and kill manage what’s running. systemctl controls services. journalctl, tail, and grep read logs. ip, ss, and ping diagnose networking. That’s the core toolkit. Everything else is a variation on these themes.
These are POSIX/GNU/Linux userland utilities, and they’ve been stable for decades. The syntax below applies broadly across Ubuntu, Debian, RHEL/CentOS/Rocky, Fedora, and Arch on any modern kernel. Where a distro’s package manager differs, I’ll call it out explicitly.
Before You Begin
Make sure you have:
- Access to a Linux environment: a native install, a virtual machine, WSL2, or SSH access to a remote server
- A non-root user account with
sudoprivileges (never do daily work logged in asroot) - Basic comfort with opening a terminal and typing commands (this guide assumes that baseline)
- A test service you can safely restart or inspect,
nginx,sshd, orcronall work well for practice
| Requirement | Details |
|---|---|
| OS | Ubuntu 24.04 LTS, Debian 12, RHEL/Rocky 9.x, Fedora 40, or Arch (rolling) |
| Shell | Bash (default on most distros) or Zsh |
| Privileges | sudo access for package management, service control, and ownership changes |
| Network | Outbound internet access for package installs (or a local mirror) |
If you’re managing bare-metal servers in a rack, put a UPS on that rack. It matters more once you rely on systemctl and journal logs, because a dirty power cut mid-write is a much uglier problem than any permissions mistake covered here.
Getting a Linux Shell on Any Platform
You need an actual Linux shell to follow along. Commands like systemctl, journalctl, ip, and ss are Linux-specific and won’t run natively on Windows or macOS.
Windows
Windows doesn’t run Linux commands natively, so install Windows Subsystem for Linux 2 (WSL2). Open PowerShell or Windows Terminal as Administrator and run:
wsl --install
Installing: Virtual Machine Platform
Installing: Windows Subsystem for Linux
Installing: Ubuntu
The requested operation is successful. Changes will not be effective until the system is rebooted.
Reboot when prompted. After the reboot, launch Ubuntu from the Start menu. On first run, it asks you to create a UNIX username and password. This account gets sudo by default.
Once WSL2 is set up, every command here runs exactly as it would on bare-metal Linux.
macOS
macOS ships a Unix-like Terminal with zsh as the default shell, and it shares many coreutils commands with Linux (ls, cd, find, chmod, chown, grep, ping). Open it with Cmd+Space, type Terminal, and press Enter.
Here’s the catch: macOS is BSD-based, not Linux. systemctl and journalctl don’t exist here at all. macOS uses launchctl instead, and there’s no direct equivalent to ss (it ships an older netstat, not iproute2). For the systemd- and iproute2-specific sections below, run a Linux virtual machine (UTM, Parallels, or VirtualBox), spin up a Docker container on a Linux base image, or SSH into a remote Linux box.
Web (Browser-Based Terminal)
If you manage a remote server or homelab box and don’t want to open a local terminal, a browser-based SSH client gives you a full Linux shell without installing anything. Self-hosted tools like Wetty or ttyd, or a hosting provider’s built-in web console, all work the same way: they proxy an SSH session into your browser tab.
Once connected, the shell behaves identically to a native terminal. Every command below runs the same way.
Step-by-Step Guide
Step 1: Navigate the Filesystem (ls, cd, find)
These three commands are where every session starts. cd moves you, ls shows you what’s there, and find locates things you can’t see from where you’re standing.
cd /var/www
ls -la
total 24
drwxr-xr-x 4 www-data www-data 4096 Sep 12 09:14 .
drwxr-xr-x 14 root root 4096 Sep 10 17:02 ..
drwxr-xr-x 3 www-data www-data 4096 Sep 12 09:14 html
-rw-r–r– 1 root root 220 Sep 10 17:02 .env
The -l flag gives you the long listing (permissions, owner, group, size, date); -a includes dotfiles like .env. That first column, drwxr-xr-x, is the permission string you’ll read constantly. The next two columns are the owner (www-data) and group (www-data).
For finding files by name, type, or age instead of scrolling through directories:
find /var/log -name "*.log" -mtime -1
/var/log/nginx/access.log
/var/log/auth.log
-mtime -1 finds files modified in the last day. That’s the fastest way to narrow down what changed right before an incident. Add -type f to restrict to regular files, or -type d for directories only. find is slower than ls on large trees because it walks the filesystem recursively, so scope it to a directory you actually care about rather than running it from / and going for coffee while it churns.
Step 2: Control File Permissions and Ownership (chmod, chown)
This is where most new Linux admins get tripped up. Every file has an owner, a group, and a permission mode expressed as three sets of read/write/execute bits (owner, group, others).
ls -l deploy.sh
-rw-r–r– 1 akishore developers 842 Sep 12 08:30 deploy.sh
That file has no execute bit, so trying to run it fails:
./deploy.sh
bash: ./deploy.sh: Permission denied
Fix it with chmod:
chmod +x deploy.sh
ls -l deploy.sh
-rwxr-xr-x 1 akishore developers 842 Sep 12 08:30 deploy.sh
For numeric mode, the digits map to read (4), write (2), and execute (1) per owner/group/others:
| Octal | Permissions | Meaning |
|---|---|---|
755 | rwxr-xr-x | Owner: full control; group/others: read + execute (typical for scripts, binaries) |
644 | rw-r--r-- | Owner: read/write; group/others: read-only (typical for regular files) |
750 | rwxr-x--- | Owner: full control; group: read + execute; others: nothing (private service directories) |
600 | rw------- | Owner-only read/write (SSH keys, credential files) |
chmod 750 /opt/myapp/config/
Ownership is separate from mode. chown sets who the owner and group actually are:
sudo chown deploy:developers /opt/myapp/config/
ls -ld /opt/myapp/config/
drwxr-x— 2 deploy developers 4096 Sep 12 08:41 /opt/myapp/config/
chown user:group path sets both in one command. Use -R to apply recursively to a directory tree, but be deliberate about it. Recursive ownership changes on the wrong path can lock out services that expect specific file owners, and figuring out why an app suddenly can’t read its own config at 2 AM is nobody’s idea of fun.
Warning: Never run
chown -Rorchmod -Ragainst a root-level path like/or/etcwithout double-checking the target directory. A misplaced trailing slash or wildcard can break system-critical file ownership and require a rescue boot to fix.
Step 3: Inspect and Manage Running Processes (ps, top, kill)
When something is eating CPU or memory, ps and top are how you find out what and kill is how you stop it.
ps aux --sort=-%cpu | head -10
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
www-data 4821 98.2 3.1 812340 128900 ? R 09:41 4:12 php-fpm: pool www
mysql 1122 4.5 12.4 1893200 512400 ? Sl Sep10 142:07 mysqld
akishore 9932 0.1 0.2 21344 8100 pts/0 R+ 09:52 0:00 ps
aux shows every process (a), including ones without a controlling terminal (x), with user-oriented output (u). --sort=-%cpu puts the heaviest CPU consumer at the top. That runaway php-fpm worker at 98.2% CPU is the one to investigate first.
For a live, refreshing view instead of a snapshot:
top
top – 09:53:14 up 12 days, 3:41, 2 users, load average: 2.14, 1.98, 1.55
Tasks: 187 total, 2 running, 185 sleeping, 0 stopped, 0 zombie
%Cpu(s): 61.2 us, 8.1 sy, 0.0 ni, 29.4 id, 0.8 wa, 0.0 hi, 0.5 si, 0.0 st
MiB Mem : 15872.0 total, 2104.3 free, 9821.6 used, 3946.1 buff/cachePID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4821 www-data 20 0 812340 128900 41200 R 98.2 3.1 4:12.44 php-fpm
1122 mysql 20 0 1893200 512400 28900 S 4.5 12.4 142:07.90 mysqld
Once you’ve identified the offending PID, stop it:
kill 4821
kill without a signal number sends SIGTERM (15), a polite request to shut down that lets the process clean up open files and connections. If it ignores that after a few seconds:
kill -9 4821
Warning:
kill -9sendsSIGKILL, which the process can’t intercept or ignore. It terminates immediately with no cleanup. Use it as a last resort, and check why the process hung in the first place (often a database lock, a stuck network call, or a memory leak) rather than treatingkill -9as routine maintenance.
Step 4: Control Services with systemctl
Most modern distros run systemd as the init system, and systemctl is how you start, stop, enable, and inspect services.
sudo systemctl status nginx
● nginx.service – A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-09-12 08:02:11 UTC; 1h 51min ago
Docs: man:nginx(8)
Main PID: 1043 (nginx)
Tasks: 5 (limit: 4683)
Memory: 12.4M
CPU: 891ms
CGroup: /system.slice/nginx.service
├─1043 “nginx: master process /usr/sbin/nginx -g daemon on;”
└─1044 “nginx: worker process”
The Active: line is the first thing to check: active (running), inactive (dead), or failed. enabled means it starts automatically at boot; disabled means it doesn’t. To change either:
sudo systemctl restart nginx
sudo systemctl enable nginx
sudo systemctl disable nginx
When a service shows failed, status output is often truncated. Get the full picture with journalctl (next section) instead of guessing from the status snippet alone.
Step 5: Install Software on Any Distro
The single most common “command not found” mistake for new Linux admins is running the wrong package manager for the distro. Check which distro you’re on first:
cat /etc/os-release
PRETTY_NAME=”Ubuntu 24.04.2 LTS”
NAME=”Ubuntu”
VERSION_ID=”24.04″
ID=ubuntu
ID_LIKE=debian
Then use the matching syntax:
| Distro family | Update index | Install package | Remove package | Search |
|---|---|---|---|---|
| Debian/Ubuntu | sudo apt update | sudo apt install nginx | sudo apt remove nginx | apt search nginx |
| RHEL/Rocky/Fedora | sudo dnf check-update | sudo dnf install nginx | sudo dnf remove nginx | dnf search nginx |
| Older RHEL/CentOS | sudo yum check-update | sudo yum install nginx | sudo yum remove nginx | yum search nginx |
| Arch | sudo pacman -Syu | sudo pacman -S nginx | sudo pacman -R nginx | pacman -Ss nginx |
sudo apt update && sudo apt install -y htop
Reading package lists… Done
Building dependency tree… Done
The following NEW packages will be installed:
htop
Setting up htop (3.3.0-4build1) …
apt update refreshes the local package index from configured repositories. It doesn’t upgrade anything by itself, which trips people up constantly. -y auto-confirms the install prompt, fine for scripted setups but worth skipping when you want to review what’s about to change on a production box.
Step 6: Search Logs Efficiently (journalctl, tail, grep)
When a service fails, journalctl -u <service> gives you the full startup and runtime log for that specific unit. It’s far more useful than the truncated snippet in systemctl status.
sudo journalctl -u nginx -xe
Sep 12 09:58:03 web01 systemd[1]: Starting nginx.service…
Sep 12 09:58:03 web01 nginx[5210]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Sep 12 09:58:03 web01 systemd[1]: nginx.service: Failed with result ‘exit-code’.
-x adds explanatory context for common errors; -e jumps to the end of the log. Narrow by time instead of scrolling:
sudo journalctl -u nginx --since "10 min ago"
sudo journalctl --since "2026-09-12 08:00" --until "2026-09-12 09:00"
For plain-text log files that predate systemd’s journal (or apps that log outside it), tail and grep are still the workhorses:
tail -f /var/log/nginx/error.log | grep -i "error"
2026/09/12 09:58:04 [error] 5211#5211: *1 connect() failed (111: Connection refused) while connecting to upstream
tail -f follows the file live as new lines are appended, useful while you reproduce an issue in another window. grep -i matches case-insensitively. For more context around a match:
grep -A 5 -B 5 "Connection refused" /var/log/nginx/error.log
-A 5 shows 5 lines after the match, -B 5 shows 5 lines before. That’s usually enough to see what led up to the error without scrolling the whole file.
Step 7: Check Network Connectivity (ip, ss, ping)
ip replaced the older ifconfig/route tools on modern distros. Start with interfaces and addresses:
ip addr show
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 3c:97:0e:8a:11:20 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.42/24 brd 192.168.1.255 scope global dynamic eth0
valid_lft 82394sec preferred_lft 82394sec
Then check the routing table:
ip route
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.42 metric 100
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.42
An interface can show state UP at the link layer while still lacking a valid address or route. That’s why you check both. If there’s no default via line, the box has no gateway configured and can’t reach anything outside its subnet.
Next, check what’s actually listening on the box:
sudo ss -tulpn
Netid State Local Address:Port Peer Address:Port Process
tcp LISTEN 0.0.0.0:22 0.0.0.0:* users:((“sshd”,pid=812,fd=3))
tcp LISTEN 0.0.0.0:80 0.0.0.0:* users:((“nginx”,pid=1043,fd=6))
tcp LISTEN 127.0.0.1:3306 0.0.0.0:* users:((“mysqld”,pid=1122,fd=21))
-t shows TCP, -u shows UDP, -l restricts to listening sockets, -p shows the owning process (requires sudo), and -n shows numeric ports instead of resolving service names. Notice that mysqld is bound to 127.0.0.1 only. It’s not reachable from other hosts on the network, which is usually intentional for a database.
Finally, test basic reachability:
ping -c 4 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=11.2 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=10.8 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=11.5 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=115 time=10.9 ms— 8.8.8.8 ping statistics —
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 10.8/11.5/11.2/0.3 ms
-c 4 sends exactly 4 packets and stops (without it, ping runs indefinitely until you hit Ctrl+C). If pinging an IP address works but a hostname doesn’t, the problem is DNS, not connectivity. Narrow it down with ping 8.8.8.8 first, then ping google.com, and compare results. On a homelab network, this same sequence is how you’d confirm a 2.5GbE switch is actually passing traffic at full speed between two hosts, rather than quietly negotiating down to 1Gb and letting you find out the hard way during a file transfer.
Configuration: Settings Worth Knowing
| Setting | Command | Default | Notes |
|---|---|---|---|
| Default file mode | umask | 022 (files: 644, dirs: 755) | Set in /etc/profile or ~/.bashrc for per-user defaults |
| Service auto-start | systemctl enable <unit> | Varies by package | Doesn’t start the service now, pair with start or use enable --now |
| Journal retention | journalctl --vacuum-time=7d | Unbounded (grows until disk fills) | Cap journal size on small root partitions |
| Firewall state | ufw status (Debian/Ubuntu) or firewall-cmd --state (RHEL) | Inactive on fresh installs | Check before assuming ss output means external reachability |
sudo systemctl enable --now nginx
--now combines enabling at boot with starting the service immediately. It’s the single command most admins actually want instead of running enable and start separately.
Tips and Troubleshooting
Permission denied when running a script
Cause: The execute bit isn’t set for the user running it.
Fix: ls -l script.sh to confirm the current mode, then chmod +x script.sh. Don’t reach for chmod 777 as a reflex. That grants write and execute access to every user on the system, which is a real security risk on multi-user boxes and a red flag in any security audit.
Changed ownership but the app still can’t read the file
Cause: You changed the user owner but not the group, or a parent directory is missing the execute bit needed to traverse into it.
Fix: Use chown user:group file to set both in one shot, and check the full path with ls -ld /path/to/parent/dir. A directory without execute permission blocks access to everything inside it, even if the file itself looks fine.
systemctl status shows failed with no useful detail
Cause: The status output truncates to a few lines by design.
Fix: Run journalctl -u servicename -xe or journalctl -u servicename --since "10 min ago" for the full startup log, which almost always contains the actual error (a port conflict, a missing config file, a permissions issue).
kill doesn’t stop a hung process
Cause: Default kill sends SIGTERM, which a misbehaving process can ignore entirely.
Fix: Escalate to kill -9 PID for SIGKILL, which the kernel enforces immediately. Investigate the root cause afterward. A process that ignores SIGTERM usually points to a stuck I/O call or deadlock worth fixing rather than working around.
Wrong package manager for the distro
Cause: apt doesn’t exist on RHEL-based systems, and dnf/yum don’t exist on Debian-based ones.
Fix: Run cat /etc/os-release first to confirm the distro family, then match the table in Step 5.
grep returns too much or too little
Cause: Default grep shows only the matching line with zero surrounding context.
Fix: Add -A N -B N for lines after/before a match, -i for case-insensitive search, and pipe tail -f into grep for live filtering of a growing log file.
Wrapping Up
Once these commands are organized by task instead of by name, troubleshooting stops feeling random. A permissions error sends you to ls -l and chmod/chown. A slow server sends you to top. A dead service sends you to journalctl -u. A connectivity issue sends you to ip and ping, in that order. That mental map matters more than memorizing every flag ever printed in a man page.
If there’s one habit worth building early, it’s resisting the urge to reach for chmod 777 or kill -9 as a first response. Both work. Both usually mask a problem you’ll run into again in a week, at a worse time, on a server you care about more.
| Step | Action | Applies To |
|---|---|---|
| 1 | Navigate and inspect with ls, cd, find | All distros |
| 2 | Fix permissions with chmod/chown, avoid 777 | All distros |
| 3 | Find and stop processes with ps, top, kill | All distros |
| 4 | Control services with systemctl | systemd-based distros |
| 5 | Install software with apt/dnf/pacman | Distro-specific |
| 6 | Search logs with journalctl, tail, grep | systemd + traditional logs |
| 7 | Diagnose networking with ip, ss, ping | All distros |