Intro
Every operator has experienced the moment when a production issue appears and the only tool available is a terminal. Bash is the common language of Linux and macOS environments, and knowing a focused set of basic commands with practical examples turns panic into a repeatable procedure. This guide is built for developers, DevOps consultants, and technical startup teams who need to diagnose, configure, verify, and recover services without guessing.
The goal is operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify the result, and document how to recover if the expected state is not reached. Each section below combines a concept with ready-to-run commands, expected output, and a decision point. Placeholders like APP_USER, SERVICE_NAME, or BACKUP_DIR must be replaced with values from your environment before execution. Never paste real credentials, tokens, private keys, or production identifiers into a command or script.
Version and Environment Inventory
Before touching a configuration file or restarting a service, you must know exactly what you are running. Start with read-only commands that reveal the operating system, kernel, Bash version, and available tools. This section shows how to collect that inventory in a structured way.
Prerequisites and Assumptions
- You can open a terminal as a non-root user with
sudorights for administrative commands. - You are working on Linux (commands are also relevant for macOS with minor differences).
- You understand the general layout of
/etc,/var, and/home.
Collecting System Facts
Run these commands one at a time and record the output. They do not modify anything.
# Operating system name and version
cat /etc/os-release
# Kernel release
uname -r
# Bash version
bash --version | head -n1
# Current user and groups
id
# Available disk space
df -h /
# Memory and swap
free -h
Expected output for cat /etc/os-release on Ubuntu 22.04 includes lines like:
PRETTY_NAME="Ubuntu 22.04.4 LTS"
VERSION_ID="22.04"
On Rocky Linux 9.3, PRETTY_NAME would show "Rocky Linux 9.3 (Blue Onyx)" and VERSION_ID="9.3".
For bash --version, the first line resembles:
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
If Bash is older than 4.0 on a system you plan to script heavily, note it; newer syntax like associative arrays may not work.
Identifying Running Services and Ports
Use ss or netstat to see what is listening. These are read-only observations.
# List listening TCP ports with process names (requires sudo for process info)
sudo ss -tulpn
# Alternatively, on older systems:
sudo netstat -tulpn
A typical line from ss -tulpn:
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
This tells you SSH is listening on port 22. If you see an unexpected listener on port 8080, investigate before making changes.
Recording Deployment Topology
Write down which services run on which host. A simple command to capture hostname and IP addresses:
hostname
ip -brief address
Sample output:
app-server-01
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0 UP 192.168.1.25/24 fe80::1/64
Store this inventory in a note or script for later reference. If a service fails, you can compare current state against the known baseline.
Safe Configuration Path
Once you understand the environment, you can make changes. The safe path means: back up the current file, edit with a clear comment, validate syntax, and restart only if verification fails. This section walks through editing a typical service configuration file with full recovery steps.
Back Up Before You Edit
Never edit a configuration file without a timestamped backup. Use cp with a date suffix:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%Y%m%d-%H%M%S)
For example, on February 3, 2025 at 14:30, the backup file becomes nginx.conf.bak-20250203-143000.
Verify the backup exists:
ls -l /etc/nginx/nginx.conf.bak-*
Expected output shows the original file size and timestamp.
Editing with a Purpose
Use vim, nano, or sed to make the smallest change. Suppose you need to change the worker processes in Nginx from auto to 2 because the server has only 2 CPU cores and you want predictable memory usage.
Before editing, view the current value:
grep -n "worker_processes" /etc/nginx/nginx.conf
Example output:
3:worker_processes auto;
Edit line 3 with sed (or use a visual editor):
sudo sed -i 's/worker_processes auto;/worker_processes 2;/' /etc/nginx/nginx.conf
Validate Syntax Before Applying
For Nginx, run:
sudo nginx -t
Expected success output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
If you see an error, revert to the backup immediately:
sudo cp /etc/nginx/nginx.conf.bak-20250203-143000 /etc/nginx/nginx.conf
sudo nginx -t
Reload or Restart Only When Needed
After a successful syntax test, reload Nginx to apply changes without dropping connections:
sudo systemctl reload nginx
If the service fails to reload or the change requires a full restart, use:
sudo systemctl restart nginx
Check status:
systemctl status nginx --no-pager
Look for Active: active (running) in the output. If not active, revert the config and restart again.
Rollback Plan
Always have a rollback plan before you make a change. For the Nginx example:
- Backup file path recorded.
- Revert command ready:
sudo cp /etc/nginx/nginx.conf.bak-20250203-143000 /etc/nginx/nginx.conf - Reload or restart command ready.
- Test URL or
curl -I http://localhostto confirm.
Verification and Diagnostics
After a change, you must verify the result. Diagnostics involve checking logs, processes, and network behavior to confirm the desired state. This section demonstrates how to check a web service after a configuration change, using Nginx as a concrete example.
Checking Service Status
systemctl is-active nginx
Expected output: active. If it says inactive or failed, investigate with:
systemctl status nginx --no-pager -l
journalctl -u nginx --since "10 minutes ago" --no-pager
The journal output will show error messages. For example, a common mistake is a missing semicolon in the config, which nginx -t would have caught. If you skipped validation, you will see something like:
nginx: [emerg] unexpected end of file, expecting ";" or "}" in /etc/nginx/nginx.conf:42
Verifying Network Response
Use curl to get headers and status code:
curl -I http://localhost
Expected output for success:
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 03 Feb 2025 14:35:00 GMT
Content-Type: text/html
If you get curl: (7) Failed to connect, check if the service is running and listening:
sudo ss -tulpn | grep :80
Should show a line with nginx and 0.0.0.0:80 or similar.
Following Logs in Real Time
For ongoing issues, tail the access and error logs:
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log
Send a test request from another terminal and watch the logs update.
Using Diagnostic Commands for Other Services
The same pattern applies to databases, message queues, or application servers. For example, to check if PostgreSQL is accepting connections after an edit to postgresql.conf:
pg_isready -h localhost -p 5432
Expected output: localhost:5432 - accepting connections. If it says no response, check the logs with journalctl -u postgresql and verify listen_addresses in the config.
Failure Modes and Recovery
Failures happen despite careful preparation. This section covers common failure scenarios, how to detect them, and how to recover using the backups and checks you already set up.
Scenario 1: Service Fails to Start After Configuration Change
Symptoms: systemctl start nginx fails, error in journal shows syntax or permission problem.
Immediate recovery:
- Revert to the last good backup:
sudo cp /etc/nginx/nginx.conf.bak-20250203-143000 /etc/nginx/nginx.conf
- Validate the reverted config:
sudo nginx -t
- Start the service:
sudo systemctl start nginx
- Verify with
curl -I http://localhost.
Scenario 2: Disk Full on Log Partition
Symptoms: Application writes fail, df -h shows 100% use on /var.
Observation:
df -h /var
Example output:
Filesystem Size Used Avail Use% Mounted on
/dev/sda3 20G 20G 0 100% /var
Immediate relief: Find large files:
sudo du -ah /var | sort -rh | head -n 10
If logs are the culprit, you can compress and truncate (after confirming they are not needed for forensics):
sudo tar -czf /tmp/old-logs-$(date +%Y%m%d).tar.gz /var/log/nginx/access.log*
sudo truncate -s 0 /var/log/nginx/access.log
Prevention: Set up logrotate with a policy like:
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -s /run/nginx.pid ] && kill -USR1 `cat /run/nginx.pid`
endscript
}
Scenario 3: Out of Memory (OOM) Kill
Symptoms: Process disappears from ps aux, dmesg shows OOM killer message.
Detection:
sudo dmesg -T | grep -i "out of memory"
Example output line:
[Mon Feb 3 14:50:12 2025] Out of memory: Killed process 1234 (node) total-vm:2048000kB, anon-rss:1800000kB, file-rss:0kB, shmem-rss:0kB
Recovery: Restart the service and monitor memory usage:
sudo systemctl restart myapp
free -h
Permanent fix: Adjust the service's memory limit in its systemd unit file under [Service] with MemoryMax=500M, then sudo systemctl daemon-reload and restart.
Operations Checklist
Use this checklist before and after any operational task. It is designed to be printed or kept in a terminal-friendly text file.
Pre-Change Checklist
| Item | Command or Action | Expected Result | Owner | Review Frequency |
|---|---|---|---|---|
| Record current date and time | date | Current timestamp, e.g., Mon Feb 3 14:30:00 UTC 2025 | Operator on duty | Every change |
| Identify service and version | nginx -v | nginx version: nginx/1.18.0 | Operator on duty | Every change |
| Take configuration backup | sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%Y%m%d-%H%M%S) | Backup file present with correct timestamp | Operator on duty | Every change |
| Note current service status | systemctl status nginx --no-pager -l | Active: active (running) | Operator on duty | Every change |
| Define expected outcome | Write in change ticket | e.g., "Nginx worker_processes set to 2, service still active" | Change requester (named person) | Before execution |
| Prepare rollback command | Write in change ticket | Exact revert command ready | Change requester | Before execution |
Post-Change Checklist
| Item | Command or Action | Expected Result | Owner | Review Frequency |
|---|---|---|---|---|
| Validate configuration | sudo nginx -t | syntax is ok and test is successful | Operator on duty | Immediately after change |
| Reload or restart service | sudo systemctl reload nginx | No error output | Operator on duty | Immediately after change |
| Verify service active | systemctl is-active nginx | active | Operator on duty | Immediately after change |
| Check logs for errors | sudo journalctl -u nginx --since "5 minutes ago" --no-pager | No new error lines | Operator on duty | Within 5 minutes |
| Confirm functionality | curl -I http://localhost | HTTP/1.1 200 OK | Operator on duty | Within 5 minutes |
| Update change ticket with result | Document in ticket system | Clear success or rollback note | Change requester | Within 15 minutes |
Who Owns What
Assign a single accountable owner for each major decision. For example, the Engineering Lead (e.g., Priya Shah) approves configuration changes to production. The on-call operator (e.g., Leo Martinez) executes the change. The change requester (e.g., an application developer, such as Anika Patel) verifies the functional result. These roles and names should be recorded in your team's runbook. Review the runbook monthly, or after any incident, whichever comes first.
Common Pitfalls and How to Avoid Them
Even experienced operators make mistakes. Here are the most frequent Bash and system administration pitfalls, why they happen, and how to prevent or recover from them.
Pitfall 1: Editing Configuration Without a Backup
Why it happens: The change seems trivial, or the operator is under time pressure.
Consequence: If the edit breaks the service, you have no quick way to revert.
How to avoid: Always run the backup command before editing. Make it a habit: sudo cp file file.bak-$(date +%s).
Recovery: If you forget, you might still find a copy in /var/backups or in a configuration management tool like Ansible or Puppet. Otherwise, you must reconstruct the file from memory or documentation.
Pitfall 2: Using rm -rf with Variables That Are Empty
Why it happens: A script uses a variable for a directory path, and the variable is unset or empty because of a typo or logic error.
Example dangerous command:
rm -rf "$TARGET_DIR/"
If TARGET_DIR is empty, this becomes rm -rf / (or a dangerous variant) and can wipe the system.
How to avoid: Always check that the variable is non-empty before using it:
if [ -z "$TARGET_DIR" ]; then
echo "TARGET_DIR is empty, aborting"
exit 1
fi
rm -rf "$TARGET_DIR/"
Better yet, use find with -delete and explicit path.
Pitfall 3: Redirecting Output with > When You Meant to Append >>
Why it happens: Typing too quickly or misunderstanding shell redirection.
Consequence: You overwrite an existing file instead of adding to it, losing previous data.
Example: Suppose you want to add a line to a log file but accidentally use >.
How to avoid: Use >> for appending. If you need to write a new file, double-check the filename. You can also set set -o noclobber in your shell to prevent overwriting existing files with >. To override, use >|.
Pitfall 4: Not Quoting Variables in Scripts
Why it happens: Forgetting that filenames or arguments may contain spaces or special characters.
Example:
file=my report.txt
cat $file
This fails because the shell splits $file into my and report.txt.
How to avoid: Always quote variables: cat "$file". Use shellcheck to lint your scripts.
Pitfall 5: Running Commands as Root Without sudo When Required (or Overusing Root)
Why it happens: Convenience or lack of awareness of privilege requirements.
Consequence: Running as root increases the damage from mistakes. Not using sudo when needed results in permission denied errors.
How to avoid: Use a normal user account and sudo only for administrative commands. Configure sudo with specific allowed commands if possible.
Pitfall 6: Misinterpreting Exit Codes
Why it happens: Not checking the exit status of a command, assuming it succeeded.
Example:
sudo systemctl restart nginx
curl -I http://localhost
If the restart failed, the curl will likely fail too, but the operator might only look at the curl output and miss the root cause.
How to avoid: Always check exit codes:
sudo systemctl restart nginx
if [ $? -ne 0 ]; then
echo "Restart failed, aborting"
exit 1
fi
curl -I http://localhost
Or use systemctl restart nginx && curl -I http://localhost.
Conclusion
Bash basic commands with practical examples become useful only when each recommendation is version-scoped, observable, and reversible where the technology permits. Copying a command without checking prerequisites and expected output is not an operations procedure; it is a gamble.
As a next step, choose one low-risk verification from the Operations Checklist and execute it in your environment. For example, pick a running service like Nginx, record its current configuration backup, make a harmless comment change, validate, reload, and verify with curl -I http://localhost. Then document the rollback steps. Review the output with a colleague, and store the runbook in your team wiki.
A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision. Practice these commands in a staging environment first, and you will be ready when production demands calm, competent action.