普通视图

发现新文章,点击刷新页面。
昨天 — 2026年5月30日Linux Tips, Tricks and Tutorials on Linuxize

iptables Command in Linux: Manage Firewall Rules

When a packet arrives at a Linux machine, the kernel decides what to do with it based on a set of firewall rules. Those rules live in the kernel’s Netfilter framework, and for more than two decades the standard way to edit them from user space has been the iptables command.

iptables controls packet filtering, network address translation, and packet mangling. Higher-level front ends such as ufw and firewalld manage the same Netfilter firewall stack through simpler interfaces, although many modern systems use nftables underneath. Understanding the underlying command is still valuable, especially when you inherit a server that was set up by someone else. This guide walks through the concepts and the commands you need to read, edit, and persist firewall rules.

Tables and Chains

Before you write a rule, you need to know where it goes. iptables is organized into tables, and each table contains chains.

The three tables you will use most often are:

  • filter - the default table, used for allowing and blocking traffic
  • nat - used for network address translation, such as port forwarding and masquerading
  • mangle - used to alter packet headers, for example to set QoS marks

Each table has a set of built-in chains that correspond to moments in the life of a packet. In the filter table:

  • INPUT - packets destined for the local machine
  • OUTPUT - packets originating from the local machine
  • FORWARD - packets routed through the machine

A rule says: for packets that enter this chain and match these criteria, take this action. The action is called a target and is usually ACCEPT, DROP, REJECT, or the name of another chain.

iptables Syntax

The general form of the command is:

txt
iptables [-t TABLE] COMMAND CHAIN [MATCH] [-j TARGET]

If -t is omitted, iptables uses the filter table. Common commands include -A (append a rule), -I (insert), -D (delete), -L (list), -F (flush), and -P (set default policy).

All commands that change the firewall require root privileges. Run them with sudo or as root.

Warning
It is easy to lock yourself out of a remote server with a single wrong rule. Before you apply a restrictive ruleset over SSH, either test on a local machine first or use iptables-apply, which rolls back automatically if you lose access.

List Rules

To print every rule in the filter table, use the -L option:

Terminal
sudo iptables -L

The default output shows service names, resolves IP addresses, and hides packet and byte counters. For real work, add -n to keep numeric output and -v to show counters and interface information:

Terminal
sudo iptables -L -n -v
output
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
1234 98K ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
0 0 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:23
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

To list a single chain, append its name:

Terminal
sudo iptables -L INPUT -n -v

To number the rules so you can reference them by index when deleting, add --line-numbers:

Terminal
sudo iptables -L INPUT -n -v --line-numbers

Add and Remove Rules

New rules are added to the end of a chain with -A (append) or at a specific position with -I (insert). The difference matters because iptables evaluates rules top to bottom and stops at the first match.

To allow incoming SSH connections:

Terminal
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

To allow HTTP and HTTPS:

Terminal
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

To insert a rule as the first one in the chain, use -I CHAIN 1:

Terminal
sudo iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT

This is important when you are working over SSH. If a broad DROP rule already appears earlier in the chain, appending the accept rule after it would not help because the packet would be dropped first.

To delete a rule, either repeat the exact specification with -D:

Terminal
sudo iptables -D INPUT -p tcp --dport 23 -j DROP

Or delete by line number, which is easier when the rule has many options:

Terminal
sudo iptables -D INPUT 3

Allow and Block Specific IPs

To block all traffic from a single IP address:

Terminal
sudo iptables -A INPUT -s 203.0.113.10 -j DROP

To block a range using CIDR notation:

Terminal
sudo iptables -A INPUT -s 203.0.113.0/24 -j DROP

To allow SSH only from a trusted subnet:

Terminal
sudo iptables -A INPUT -p tcp -s 192.168.1.0/24 --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j DROP

The first rule accepts SSH from the local subnet. The second drops SSH from everywhere else. Order matters: if you reversed the two lines, every SSH attempt would be dropped before the accept rule had a chance to match.

Allow Established Connections

Most firewall setups include a rule that accepts traffic belonging to an already established connection. This lets return traffic through without needing a matching rule for each outbound request:

Terminal
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Put this rule near the top of the INPUT chain so it matches early. Without it, the default DROP policy breaks outbound connections that expect responses.

Set a Default Policy

Each built-in chain has a default policy that applies when no rule matches. The -P option changes it:

Terminal
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

Switching INPUT to DROP is the foundation of a deny-by-default firewall: nothing gets in unless an explicit rule allows it. Before you flip the policy, make sure you have already added the rules that allow SSH, established connections, and anything else you need.

Flush Rules

To remove every rule from every chain in the current table:

Terminal
sudo iptables -F

To flush a specific chain only:

Terminal
sudo iptables -F INPUT

Flushing does not reset the default policies. If you have set INPUT to DROP, flushing will leave it at DROP with no rules, which blocks all inbound traffic. Reset the policy to ACCEPT first if that is not what you want:

Terminal
sudo iptables -P INPUT ACCEPT
sudo iptables -F

Save and Restore Rules

Rules added with iptables live in kernel memory only. They disappear on reboot unless you save them.

On Ubuntu, Debian, and Derivatives, the iptables-persistent package saves rules to /etc/iptables/rules.v4 and reloads them at boot:

Terminal
sudo apt install iptables-persistent

The installer asks whether to save the current rules. To update the saved copy later:

Terminal
sudo netfilter-persistent save

On Fedora, RHEL, and Derivatives, the equivalent service is iptables-services:

Terminal
sudo dnf install iptables-services
sudo systemctl enable --now iptables
sudo service iptables save

Independent of the distribution, you can dump and restore rules manually with iptables-save and iptables-restore:

Terminal
sudo iptables-save -f /etc/iptables/rules.v4
sudo iptables-restore /etc/iptables/rules.v4

This is also the recommended way to edit a large ruleset: save to a file, edit the file, then restore it atomically.

Troubleshooting

Rules disappear after a reboot
iptables rules are not persistent by default. Install iptables-persistent on Debian-based systems or iptables-services on RHEL-based ones, and save the ruleset.

SSH stops working after setting a DROP policy
You switched INPUT to DROP without an ACCEPT rule for port 22, or the ACCEPT rule is positioned after a more general DROP rule. Connect through the console, add the rule with -I INPUT 1, and save.

A rule looks correct but does not match
Check the order. iptables walks the chain top to bottom and stops at the first match, so an earlier accept or drop may be catching the packet first. Use iptables -L INPUT -n -v --line-numbers to inspect the order.

Changes are silently ignored
You may be editing the wrong table. A rule in filter does not affect NAT, and vice versa. Pass -t TABLE explicitly when you are not working in filter.

iptables: command not found
On some modern distributions, only nftables is installed by default. Install iptables with your package manager, or use nft directly.

Quick Reference

For a printable quick reference, see the iptables cheatsheet .

Action Command
List rules (verbose, numeric) iptables -L -n -v --line-numbers
Allow port iptables -A INPUT -p tcp --dport PORT -j ACCEPT
Block IP iptables -A INPUT -s IP -j DROP
Allow established connections iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Insert rule at top iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT
Delete rule by number iptables -D INPUT N
Set default policy iptables -P INPUT DROP
Flush all rules iptables -F
Save rules iptables-save -f /etc/iptables/rules.v4
Restore rules iptables-restore /etc/iptables/rules.v4

FAQ

Is iptables still relevant in 2026?
It is still installed and widely used, but it is being replaced by nftables, which offers a cleaner syntax and better performance. On recent distributions, the iptables command is often a compatibility front end that writes nftables rules underneath.

Should I use iptables, ufw, or firewalld?
If you are managing rules by hand on Debian or Ubuntu, ufw is simpler and covers most cases. On Fedora and RHEL, firewalld is the default. Reach for raw iptables when you need fine-grained control that the front ends do not expose, or when you are troubleshooting an existing ruleset.

What is the difference between DROP and REJECT?
DROP silently discards the packet; the sender sees a timeout. REJECT sends an ICMP error back (or a TCP reset for TCP), so the sender gets immediate feedback. DROP is often preferred on public interfaces because it does not confirm that the port exists.

How do I block a country or a list of IPs?
For a handful of addresses, add one -s rule per entry. For larger lists, use the ipset tool to manage the addresses and reference the set from a single iptables rule.

Does iptables handle IPv6?
No. Use ip6tables for IPv6 rules. It has the same syntax and the same tables and chains, but operates on a separate rule set.

Conclusion

iptables is a low-level but reliable way to read and shape the Linux firewall. Once you have a working ruleset, save it with iptables-save and commit the file so the next person to touch the server has a clear starting point.

昨天以前Linux Tips, Tricks and Tutorials on Linuxize

Network Bandwidth Monitoring Tools: iftop, nload, bmon, and vnstat

When a server feels slow or a connection is saturated, the first question is usually “what is using the bandwidth?” The answer depends on what you need to see: the total rate on an interface, the individual connections moving the most data, a graph across several interfaces, or how much you have transferred this month. No single tool does all of this well, so it helps to know which one fits each question.

This guide compares four command-line bandwidth monitors: nload, iftop, bmon, and vnstat. Each one is small, available in the default repositories, and built for a different angle on network usage.

nload: Per-Interface Throughput

nload is the simplest of the four. It shows the incoming and outgoing rate on a single interface in real time, with a small ASCII graph and running statistics. It answers the question “how fast is this interface moving data right now?”

Install it on Ubuntu, Debian, and Derivatives:

Terminal
sudo apt install nload

On Fedora, RHEL, and Derivatives, install it with dnf. On RHEL-compatible systems, enable EPEL first if the package is not available:

Terminal
sudo dnf install nload

Run it with no arguments to monitor the default interface, or name an interface directly:

Terminal
nload eth0

The display splits into incoming and outgoing sections, each showing the current rate along with the average, minimum, maximum, and total transferred:

output
Device eth0 [192.168.1.50] (1/1):
Incoming:
Curr: 4.21 MBit/s
Avg: 2.88 MBit/s
Max: 9.10 MBit/s
Ttl: 1.42 GByte
Outgoing:
Curr: 512.00 kBit/s
Avg: 320.10 kBit/s

When more than one interface is present, the left and right arrow keys cycle between them. Press q to quit. nload does not need root privileges, which makes it the quickest way to glance at raw throughput.

iftop: Bandwidth by Connection

nload tells you how much traffic is flowing, but not where it is going. iftop fills that gap. It works like top for the network, listing active connections sorted by bandwidth so you can see which hosts and ports are responsible for the load.

Install it on Ubuntu, Debian, and Derivatives:

Terminal
sudo apt install iftop

On Fedora, RHEL, and Derivatives, use dnf. On RHEL-compatible systems, enable EPEL first if the package is not available:

Terminal
sudo dnf install iftop

iftop needs to capture packets, so it must run as root. Point it at an interface:

Terminal
sudo iftop -i eth0

Each row shows a pair of hosts and the bandwidth between them, averaged over the last 2, 10, and 40 seconds:

output
 => 192.168.1.50 4.10Mb 3.85Mb 3.20Mb
192.0.2.14 <= 256Kb 210Kb 180Kb

By default iftop resolves IP addresses to hostnames and port numbers to service names, which can be slow and can clutter the view. Three flags make it more useful on a busy server: -n skips DNS hostname lookups, -N shows ports as numbers instead of service names, and -P displays the port column so you can tell which service is responsible:

Terminal
sudo iftop -i eth0 -nNP

Press q to quit. When you need to find the connection saturating a link, iftop is the tool to reach for. To then identify the local process behind a port, pair it with the ss command .

bmon: Multiple Interfaces at a Glance

bmon (bandwidth monitor) is the right choice when you want to watch several interfaces at once. It lists every interface with its current receive and transmit rates, and draws a live graph for the one you select, which is handy on routers, gateways, and virtualization hosts with many NICs and bridges.

Install it on Ubuntu, Debian, and Derivatives:

Terminal
sudo apt install bmon

On Fedora, RHEL, and Derivatives, use dnf. On RHEL-compatible systems, enable EPEL first if the package is not available:

Terminal
sudo dnf install bmon

Launch it with no arguments:

Terminal
bmon

The top pane lists all interfaces with their RX and TX rates. Use the arrow keys to highlight an interface, and the lower pane updates its graph to match. Press d to toggle detailed counters such as errors and dropped packets, and q to quit. bmon reads kernel counters and does not require root for basic monitoring.

vnstat: Historical Usage Over Time

The first three tools show what is happening now and forget it the moment you quit. vnstat is different: it runs as a background service, records traffic per interface into a database, and reports usage by hour, day, and month. It answers “how much data have I used this month?” rather than “what is the rate right now?”, which makes it the tool for usage caps and capacity planning.

Install it and enable the service on Ubuntu, Debian, and Derivatives:

Terminal
sudo apt install vnstat
sudo systemctl enable --now vnstat

On Fedora, RHEL, and Derivatives, install the package first. On RHEL-compatible systems, enable EPEL first if the package is not available:

Terminal
sudo dnf install vnstat
sudo systemctl enable --now vnstat

vnstat needs time to collect data, because it samples the interface counters periodically rather than capturing packets. After it has been running for a while, view a summary:

Terminal
vnstat
output
 eth0 since 2026-01-01
rx: 42.18 GiB tx: 8.04 GiB total: 50.22 GiB
monthly
rx | tx | total
------------------------+-------------+------------
2026-01 42.18 GiB | 8.04 GiB | 50.22 GiB

Several flags change the reporting period: vnstat -h for hourly, vnstat -d for daily, and vnstat -m for monthly. For a live rate display similar to the other tools, use vnstat -l, which shows the current throughput until you press Ctrl+C. Because the daemon keeps logging, the history survives reboots.

Which Tool to Use

The four tools overlap a little but each has a clear best use:

Tool Shows Needs root Best for
nload Per-interface in/out rate No A quick glance at total throughput
iftop Per-connection bandwidth Yes Finding which host or port is using the link
bmon All interfaces plus graphs No Hosts with many interfaces
vnstat Historical hourly/daily/monthly totals No Tracking usage over time and quotas

In practice, many administrators keep vnstat running for history and reach for iftop when they need to investigate a live spike.

Troubleshooting

command not found after installing
Confirm the package installed with apt list --installed | grep <tool> or dnf list installed <tool>. On Ubuntu, the package may be in the universe repository; on RHEL-compatible systems, you may need to enable EPEL first.

iftop shows no traffic
iftop must run as root and be pointed at the active interface. List interfaces with ip link and pass the correct one with -i. Traffic on the loopback interface will not appear unless you select lo.

vnstat reports “Not enough data available yet”
The daemon has not collected a full sampling interval. Confirm the service is running with systemctl status vnstat and wait a few minutes for the first data points.

Rates look wrong on a virtual interface
Bridges, bonds, and VLAN interfaces report aggregate counters. Monitor the underlying physical interface as well to see where the traffic actually enters the host.

FAQ

Which tool shows bandwidth per process?
None of these four map traffic to a process directly. iftop shows it per connection; to tie a connection to a process, use ss -tp or nethogs, which groups bandwidth by process.

Do these tools work over SSH?
Yes. All four are terminal applications and run fine in an SSH session, which is exactly how they are used on headless servers.

Does vnstat slow down the network?
No. vnstat reads interface counters periodically rather than inspecting packets, so its overhead is negligible even on busy links.

Can I monitor a specific interface only?
Yes. nload, iftop, and vnstat all accept an interface name (for example eth0), and bmon lets you select one from its list.

Conclusion

Pick the tool that matches the question: nload for a fast look at throughput, iftop to find the connection eating the link, bmon for many interfaces, and vnstat for usage history. When you need to move from bandwidth numbers to sockets and interface details, pair them with the lower-level ss and ip commands.

Bash set Command: set -e, set -x, and set -u Explained

By default, a bash script keeps running even after a command fails. A typo in a variable name expands to an empty string, a failed cd is ignored, and the script marches on as if nothing happened, often doing real damage by the time it stops. The set builtin changes that. With a few flags, you can make bash stop on the first error, treat unset variables as mistakes, and print each command as it runs.

This guide explains the set command and the three options you will use most: set -e, set -u, and set -x, plus pipefail and how to combine them into a strict mode.

What the set Command Does

set is a shell builtin that turns shell options on or off and sets the positional parameters. For options, it takes this form:

txt
set [options]

Each option has a short form and a long form. A leading dash turns an option on, and a leading plus turns it off, which is the reverse of what most people expect:

  • set -e enables an option (here, exit on error).
  • set +e disables it.
  • set -o errexit is the long form of set -e.
  • set +o errexit is the long form of set +e.

You place these near the top of a script so they apply to everything that follows, or around a specific block when you only want them in part of the script.

set -e: Exit on Error

set -e (also written set -o errexit) tells bash to exit immediately if any command returns a non-zero status. It stops a script from continuing past a failure.

Consider a script that changes into a directory and then writes a log message:

cleanup.shsh
#!/bin/bash
set -e

cd /var/cache/myapp
printf 'Cleaning application cache\n'

Without set -e, if the cd fails because the directory does not exist, the script would continue and run the next command in whatever directory it started in. With set -e, the failed cd stops the script before the log message or any later cache command runs.

There are important exceptions where set -e does not trigger an exit, and they trip people up. A command does not cause an exit when it is:

  • part of an if, while, or until test,
  • joined with && or ||,
  • preceded by !.

This is by design, so that you can test a command’s exit status. If you need a specific command’s failure to be tolerated, append || true:

sh
grep "pattern" file.txt || true

set -u: Treat Unset Variables as Errors

set -u (or set -o nounset) makes bash exit when you reference a variable that has not been set. This catches one of the most common scripting bugs: a typo in a variable name that silently expands to nothing.

deploy.shsh
#!/bin/bash
set -u

target_dir="/srv/www"
printf 'Target cache directory: %s\n' "${tagret_dir}/cache"

The variable name is misspelled as tagret_dir. Without set -u, ${tagret_dir} expands to an empty string, and the command prints /cache instead of /srv/www/cache. With set -u, bash stops with an error instead:

output
deploy.sh: line 5: tagret_dir: unbound variable

When a variable is legitimately optional, provide a default with ${VAR:-default} so the reference is always defined:

sh
echo "Deploying to ${ENVIRONMENT:-staging}"

set -x: Trace Each Command

set -x (or set -o xtrace) prints every command to standard error before it runs, with the expanded values of any variables. It is the fastest way to see exactly what a script is doing without adding echo statements everywhere.

greet.shsh
#!/bin/bash
set -x

name="Linuxize"
echo "Hello, $name"

Running the script shows each command, prefixed with a +, alongside its normal output:

output
+ name=Linuxize
+ echo 'Hello, Linuxize'
Hello, Linuxize

The trace shows the variable already expanded, so you see the real value bash used. The prefix comes from the PS4 variable. Set it to include the script name and line number, which is invaluable in longer scripts:

sh
PS4='+ ${BASH_SOURCE}:${LINENO}: '

Because tracing is noisy, it is common to enable it only around the section you are debugging with set -x and then turn it off again with set +x.

set -o pipefail: Catch Failures in a Pipeline

By default, a pipeline returns the exit status of its last command, so a failure earlier in the pipe is hidden. In the pipeline below, wc succeeds even though grep failed because the file does not exist:

sh
grep "ERROR" missing.log | wc -l

Without pipefail, the pipeline exits with the status from wc, which is 0. set -o pipefail changes this so the pipeline returns the status of the rightmost command that failed, or 0 if every command succeeded. Combined with set -e, it makes a failed first stage actually stop the script.

Combine Them into a Strict Mode

These options are most useful together. The common combination at the top of a careful script is:

script.shsh
#!/bin/bash
set -euo pipefail

This enables exit-on-error, unset-variable checking, and pipeline failure detection in one line. Add x as set -euxo pipefail while debugging, then remove it. This pattern is the foundation of what is often called “bash strict mode”, which our bash strict mode guide covers in more depth, including the trade-offs of set -e.

Enable and Disable Options for a Block

You do not have to apply an option to the whole script. Turn one on for a block and off afterward. This is common with tracing:

sh
set -x
deploy_application
sync_assets
set +x

Only the two commands between set -x and set +x are traced. The same pattern works with set -e when you have a section where you want to handle errors manually.

View the Current Options

Running set -o with no flag prints every option and whether it is on or off. The full list has more than two dozen entries; the four options discussed here are:

Terminal
set -o
output
errexit off
nounset off
pipefail off
xtrace off

This is a quick way to confirm which options an interactive shell or a sourced script has enabled.

Set Positional Parameters

The set command has a second job unrelated to options: when given arguments after --, it replaces the positional parameters $1, $2, and so on. This is useful when you want to reset the arguments a script or function reads:

sh
set -- one two three
echo "$2"
output
two

The -- marks the end of options so that arguments starting with a dash are not mistaken for flags.

Quick Reference

For a printable quick reference, see the Bash cheatsheet .

Option Long form Effect
set -e set -o errexit Exit immediately on a command failure
set -u set -o nounset Error when an unset variable is used
set -x set -o xtrace Print each command before running it
set -o pipefail set -o pipefail Fail a pipeline if any command in it fails
set +e set +o errexit Disable an option (plus instead of dash)
set -o List all options and their state
set -- a b c Set the positional parameters

FAQ

What is the difference between set -e and set -o errexit?
They are the same thing. set -e is the short form and set -o errexit is the long, more readable form. Use whichever you prefer; long forms are common in shared scripts because they are self-documenting.

Why does my script still continue after a command fails with set -e?
The failed command is probably part of an if test, joined with && or ||, or preceded by !. In those positions bash deliberately ignores the failure so you can test exit status. A command substitution or a function call can also mask the failure.

What does the plus sign do, as in set +x?
A plus disables an option that a dash would enable. set -x turns tracing on and set +x turns it off, so you can scope an option to part of a script.

Should I always use set -euo pipefail?
It is a good default for new scripts and catches many bugs early, but set -e in particular has surprising edge cases. For complex scripts, understand how each option behaves and handle the exceptions explicitly rather than assuming the script is fully protected.

Conclusion

The set builtin turns bash from a forgiving shell into a strict one: set -e stops on errors, set -u catches typos in variable names, and set -x shows you exactly what ran. Combine them as set -euo pipefail at the top of a script, scope tracing to the block you are debugging, and your scripts will fail loudly and early instead of quietly doing the wrong thing. For more on writing reliable scripts, see our guides on bash best practices and bash functions .

ripgrep Cheatsheet

Basic Search

Search files and directories for matching text.

Command Description
rg "pattern" Search recursively from the current directory
rg "pattern" file.txt Search a single file
rg "pattern" dir/ Search a specific directory
rg "pattern" file1 file2 Search specific files
rg --version Show the installed ripgrep version

File Type Filters

Limit searches to known file types.

Command Description
rg -t py "pattern" Search only Python files
rg -t js "pattern" Search only JavaScript files
rg -t markdown "pattern" Search only Markdown files
rg -T js "pattern" Exclude JavaScript files
rg --type-list Show available file type names

Glob Filters

Include or exclude paths with glob patterns.

Command Description
rg -g '*.log' "error" Search only .log files
rg -g '*.conf' "listen" /etc Search matching config files
rg -g '!*.min.js' "console.log" Exclude minified JavaScript files
rg -g '!node_modules/' "TODO" Exclude a directory
rg -g '*.md' -g '!README.md' "pattern" Combine include and exclude globs

Case and Literal Search

Control case matching and regex handling.

Command Description
rg -i "warning" Case-insensitive search
rg -S "warning" Smart case search
rg -s "Warning" Force case-sensitive search
rg -F "price[0]" Search for a fixed string
rg -w "id" Match whole words only

Counts and File Lists

Summarize matches or print filenames.

Command Description
rg -c "error" Count matching lines per file
rg --count-matches "error" Count individual matches per file
rg -l "error" List files with matches
rg --files-without-match "error" List files without matches
rg --stats "error" Print search statistics

Context Output

Show lines around each match.

Command Description
rg -C 3 "panic" Show 3 lines before and after
rg -A 2 "error" Show 2 lines after
rg -B 2 "error" Show 2 lines before
rg -n "error" Show line numbers
rg -N "error" Hide line numbers

Patterns and Regex

Search with multiple patterns and regex features.

Command Description
rg -e "error" -e "warning" Match either pattern
rg -e "--force" Search for a pattern starting with -
rg 'error|warning' Use regex alternation
rg '^server' Match lines starting with server
rg 'listen$' Match lines ending with listen

Hidden and Ignored Files

Search paths that rg skips by default.

Command Description
rg --hidden "api_key" Include hidden files and directories
rg --no-ignore "TODO" Ignore .gitignore, .ignore, and .rgignore
rg --hidden --no-ignore "password" Search hidden and ignored files
rg -u "pattern" Reduce ignore filtering by one level
rg -uuu "pattern" Search almost everything, including binary files

Replacement Preview

Preview changed output without editing files.

Command Description
rg "old" -r "new" Preview replacing old with new
rg '(foo)(bar)' -r '$2$1' Reorder capture groups in output
rg -o 'v[0-9]+\.[0-9]+\.[0-9]+' Print only matched version strings
rg -o -r '$1' 'version = "([^"]+)"' Extract a captured value
rg "foo" -r "bar" Preview before using sed or an editor

Troubleshooting

Quick checks for common rg issues.

Issue Check
File is missing from results Run rg --debug "pattern"
Hidden files are skipped Add --hidden
Ignored files are skipped Add --no-ignore
Glob does not work Quote it, for example -g '*.conf'
Pattern starts with - Use rg -e "--flag"

Common Options

Useful flags to remember.

Option Description
-t TYPE Search only a file type
-T TYPE Exclude a file type
-g GLOB Include or exclude paths by glob
-i Ignore case
-S Smart case
-F Fixed string search
-w Whole-word match
-l List matching files
--files-without-match List non-matching files
-C N Show context lines
-r TEXT Preview replacement output
--hidden Include hidden files
--no-ignore Ignore ignore-file rules

Related Guides

Use these guides for full command workflows.

Guide Description
ripgrep Command in Linux Full rg tutorial with practical examples
grep Command in Linux Standard text search with GNU grep
Grep Exclude Exclude files, directories, and patterns with grep
find Files in Linux Locate files by metadata and path
sed Find and Replace Replace text after previewing matches

ripgrep Command in Linux: Fast Recursive Search

Searching through a large codebase or a directory full of log files with grep often means adding -r for recursion, -n for line numbers, and --include or --exclude rules to avoid unrelated files. In a Git repository, you may also need extra excludes for build directories, vendor files, and binary output.

ripgrep (rg) handles the common case with better defaults. It searches directories recursively, respects .gitignore, .ignore, and .rgignore rules, skips hidden and binary files during recursive searches, and shows friendly interactive output. It is also much faster than traditional recursive grep on many large file trees because it uses Rust’s regex engine and parallel directory traversal.

This guide explains how to use the ripgrep command in Linux, from basic searches to file type filters, context lines, replacement previews, and config defaults.

Installing ripgrep

On Ubuntu, Debian, and Derivatives, install the ripgrep package with apt:

Terminal
sudo apt install ripgrep

On Fedora, RHEL, and Derivatives, use dnf:

Terminal
sudo dnf install ripgrep

On Arch Linux, install it from the official repositories:

Terminal
sudo pacman -S ripgrep

Verify the installation with:

Terminal
rg --version

The output shows the installed version:

output
ripgrep 15.1.0 (rev af60c2de9d)

Your distribution may ship a different version, but the examples in this guide use common options available in current ripgrep releases.

Syntax

The basic syntax for the rg command is:

txt
rg [OPTIONS] PATTERN [PATH ...]

The PATTERN argument is the text or regular expression to search for. If you omit PATH, rg searches recursively from the current directory.

For example, rg "error" searches the current directory tree, while rg "error" logs/ limits the search to the logs directory.

Basic Search

Search for a pattern in all files under the current directory:

Terminal
rg "error"

Example output:

output
logs/app.log:42:error: connection refused
logs/app.log:78:error: timeout after 30s
src/main.py:114:raise ValueError("error parsing config")

The output shows the filename, line number, and matching line. In an interactive terminal, rg also uses color by default, so you do not need the usual recursive grep flags for the common code-search workflow.

To search in a specific file, pass the filename after the pattern:

Terminal
rg "error" logs/app.log

To search in a specific directory, pass the directory path:

Terminal
rg "error" logs/

Filtering by File Type

Use -t to restrict the search to a known file type:

Terminal
rg -t py "import os"

This searches only Python files. ripgrep includes built-in definitions for many file types. To see the full list, run:

Terminal
rg --type-list

To exclude a file type, use -T:

Terminal
rg -T js "TODO"

You can also use glob patterns with -g to match or exclude specific filenames. Quote glob patterns so the shell does not expand them before rg receives them:

Terminal
rg -g '*.log' "error"

Use a leading ! to exclude matching paths:

Terminal
rg -g '!*.min.js' "console.log"

The ! prefix belongs to rg, so quoting is especially useful in shells where ! can trigger history expansion.

Case-Insensitive Search

Add -i to ignore letter case:

Terminal
rg -i "warning"

This matches warning, Warning, WARNING, and other case variants.

For a more flexible default, use -S or --smart-case:

Terminal
rg -S "warning"

With smart case, an all-lowercase pattern is case-insensitive, but a pattern with any uppercase letter is case-sensitive. For example, rg -S "warning" matches Warning, while rg -S "Warning" searches for that exact capitalization.

Fixed String Search

By default, rg treats the pattern as a regular expression. Use -F to search for a literal string instead, which is useful when the pattern contains regex characters:

Terminal
rg -F "price[0]"

Without -F, [0] would be interpreted as a character class. With -F, rg searches for the literal text price[0].

Counting and Listing Matches

To count matching lines per file instead of printing the matching lines, use -c:

Terminal
rg -c "error"

Example output:

output
logs/app.log:14
logs/nginx/access.log:3

The count is the number of matching lines, not the total number of matching words or strings.

To print only filenames that contain at least one match, use -l:

Terminal
rg -l "error"

To print only filenames that contain no matches, use --files-without-match:

Terminal
rg --files-without-match "error"

This is different from combining -v with -l. The --files-without-match option checks whether a file has zero matching lines.

Context Lines

When a matching line alone is not enough to understand the surrounding code, add context with -C:

Terminal
rg -C 3 "panic"

This prints three lines before and three lines after each match.

Use -A for lines after the match only:

Terminal
rg -A 2 "def connect"

Use -B for lines before the match only:

Terminal
rg -B 2 "def connect"

Context output is useful when you want to inspect the code around a function, error message, or configuration value without opening each file.

Multiple Patterns

Use -e to search for more than one pattern in a single pass:

Terminal
rg -e "error" -e "warning"

This matches any line containing either word. It is equivalent to the regex error|warning, but repeated -e options are easier to read when patterns become longer.

The -e option is also useful when a pattern begins with a dash:

Terminal
rg -e "--force"

Without -e, rg would try to interpret --force as an option.

Whole-Word Match

The -w flag restricts matches to whole words. This is useful when you want to find a variable name without matching it inside a longer name:

Terminal
rg -w "id"

This matches id, but not uid or invalid.

Invert Match

The -v flag prints lines that do not match the pattern:

Terminal
rg -v "^#" config.txt

This shows all lines in config.txt that do not begin with #.

If you want filenames that do not contain a pattern at all, use --files-without-match instead:

Terminal
rg --files-without-match "version" -g '*.json'

Searching Hidden Files and Ignoring .gitignore

By default, recursive rg searches skip hidden files and directories, and respect .gitignore, .ignore, and .rgignore files. This is usually what you want in a source tree.

To include hidden files and directories, use --hidden:

Terminal
rg --hidden "api_key"

To ignore .gitignore, .ignore, and .rgignore rules, use --no-ignore:

Terminal
rg --no-ignore "TODO"

The --no-ignore option does not include hidden files by itself. To search hidden files and ignored files in the same search, combine both options:

Terminal
rg --hidden --no-ignore "password"
Warning
Searching with --hidden --no-ignore can include build artifacts, dependency directories, cache folders, and other large trees. It is much slower and can produce noisy results, so use it only when you have a specific reason to search outside the normal project files.

Replacing Output

The -r option rewrites matching text in the output to show what a replacement would look like. It does not modify any files:

Terminal
rg "foo" -r "bar"

Example output:

output
src/config.py:5:bar = get_setting("bar")

The output shows the line as it would look after replacing foo with bar. This is useful for previewing a project-wide rename before using sed or a text editor’s find-and-replace feature.

For capture groups in replacements, wrap the command in single quotes so the shell does not expand $1, $name, or similar replacement references before rg runs.

Showing Only the Match

By default, rg prints the full line containing the match. Use -o to print only the matched text:

Terminal
rg -o 'v[0-9]+\.[0-9]+\.[0-9]+'

Example output:

output
package.json:4:v1.4.2
package-lock.json:8:v1.4.2

This is useful when you want to extract values from files rather than inspect matching lines in context.

ripgrep Configuration File

ripgrep can read default options from a configuration file, but it does not automatically look in ~/.config/ripgrep/ or any other fixed path. You must point RIPGREP_CONFIG_PATH to the file you want rg to read.

For example, create a config file named ~/.ripgreprc:

~/.ripgreprctxt
--smart-case
--hidden
--glob=!.git/

Then export the environment variable:

Terminal
export RIPGREP_CONFIG_PATH="$HOME/.ripgreprc"

Add that export line to your shell startup file, such as ~/.bashrc or ~/.zshrc, if you want the setting to apply in new terminal sessions.

Each config file line is passed to rg as one command-line argument. For options with values, use either --option=value on one line or put the option and its value on separate lines.

Troubleshooting

rg does not find a file you expected
The file may be hidden, ignored by .gitignore, ignored by .ignore or .rgignore, or detected as binary. Start with rg --debug "pattern" to see why paths were skipped, then add --hidden, --no-ignore, or both only when needed.

A glob pattern does not work as expected
Quote glob patterns, such as rg -g '*.conf' "server" and rg -g '!*.min.js' "console.log". Without quotes, your shell may expand * before rg sees the pattern.

A pattern starting with - is treated as an option
Use -e before the pattern, for example rg -e "--force". You can also use -- to stop option parsing, as in rg -- "--force".

Options Reference

  • -t TYPE - Search only files of the given type.
  • -T TYPE - Exclude files of the given type.
  • -g GLOB - Include or exclude files by glob. A ! prefix excludes paths.
  • -i - Search case-insensitively.
  • -F - Treat the pattern as a fixed string, not a regex.
  • -c - Count matching lines per file.
  • -l - List only filenames with matches.
  • --files-without-match - List only filenames without matches.
  • -C N - Show N lines of context around each match.
  • -A N - Show N lines after each match.
  • -B N - Show N lines before each match.
  • -e PATTERN - Add a search pattern. Repeat it to search for multiple patterns.
  • -w - Match whole words only.
  • -v - Invert the match and print non-matching lines.
  • -o - Print only the matched text, not the full line.
  • -r REPLACEMENT - Show output with matches replaced. This is a preview only.
  • -n - Show line numbers. This is enabled by default in interactive terminal output.
  • --hidden - Search hidden files and directories.
  • --no-ignore - Do not respect .gitignore, .ignore, and .rgignore rules.
  • -S, --smart-case - Search case-insensitively when the pattern is lowercase, and case-sensitively when it contains uppercase.
  • --stats - Print a summary of the search at the end.
  • -M N - Omit lines longer than N bytes.

Quick Reference

For a printable quick reference, see the ripgrep cheatsheet .

Task Command
Search recursively from the current directory rg "pattern"
Search in a specific file rg "pattern" file.txt
Search in a specific directory rg "pattern" logs/
Search only Python files rg -t py "pattern"
Exclude JavaScript files rg -T js "pattern"
Search by glob rg -g '*.log' "pattern"
Exclude by glob rg -g '!*.min.js' "pattern"
Search case-insensitively rg -i "pattern"
Use smart case rg -S "pattern"
Search for a fixed string rg -F "price[0]"
Count matching lines per file rg -c "pattern"
List matching filenames rg -l "pattern"
List filenames without matches rg --files-without-match "pattern"
Show three lines of context rg -C 3 "pattern"
Search multiple patterns rg -e "foo" -e "bar"
Match a whole word rg -w "id"
Invert line matches rg -v "pattern"
Include hidden files rg --hidden "pattern"
Ignore ignore files rg --no-ignore "pattern"
Include hidden and ignored files rg --hidden --no-ignore "pattern"
Preview replacement output rg "old" -r "new"
Print only matched text rg -o "pattern"

Conclusion

ripgrep covers the same ground as grep , but its recursive search, file filtering, and ignore-file support make it a better default for many code and log searches. For path-based file finding, pair it with find , or use rg -l when you need a list of files that contain a specific pattern.

whois Command in Linux: Query Domain Registration Info

When you need to know who owns a domain, when it expires, which registrar handles it, or which organization holds a particular IP block, the whois command is the fastest route. It queries the registry databases that record this information and returns a plain-text response you can scan in a terminal. The output format varies by registry, but the questions you can answer are consistent: registrar, name servers, registration and expiry dates, and contact info (where privacy rules allow).

This guide explains how to use whois in Linux to look up domains, IP addresses, and AS numbers, how to target a specific server, and how to parse the output for the fields you actually care about.

whois Syntax

The general form is:

txt
whois [OPTIONS] OBJECT

OBJECT is the domain, IP address, or AS number you want information about. With no options, whois picks the right registry automatically based on the type of query.

Install whois

whois is not always installed by default. On Ubuntu, Debian, and Derivatives:

Terminal
sudo apt update
sudo apt install whois

On Fedora, RHEL, and Derivatives:

Terminal
sudo dnf install whois

Confirm it is in place:

Terminal
whois --version
output
Version 5.6.6.

The Debian-family whois is an actively maintained client with built-in routing logic that knows which registry to ask for each TLD.

Look Up a Domain

The most common use is checking a domain:

Terminal
whois example.com
output
 Domain Name: EXAMPLE.COM
Registry Domain ID: 2336799_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.iana.org
Registrar URL: http://res-dom.iana.org
Updated Date: 2026-01-16T18:26:50Z
Creation Date: 1995-08-14T04:00:00Z
Registry Expiry Date: 2026-08-13T04:00:00Z
Registrar: RESERVED-Internet Assigned Numbers Authority
Registrar IANA ID: 376
Name Server: ELLIOTT.NS.CLOUDFLARE.COM
Name Server: HERA.NS.CLOUDFLARE.COM
DNSSEC: signedDelegation
...

The fields that matter most for everyday questions are:

  • Registrar, the company managing the registration.
  • Creation Date and Registry Expiry Date, which tell you how old the domain is and when it needs renewing.
  • Name Server, which lists the DNS servers authoritative for the domain.
  • DNSSEC, which shows whether the domain is cryptographically signed.

For ccTLDs (.de, .uk, .jp), the format differs because each country runs its own registry. The information is similar; the field names and order change.

Look Up an IP Address

whois on an IP returns the network allocation, not the domain:

Terminal
whois 93.184.216.34
output
inetnum: 93.184.216.0 - 93.184.216.255
netname: EDGECAST-NETBLK-03
descr: NETBLK-03-EU-93-184-216-0-24
country: EU
admin-c: DS7892-RIPE
tech-c: DS7892-RIPE
status: ASSIGNED PA
...

This kind of query is the right tool for “who owns this IP that has been hitting my server” investigations. The output names the network block, maintainer, and abuse contact details when the registry publishes them.

Look Up an AS Number

Pass an autonomous system number with the AS prefix:

Terminal
whois AS15169
output
ASNumber: 15169
ASName: GOOGLE
ASHandle: AS15169
RegDate: 2000-03-30
Updated: 2012-02-24
Ref: https://rdap.arin.net/registry/autnum/15169

AS lookups are useful when you trace a route with mtr or traceroute and want to know which network each hop belongs to.

Pick a Specific WHOIS Server

The default routing finds the right server for most TLDs, but you can force a query against a specific server with -h:

Terminal
whois -h whois.arin.net 8.8.8.8

The flag is the right tool for two situations: when the default routing picks the wrong upstream (rare but happens for some legacy TLDs), and when you want to compare answers between regional registries (ARIN, RIPE, APNIC, AFRINIC, LACNIC).

Limit the Recursion

Most modern whois clients follow a referral chain: query IANA, follow the pointer to the TLD registry, follow the pointer to the registrar, and return the most specific answer. To stop registry-to-registrar recursion, pass --no-recursion:

Terminal
whois --no-recursion example.com

The flag is most useful when you specifically want the registry data and not the registrar’s slightly different format.

The -H option has a different purpose. It hides legal disclaimers from the output, which can make short lookups easier to read:

Terminal
whois -H example.com

Filter the Output

Real whois responses are dozens of lines long with legal disclaimers and template text. To extract one field, pipe through grep:

Terminal
whois example.com | grep -E "Registrar:|Expiry Date:"
output
 Registry Expiry Date: 2026-08-13T04:00:00Z
Registrar: RESERVED-Internet Assigned Numbers Authority

For a name-server list:

Terminal
whois example.com | awk '/Name Server:/ {print $NF}'
output
ELLIOTT.NS.CLOUDFLARE.COM
HERA.NS.CLOUDFLARE.COM

These short patterns work for monitoring scripts that watch for domain expirations or DNSSEC status changes.

Check Domain Availability

If the domain is not registered, the response says so explicitly. The exact wording depends on the registry:

Terminal
whois never-existed-domain-xyzzy.com
output
No match for domain "NEVER-EXISTED-DOMAIN-XYZZY.COM".

Some registries (notably .io, .co, and several ccTLDs) return an empty or near-empty response for unregistered domains. Two heuristics that work in scripts:

  • For .com/.net/.org, grep for No match for or Domain Name: in the output.
  • For ccTLDs, grep for Domain not found or check whether the registration fields exist.

Rate Limits and Etiquette

Registries rate-limit whois queries. Hammering them with a script is the fastest way to get blocked. If you query many domains, add a sleep between calls and cache the result locally. For bulk lookups, use the registry’s RDAP service directly or pay for a commercial WHOIS API.

A simple polite pattern:

Terminal
while IFS= read -r domain; do
 whois "$domain"
 sleep 2
done < domains.txt

Two seconds between queries is a sane starting point; raise it if you see throttling responses.

Privacy and Redacted Output

Since GDPR took effect, most TLDs redact personal contact information for individual registrants. The response usually contains placeholders like REDACTED FOR PRIVACY or Data Protected, Not Disclosed. For organizations and legal entities, the contact information often stays visible.

This is not a defect in whois; the underlying registry data is simply less detailed than it used to be. For account-takeover prevention and abuse handling, focus on the registrar field and the abuse contact email, which remain published.

Quick Reference

Task Command
Look up a domain whois example.com
Look up an IP address whois 93.184.216.34
Look up an AS number whois AS15169
Query a specific server whois -h whois.arin.net 8.8.8.8
Stop registry-to-registrar recursion whois --no-recursion example.com
Hide legal disclaimers whois -H example.com
Extract registrar and expiry fields whois example.com | grep -E “Registrar:|Expiry Date:"
List name servers whois example.com | awk ‘/Name Server:/ {print $NF}’

Troubleshooting

whois: command not found
Install the package: sudo apt install whois on Ubuntu, Debian, and Derivatives, or sudo dnf install whois on Fedora, RHEL, and Derivatives. The package is small and adds no significant dependencies.

Output says “fgets: Connection reset by peer”
The registry rate-limited or blocked your IP. Wait a few minutes and retry, slow your script down, or query through a different network.

Response is in a different language or alphabet
Some ccTLD registries return data in the local language. Look for the English section (usually further down), or pipe through iconv if the encoding makes the response unreadable in your terminal.

FAQ

What is the difference between WHOIS and RDAP?
RDAP (Registration Data Access Protocol) is the modern replacement for WHOIS. It returns structured JSON instead of free-text and supports authentication and access controls. Most registries now serve both, and RDAP is usually the better choice for scripts that need predictable fields.

Why does the data for the same domain look different between two whois runs?
Different clients and servers can follow the referral chain differently. One response may come from the registry, while another may include data from the registrar’s WHOIS server. Use --no-recursion when you want to stop at the registry answer.

Can I run my own WHOIS server?
Yes, but only registrars and registries have authoritative data. Self-hosted WHOIS servers are useful for internal directories (IP allocation in a large network), not for public domain lookups.

Conclusion

whois is the answer to “who owns this”, whether the “this” is a domain, an IP, or an AS number. The output is plain text, the flags are short, and a handful of grep/awk patterns turn it into a script-friendly data source. For bulk work, slow the queries down and respect the rate limits the registries publish.

For related reading, see our guides on the dig command and the nslookup command .

How to Install and Use Zsh on Ubuntu

Bash has been the default login shell on Ubuntu for years, and it works fine for running scripts and one-off commands. When we spend most of the day inside a terminal, though, small details start to matter: smarter tab completion, spell correction on commands, recursive globs, and rich theming. Zsh adds those features without breaking the Bash scripts we already rely on.

This guide explains how to install Zsh on Ubuntu, set it as the default shell, configure ~/.zshrc, and bolt on the Oh My Zsh framework for plugins and themes.

Quick Reference

Task Command
Install Zsh sudo apt install zsh
Check version zsh --version
Set as default shell chsh -s $(which zsh)
Reload config source ~/.zshrc
List shells cat /etc/shells
Switch back to Bash chsh -s /bin/bash
Install Oh My Zsh sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

Prerequisites

You need an Ubuntu system with a user account that has sudo privileges. The Zsh package comes from the default Ubuntu repositories, while the Oh My Zsh installer needs curl and git.

Install those helper packages if they are missing:

Terminal
sudo apt update
sudo apt install curl git

Install Zsh

Zsh is available in the default Ubuntu repositories. Refresh the package index and install it:

Terminal
sudo apt update
sudo apt install zsh

Verify the installation by checking the Zsh version:

Terminal
zsh --version

The output looks similar to:

output
zsh 5.9 (x86_64-ubuntu-linux-gnu)

Run Zsh for the First Time

Start Zsh without changing your default shell yet:

Terminal
zsh

The first launch opens the zsh-newuser-install configuration wizard. The relevant choices are:

  • 0 exits the wizard and creates an empty ~/.zshrc so Zsh stops asking on every start.
  • 1 continues to the interactive menu, where we can pick defaults for history, completion, key bindings, and a few other behaviors.
  • 2 populates ~/.zshrc with the system-wide recommended defaults.
  • q quits without creating a config file at all, which means the wizard runs again next time.

Pick option 0 or q if you plan to install Oh My Zsh next, since it ships its own template and will overwrite the file anyway. Otherwise, run the interactive setup to generate a sensible starting point.

Set Zsh as the Default Shell

Once you are happy with the way Zsh behaves, change the default login shell for your user:

Terminal
chsh -s $(which zsh)

Log out and log back in so the new shell takes effect. Confirm the change with:

Terminal
echo $SHELL

The output should be /usr/bin/zsh or /bin/zsh, depending on your Ubuntu release and shell path layout.

If you prefer to keep Bash as the login shell and only run Zsh on demand, skip this step and start Zsh manually with zsh whenever you need it.

Edit ~/.zshrc

The ~/.zshrc file runs every time an interactive Zsh session starts. Open it with your editor:

Terminal
nano ~/.zshrc

A minimal useful starting point looks like this:

~/.zshrcsh
# History
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt SHARE_HISTORY
setopt HIST_IGNORE_DUPS

# Completion
autoload -Uz compinit
compinit

# Aliases
alias ll='ls -lah'
alias gs='git status'

# Prompt
PROMPT='%n@%m %~ %# '

Reload the file in the current session without opening a new terminal:

Terminal
source ~/.zshrc

For more on aliases, see how to create Bash aliases . The same syntax works in Zsh.

Install Oh My Zsh

Oh My Zsh is a popular framework that ships a large catalog of plugins and themes. The official installer is the most common setup path:

Warning
Piping a remote install script into sh runs code from the network as your user. If you are setting up a shared or production machine, download the installer first, review it, and then run it locally.

To inspect the script before running it, use:

Terminal
curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -o install-oh-my-zsh.sh
less install-oh-my-zsh.sh
sh install-oh-my-zsh.sh

For a quick personal workstation setup, you can run the installer directly:

Terminal
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

The installer clones Oh My Zsh into ~/.oh-my-zsh, backs up the existing ~/.zshrc to ~/.zshrc.pre-oh-my-zsh, and writes a new ~/.zshrc with sensible defaults.

Pick a Theme

Open ~/.zshrc and find the ZSH_THEME line. The default value is robbyrussell. Try a different theme by setting:

sh
ZSH_THEME="agnoster"

The agnoster theme uses powerline-style segments and requires a Nerd Font for the special glyphs. If you want to sample themes without installing extra fonts first, use ZSH_THEME="random" to cycle themes on every login until you find one you like.

Reload the configuration to apply the new theme:

Terminal
source ~/.zshrc

Enable Plugins

Plugins extend Zsh with completion definitions, shortcuts, and visual aids. Edit the plugins=() line in ~/.zshrc and add the plugins you want:

sh
plugins=(git docker kubectl sudo zsh-autosuggestions zsh-syntax-highlighting)

git, docker, kubectl, and sudo ship with Oh My Zsh. zsh-autosuggestions and zsh-syntax-highlighting are external plugins. Install them with:

Terminal
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

Reload ~/.zshrc to activate them:

Terminal
source ~/.zshrc

zsh-autosuggestions shows a faded completion based on your history as you type. Press the right arrow key to accept it. zsh-syntax-highlighting colors valid commands green and unknown commands red, which catches typos before you press Enter.

Useful Built-in Features

Zsh ships several features that are not available in Bash without extra setup:

  • Recursive glob patterns: ls **/*.log lists every .log file under the current directory.
  • Globbing qualifiers: ls *(.) lists only regular files; ls *(/) lists only directories.
  • Spell correction: setopt CORRECT prompts you to fix mistyped commands.
  • Sharing history across sessions in real time with setopt SHARE_HISTORY.

Troubleshooting

chsh reports “non-standard shell”
Make sure /usr/bin/zsh is listed in /etc/shells. If it is missing, add it with echo $(which zsh) | sudo tee -a /etc/shells and run chsh again.

Zsh starts but the prompt is broken on a remote SSH session
The theme probably depends on a Nerd Font that the local terminal does not have. Switch to a simpler theme such as robbyrussell or install a Nerd Font in the terminal application.

Plugins are listed but nothing happens
Check the ZSH_CUSTOM path matches where you cloned the plugin. Run ls ~/.oh-my-zsh/custom/plugins to confirm the plugin directory exists, then reload ~/.zshrc.

Conclusion

From here we can keep tweaking ~/.zshrc to match the way we work: add aliases for repetitive commands, pin a theme that reads well in the terminal we use most, and pull in plugins as new tools enter the workflow. The alias syntax matches Bash, so existing alias workflows carry over without changes.

Bash Arithmetic: Integer and Floating-Point Math

Bash treats everything as a string by default, which catches many people out the first time they write total=$count+1 and end up with the literal text 5+1 instead of 6. The shell has dedicated arithmetic forms that evaluate numeric expressions properly, but they only handle integers; for floating-point math you reach for bc or awk. Once you know which form to use where, the math is short and predictable.

This guide explains how Bash arithmetic works, the differences between (( )), $(( )), and let, and how to combine them with external tools when you need decimals.

Why Plain Assignment Does Not Add

A first attempt at incrementing a variable usually looks like this:

sh
count=5
total=$count+1
echo "$total"
output
5+1

The shell sees $count as the string 5, joins it with +1, and assigns the result. To force numeric evaluation, use one of the arithmetic forms below.

The (( )) Compound Command

Wrap an expression in double parentheses to evaluate it as integer math:

sh
count=5
(( count = count + 1 ))
echo "$count"
output
6

Inside (( )), variable names work without $, the C-style operators are available (+, -, *, /, %, **, ++, --, <<, >>, &, |, ^, &&, ||), and the exit status of the whole expression is non-zero when the result is 0, which means you can use arithmetic in if:

sh
if (( count > 5 )); then
 echo "count is above 5"
fi
output
count is above 5

The shortcut increment and decrement operators read cleanly in loops:

sh
for (( i = 0; i < 3; i++ )); do
 echo "i=$i"
done
output
i=0
i=1
i=2

(( )) is the right form when you assign back to a variable or use the result for control flow. It does not print anything to standard output.

The $(( )) Expansion

When you want the value of an expression rather than the side effect, use the $(( )) arithmetic expansion. The result is substituted into the surrounding command like any other expansion:

sh
count=5
echo "Next: $(( count + 1 ))"
output
Next: 6

$(( )) is the right form for inline math inside echo, assignments to other variables, command arguments, and printf:

sh
files=42
echo "Average: $(( files / 7 )) per day"
output
Average: 6 per day

Notice that the division truncates: 42 / 7 happens to be exact, but 43 / 7 would yield 6, not 6.14. Bash arithmetic is integer-only; the decimal part is silently discarded.

The let Builtin

let is the older builtin for arithmetic. It evaluates each argument as an expression:

sh
let "count = 5 + 1"
echo "$count"
output
6

let is mostly historical at this point. (( )) does the same work without the quoting trap (let count=* would expand the * as a glob unless quoted) and reads more cleanly. New scripts should prefer (( )); let is worth recognizing in code you inherit.

Operators You Will Actually Use

Bash supports a wide list of arithmetic operators. These are the ones that come up in everyday scripts:

  • +, -, *, /, % - Standard arithmetic and remainder.
  • ** - Exponentiation.
  • ++, -- - Increment and decrement, prefix or postfix.
  • +=, -=, *=, /=, %= - Compound assignment.
  • ==, !=, <, <=, >, >= - Numeric comparison (inside (( ))).
  • &&, ||, ! - Logical AND, OR, NOT.
  • <<, >>, &, |, ^, ~ - Bitwise shifts and operations.

A small example that uses several:

sh
size=1024
if (( size > 0 && size % 2 == 0 )); then
 echo "Power of two? $(( (size & (size - 1)) == 0 ? 1 : 0 ))"
fi
output
Power of two? 1

The ternary operator (? :) works inside arithmetic contexts, which keeps short branches readable without nesting if.

Number Bases

Bash arithmetic supports several integer bases. Prefix a number to declare its base:

  • 0xNN for hexadecimal.
  • 0NN for octal.
  • BASE#NNN for any base from 2 to 64.

Examples:

sh
echo "$(( 0xff ))"
echo "$(( 0755 ))"
echo "$(( 2#1010 ))"
output
255
493
10

Watch out for leading zeros: 08 is interpreted as octal and rejected because 8 is not a valid octal digit. If user input may include leading zeros (HTTP status codes parsed from a string, timestamps), strip them before arithmetic with ${var#0} or use the explicit 10# base prefix:

sh
value="08"
echo "$(( 10#$value ))"
output
8

Floating-Point Math with bc

Bash itself cannot do decimal math, but it is happy to call out to a tool that can. bc is the arbitrary-precision calculator that ships with most distributions:

sh
echo "scale=2; 43 / 7" | bc
output
6.14

The scale=2 directive sets two decimal places of precision. Without it, bc truncates the division the same way Bash does. Use a higher scale for currency or scientific work:

sh
echo "scale=10; 4*a(1)" | bc -l
output
3.1415926532

The -l flag loads the math library, which gives you s (sine), c (cosine), a (arctangent), l (natural log), e (exponential), and sqrt. The expression above computes pi from 4 * arctan(1).

To capture the result into a variable:

sh
pi=$(echo "scale=4; 4*a(1)" | bc -l)
echo "pi = $pi"
output
pi = 3.1415

bc is the right tool when you need genuine decimals and do not want a heavyweight dependency. For one-off interactive calculations, run bc -l and type expressions at its prompt.

Floating-Point Math with awk

awk is the other obvious choice and is often faster because it does not start a separate calculator process. Use awk for inline expressions inside scripts:

sh
rate=$(awk 'BEGIN { printf "%.2f\n", 43 / 7 }')
echo "rate = $rate"
output
rate = 6.14

printf inside awk works the same way as the C function, with %.2f controlling the number of decimal places. For a percentage:

sh
pct=$(awk -v a=23 -v b=89 'BEGIN { printf "%.1f%%\n", (a / b) * 100 }')
echo "pct = $pct"
output
pct = 25.8%

Pass values into awk with -v name=value rather than building the expression by string concatenation; the -v form avoids quoting headaches and shell-injection issues when the inputs come from user data.

Increment Counters in a Loop

Counter loops are the bread and butter of arithmetic in scripts, and they lean on the increment and decrement operators . Two equivalent forms work; pick the one that reads best:

sh
count=0
while IFS= read -r line; do
 (( count++ ))
done < input.txt
echo "Lines: $count"
sh
count=0
while IFS= read -r line; do
 count=$(( count + 1 ))
done < input.txt
echo "Lines: $count"

The (( count++ )) form is shorter and is what most authors prefer. Both produce the same result and run at the same speed.

If the script uses set -e, avoid (( count++ )) when count may start at zero. The arithmetic command returns a failure status when the expression evaluates to 0, so the first increment can stop the script. Use pre-increment or assignment instead:

sh
(( ++count ))
(( count += 1 ))

Troubleshooting

syntax error in expression
A variable that should hold a number contains non-numeric characters (often a leading or trailing space from read, or a stray letter). Inside (( )) and $(( )), Bash reports a syntax error on the offending token. Trim the input with ${var//[[:space:]]/} or validate it before the arithmetic line.

division by 0
A divisor evaluated to zero, often because the variable was empty and Bash treated it as 0. Quote the expansion in your check (if [ -n "$divisor" ]) before the divide, and either skip the operation or substitute a default with ${divisor:-1}.

Result is 0 when it should be a fraction
Bash arithmetic is integer-only. Switch to bc with an explicit scale= or to awk with %.Nf formatting for decimal output.

FAQ

When should I use (( )) versus $(( ))?
Use (( )) for assignments and control flow (if, while, for). Use $(( )) when you want the value substituted into a surrounding command. They share the same expression syntax.

Can I do floating-point math without an external tool?
Not in Bash. ksh93 and zsh support floating-point in their arithmetic contexts; Bash does not. For Bash, route decimals through bc or awk.

Is there a performance difference between bc and awk?
awk is usually faster because it runs entirely inside a single process and does not communicate over a pipe. For one-off calculations the difference is negligible; in a tight loop, prefer awk or precompute the values with a single awk call instead of running bc per iteration.

Conclusion

Reach for (( )) and $(( )) whenever a script needs counters, conditions, or quick integer math, and switch to bc or awk the moment decimals enter the picture. Pair that habit with Bash strict mode and the patterns in our Bash best practices guide so numeric code keeps working even when the inputs do not.

Docker Networking: Connect Containers

Once you move past single-container tests, the next problem is communication. A web application needs to reach its database, a reverse proxy needs to forward traffic to an app server, and none of those internal services should be exposed to the public internet by accident.

Docker networking handles that traffic. The defaults work for quick tests, but production-like setups are easier to manage when you understand networks, drivers, DNS names, and published ports. This guide explains how Docker networking works and walks through practical examples for connecting and isolating containers.

How Docker Networking Works

When the Docker daemon starts, it creates three networks automatically: bridge, host, and none. You can see them with docker network ls:

Terminal
docker network ls
output
NETWORK ID NAME DRIVER SCOPE
b84a2c1f9d8e bridge bridge local
c1f3a7b2d4e5 host host local
e7d6a8c2b1f4 none null local

Each network uses a driver that decides how containers on that network communicate:

  • bridge - Creates an isolated virtual network on one Docker host.
  • host - Removes network namespace isolation and uses the host network directly.
  • none - Starts the container with only a loopback interface.

The bridge driver is the default and works for most single-host applications. For anything beyond a quick test, create a user-defined bridge network instead of relying on the default bridge network. User-defined networks give containers automatic DNS resolution by name and keep unrelated services separated.

The Default Bridge Network

When you start a container without specifying a network, Docker attaches it to the default bridge network. Containers on this network can reach each other by IP address, but not by container name, which makes configuration brittle.

Start two containers on the default network:

Terminal
docker run -d --name default_web1 nginx
docker run -d --name default_web2 nginx

Inspect the default bridge to see both containers and their IP addresses:

Terminal
docker network inspect bridge

The output includes a Containers section listing each container with its assigned IP address. Containers on the default bridge can communicate with those addresses, but a name such as default_web2 does not resolve automatically from default_web1. That is the main reason to avoid the default bridge for application stacks.

Creating a User-Defined Bridge Network

User-defined bridge networks solve the name resolution problem and let you group related containers together. Create one with docker network create:

Terminal
docker network create app_net
output
a1b2c3d4e5f67890123456789abcdef0

Now run containers on that network by passing --network:

Terminal
docker run -d --name net_web --network app_net nginx
docker run -d --name net_client --network app_net alpine sleep 3600

From inside net_client, you can reach net_web by container name:

Terminal
docker exec -it net_client ping -c 2 net_web
output
PING net_web (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.098 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.074 ms

The embedded Docker DNS server resolves the container name to the correct IP address. If you add or remove containers, the DNS records update automatically, so you do not have to track container IP addresses yourself.

Publishing Ports to the Host

Containers on a bridge network are reachable from other containers on the same network, but not from outside the Docker host unless you publish a port. Use -p host_port:container_port with docker run:

Terminal
docker run -d --name port_web -p 8080:80 nginx

Nginx listens on port 80 inside the container, and Docker forwards traffic from port 8080 on the host to it. A browser pointed at http://localhost:8080 reaches the container.

You can publish multiple ports by repeating the flag:

Terminal
docker run -d --name port_app -p 8081:80 -p 8443:443 nginx

By default, Docker publishes the port on all host interfaces. To bind only to a specific host interface, prefix the host port with the address:

Terminal
docker run -d --name loopback_web -p 127.0.0.1:8082:80 nginx

This binds the port to the loopback interface only. The container is reachable from the host itself, but not from other machines on the network.

Connecting a Running Container to a Network

A container is not locked to the network it was started on. You can attach a running container to an additional network with docker network connect:

Terminal
docker network connect app_net default_web1

After connecting, default_web1 has an IP address on both its original network and app_net. Containers on either network can reach it through the network they share. This is useful when a service needs to talk to more than one group of containers, for example a shared logger or monitoring agent.

To remove a container from a network, use docker network disconnect:

Terminal
docker network disconnect app_net default_web1

Isolating Containers with Separate Networks

Multiple user-defined networks act as separate network segments. Containers on different networks cannot reach each other unless one of them is explicitly connected to both. This makes it simple to isolate a frontend tier from a database tier.

Create two networks:

Terminal
docker network create frontend_net
docker network create backend_net

Start a database on backend_net, an application container on both networks, and a web server on frontend_net:

Terminal
docker run -d --name db_net --network backend_net -e POSTGRES_PASSWORD=localpass postgres:16-alpine
docker run -d --name app_net_demo --network backend_net alpine sleep 3600
docker network connect frontend_net app_net_demo
docker run -d --name web_net --network frontend_net -p 8083:80 nginx

The web_net container can reach app_net_demo over frontend_net, and app_net_demo can reach db_net over backend_net. The web_net container cannot reach db_net directly because they do not share a network. If the web container is compromised, it cannot open a connection to the database without going through the application layer.

Warning
The PostgreSQL password above is only a local demo value. Use strong secrets for real services, and do not store production credentials in shell history, Compose files, or source control.

The host Network Driver

The host driver skips network namespace isolation and attaches the container directly to the host network. The container shares the host network interfaces, so any port it listens on is immediately available on the host:

Terminal
docker run --rm -d --name host_nginx --network host nginx

Nginx is now reachable on port 80 of the host with no port publishing. In host mode, -p, --publish, and --publish-all are ignored because there is no separate container network namespace to map from.

Use host networking only when you specifically need it, such as for very high port counts or when the application must bind directly to host interfaces. The trade-off is weaker network isolation and possible port conflicts with services already running on the host.

Host networking is native to Docker Engine on Linux. Docker Desktop supports host networking in version 4.34 and later, but it must be enabled in Docker Desktop settings.

The none Network Driver

The none driver disables external networking for the container. The container gets a loopback interface and nothing else:

Terminal
docker run --rm --network none alpine ip addr

This is useful for batch jobs that process local files and should not reach the network.

Inspecting Networks

docker network inspect prints the full configuration of a network in JSON:

Terminal
docker network inspect app_net

The output includes the subnet, gateway, driver options, and a list of every container attached to the network with its IPv4 and IPv6 addresses. When a container cannot reach another container, this is usually the first place to look.

For a quick overview across all bridge networks, use a filter:

Terminal
docker network ls --filter driver=bridge

You can also inspect a container to see its network attachments:

Terminal
docker inspect net_web

Look for the NetworkSettings.Networks section in the output.

Removing Networks

Unused networks remain on the host until you remove them. Delete a single network with docker network rm:

Terminal
docker network create temp_net
docker network rm temp_net

The removal command fails if any containers are still attached. Stop or disconnect those containers first, then retry.

To clean up every user-defined network that is not in use, run:

Terminal
docker network prune

Docker asks for confirmation and then removes idle user-defined networks. The built-in bridge, host, and none networks are not removed.

Docker Compose Networking

When you run applications with Docker Compose , Compose creates a dedicated default network for each project and attaches every service to it. Services reach each other by service name without extra configuration:

docker-compose.ymlyaml
services:
 web:
 image: nginx
 ports:
 - "8080:80"

 db:
 image: postgres:16-alpine
 environment:
 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Define the password in a local .env file:

.envtxt
POSTGRES_PASSWORD=change-this-password

From the web service, db resolves to the database container IP address. You can also declare multiple named networks under a top-level networks key and attach services selectively, which mirrors the frontend and backend split shown earlier.

Warning
Do not commit .env files or real passwords to version control. Add .env to .gitignore, and use secret management from your platform for production credentials.

Quick Reference

For a printable quick reference, see the Docker cheatsheet .

Command Description
docker network ls List Docker networks
docker network create app_net Create a user-defined bridge network
docker run --network app_net image Start a container on a network
docker network connect app_net container Attach a running container to a network
docker network disconnect app_net container Remove a container from a network
docker network inspect app_net Show network configuration and attached containers
docker run -p 8080:80 image Publish container port 80 on host port 8080
docker run -p 127.0.0.1:8080:80 image Publish a port only on the host loopback interface
docker run --network host image Run a container on the host network
docker run --network none image Run a container with no external network access
docker network rm app_net Remove an unused network
docker network prune Remove all unused user-defined networks

Troubleshooting

Containers cannot reach each other by name
Name-based DNS works on user-defined networks. If you started the containers on the default bridge network, recreate them on a user-defined network, or attach them with docker network connect.

Port is already allocated when publishing
Another process on the host is already using that port. Pick a different host port, or find and stop the conflicting process with lsof -i :PORT .

Cannot remove a network
The network still has containers attached. List them with docker network inspect NAME, disconnect or stop them, and try again.

Service is unreachable from the host after publishing a port
Check that the service inside the container is listening on 0.0.0.0, not only on 127.0.0.1. A service bound to the container loopback interface is not reachable through the published port.

The -p option does not work with host networking
Published ports are ignored when a container uses --network host. The container is already using the host network directly, so it must listen on the desired host port itself.

FAQ

What is the difference between a bridge and a host network?
A bridge network creates an isolated virtual network for containers and requires port publishing to expose services on the host. A host network attaches the container directly to the host network, with no separate container network namespace and no port publishing.

Do containers on the same user-defined bridge network need published ports to talk to each other?
No. Port publishing controls access from the host or external network. Containers on the same user-defined network reach each other directly on the container port.

Can two containers share the same name on different networks?
No. Container names are unique per Docker daemon, regardless of how many networks they are attached to. Network-scoped aliases, set with --network-alias, can overlap across networks.

Why does ping not work from some containers?
Some minimal images do not include ping. Alpine includes it by default, which makes it useful for quick network tests. Other images may require installing the relevant package, or you can use a temporary troubleshooting image.

Conclusion

Docker networking becomes easier to manage when each application gets its own user-defined network and only public-facing services publish ports. For more Docker basics, see the docker run and docker exec guides.

ss Cheatsheet

Basic Syntax

Core ss command forms and output controls.

Command Description
ss Show non-listening sockets with an established connection
ss -a Show all sockets, listening and non-listening
ss -n Show numeric addresses and ports, no name resolution
ss -p Show the process that owns each socket
ss -s Show a summary of socket counts by type and state

Filter by Protocol

Restrict output to a single socket family.

Command Description
ss -t TCP sockets
ss -u UDP sockets
ss -x Unix domain sockets
ss -ta All TCP sockets, including listening
ss -4 IPv4 sockets only
ss -6 IPv6 sockets only

Listening Ports

Find services that are accepting connections.

Command Description
ss -l Show listening sockets only
ss -tl Listening TCP sockets
ss -ul Listening UDP sockets
ss -tulpn Listening TCP/UDP with process and numeric output
sudo ss -tlpn 'sport = :80' Find the process listening on TCP port 80

Connection State Filters

Narrow output to a specific TCP state.

Command Description
ss -tn state ESTABLISHED Established TCP connections
ss -tn state listening Listening TCP sockets
ss -tn state TIME-WAIT Connections in TIME-WAIT
ss -tn state CLOSE-WAIT Connections in CLOSE-WAIT
ss -tn state ESTABLISHED | tail -n +2 | wc -l Count established TCP connections

Address and Port Filters

Match sockets by source or destination.

Command Description
ss -tnp 'dport = :443' Filter by destination port
ss -tnp 'sport = :22' Filter by source port
ss -tn dst 192.168.1.5 Filter by remote address
ss -tn src 192.168.1.10 Filter by local address
sudo ss -tlpn sport = :8080 Find the process listening on port 8080

Process and Statistics

Tie sockets to processes and read summary counts.

Command Description
sudo ss -tp TCP sockets with process name and PID
sudo ss -tulpn Listening sockets with owning processes
ss -s Total sockets by transport and state
ss -tn TCP sockets with numeric addresses
ss -tn dst 203.0.113.10 All connections to a remote host

netstat to ss Translation

Map old netstat commands to their ss equivalents.

netstat Command ss Command
netstat -tuln ss -tuln
sudo netstat -tulnp sudo ss -tulpn
netstat -at ss -ta
netstat -ant | grep ESTABLISHED ss -tn state ESTABLISHED
netstat -s ss -s

Troubleshooting

Common ss issues and quick fixes.

Issue Check
-p shows no process Run with sudo to see sockets owned by other users
Filters return nothing Quote the expression and verify sport versus dport
Service names hide ports Add -n to keep numeric ports
Output too broad Start with -t, -u, or a state filter, then narrow
Port match too broad Use a built-in filter, such as ss -tlpn 'sport = :80'

Related Guides

Use these guides for full walkthroughs and related tools.

Guide Description
ss Command in Linux Full ss guide with examples
netstat Command in Linux The legacy tool ss replaces
ip Command in Linux Modern routes and interface management
How to Check Listening Ports in Linux Compare ss, netstat, and lsof
lsof Command in Linux Tie sockets and files back to processes

How to Install Nginx on Debian 13

Nginx is a fast, lightweight web server that serves static content, terminates TLS, and proxies requests to application servers behind it. Debian 13 Trixie ships Nginx in its main apt repository, which makes the install a single command on a fresh system.

This guide explains how to install Nginx on Debian 13, open the firewall, manage the service with systemd, and create a basic server block for your first site.

Prerequisites

You will need a Debian 13 server with a sudo user. UFW should be installed and active so the install is firewalled by default.

Quick Reference

Task Command
Install Nginx sudo apt install nginx
Allow HTTP and HTTPS in UFW sudo ufw allow 'Nginx Full'
Start the service sudo systemctl start nginx
Enable on boot sudo systemctl enable nginx
Reload after config change sudo systemctl reload nginx
Test configuration sudo nginx -t
Show service status systemctl status nginx
Site config directory /etc/nginx/sites-available/
Enabled-site symlinks /etc/nginx/sites-enabled/
Default document root /var/www/html/

Step 1: Update the Package Index

Refresh apt so it sees the latest package metadata before the install:

Terminal
sudo apt update

The output lists the repositories that were checked. A clean run finishes without errors.

Step 2: Install Nginx

Install the nginx package:

Terminal
sudo apt install nginx

The default install pulls in the nginx-common package, the systemd unit, and the standard set of modules. apt also creates the www-data user that owns the document root and runs the worker processes.

Confirm the version:

Terminal
nginx -v
output
nginx version: nginx/1.26.3

The exact version may differ as Debian publishes point updates, but 1.26.x is the version that ships with Debian 13 Trixie.

Step 3: Allow HTTP and HTTPS in UFW

The Nginx package registers application profiles with UFW. Allow both HTTP and HTTPS in one rule:

Terminal
sudo ufw allow 'Nginx Full'

Verify the rule is in place:

Terminal
sudo ufw status
output
Status: active
To Action From
-- ------ ----
Nginx Full ALLOW Anywhere
OpenSSH ALLOW Anywhere

If you only need HTTP for now, replace the rule with Nginx HTTP. Switch to Nginx Full later when you add TLS.

Step 4: Manage the Nginx Service with systemd

The Nginx package starts the service at install time and enables it for boot. Confirm both:

Terminal
systemctl is-active nginx
systemctl is-enabled nginx
output
active
enabled

If the service is not running, start it:

Terminal
sudo systemctl start nginx

The most common service operations are restart, reload, and status:

Terminal
sudo systemctl restart nginx
sudo systemctl reload nginx
systemctl status nginx

reload re-reads the configuration without dropping connections and is the right command after editing a server block. restart stops and starts the service, which is only needed when the binary itself changes.

Step 5: Verify the Default Page

Open a browser and visit the server’s IP, or use curl:

Terminal
curl -I http://localhost
output
HTTP/1.1 200 OK
Server: nginx/1.26.3
Content-Type: text/html

A 200 OK response confirms Nginx is serving the default page. The default document root is /var/www/html/ and contains an index.nginx-debian.html welcome page.

Step 6: Create Your First Server Block

Server blocks are how Nginx serves multiple sites from a single host. Create the document root for the new site:

Terminal
sudo mkdir -p /var/www/example.com/html
sudo chown -R $USER:$USER /var/www/example.com/html
echo '<h1>Hello from example.com</h1>' | sudo tee /var/www/example.com/html/index.html

Create a server block file:

Terminal
sudo nano /etc/nginx/sites-available/example.com

Paste the following configuration and adjust the domain name:

/etc/nginx/sites-available/example.comnginx
server {
 listen 80;
 listen [::]:80;

 server_name example.com www.example.com;

 root /var/www/example.com/html;
 index index.html;

 location / {
 try_files $uri $uri/ =404;
 }
}

Enable the site by creating a symlink in sites-enabled:

Terminal
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

Test the configuration before reloading:

Terminal
sudo nginx -t
output
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload Nginx so the new server block takes effect:

Terminal
sudo systemctl reload nginx

Visit the domain to verify the site loads. If the domain does not yet point at the server, edit /etc/hosts on a workstation or use curl --resolve example.com:80:SERVER_IP http://example.com/ to test.

Important Nginx Files and Directories

The default Debian layout splits configuration across several locations:

  • /etc/nginx/nginx.conf is the main configuration file.
  • /etc/nginx/sites-available/ holds the server block templates.
  • /etc/nginx/sites-enabled/ holds symlinks to the active server blocks.
  • /etc/nginx/snippets/ is for shared configuration snippets such as TLS hardening.
  • /var/log/nginx/access.log and /var/log/nginx/error.log are the log files.
  • /var/www/html/ is the default document root.

Troubleshooting

Default page does not load
Check that the service is running with systemctl status nginx. If it is not, run sudo nginx -t to spot configuration errors. UFW may also block port 80; confirm with sudo ufw status.

bind() to 0.0.0.0:80 failed (98: Address already in use)
Another web server is bound to port 80. Apache is the usual culprit on Debian. Stop it with sudo systemctl stop apache2 and disable it on boot with sudo systemctl disable apache2.

Server block returns 404
The symlink in sites-enabled is missing or points to the wrong file. List it with ls -l /etc/nginx/sites-enabled/. Recreate the symlink and reload Nginx.

nginx -t reports duplicate default_server
Two server blocks set listen 80 default_server. Only one can be the default. Remove the keyword from the new server block and reload.

Conclusion

Nginx on Debian 13 is one apt command away. Stick with the Debian package for general-purpose use, and switch to the upstream nginx.org repository when you need a feature only the latest mainline release supports.

How to Install Podman on Ubuntu: Rootless Container Alternative to Docker

Podman is a daemonless container engine that runs OCI containers without a long-lived root process. Each container is started as a regular user-owned process, which removes the privileged daemon that Docker relies on and makes containers safer to run on shared servers. The command-line surface mirrors Docker almost exactly, so most Dockerfiles, image references, and docker run patterns work with little to no change.

This guide explains how to install Podman on Ubuntu, configure rootless mode, run your first container, and use Podman as a drop-in replacement for the Docker CLI.

Quick Reference

Task Command
Install Podman sudo apt install podman
Run a container podman run -d --name web -p 8080:80 nginx
List containers podman ps -a
Build an image podman build -t myimage .
List images podman images
Pull an image podman pull alpine:latest
Stop and remove podman rm -f web
Inspect rootless setup podman info

Install Podman

Podman has been part of the Ubuntu universe repository since 20.10, so on every supported release you can install it through apt without adding a third-party PPA.

Update the package index, then install:

Terminal
sudo apt update
sudo apt install podman

Confirm the version:

Terminal
podman --version
output
podman version 5.7.0

Ubuntu 26.04 ships Podman 5.7 from the universe repository. Ubuntu 24.04 ships the 4.9 series, while Ubuntu 22.04 ships the 3.4 series, so the exact version depends on the Ubuntu release you are using.

Verify the Rootless Setup

Podman supports both root and rootless modes, and the rootless mode is the one most users want. Run podman info as your regular user and look for rootless: true:

Terminal
podman info | head -20
output
host:
arch: amd64
cgroupManager: systemd
...
security:
rootless: true

Two pieces have to be in place for rootless containers to work: subordinate UID/GID mappings, and a user namespace. Ubuntu sets both up automatically on first run, but you can confirm with:

Terminal
cat /etc/subuid /etc/subgid
output
sara:100000:65536
sara:100000:65536

Each user gets a range of 65,536 subordinate UIDs and GIDs that Podman uses to map container processes to non-overlapping host IDs. If the file is missing your user, add an entry with sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 sara and run podman system migrate to apply the change.

Run Your First Container

Pull and run an Nginx container in detached mode, exposing port 8080 on the host:

Terminal
podman run -d --name web -p 8080:80 nginx
output
Resolved "nginx" as an alias (/etc/containers/registries.conf.d/shortnames.conf)
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob sha256:...
Writing manifest to image destination
Storing signatures
2b9bd4e1f3...

Check that the container is running:

Terminal
podman ps
output
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2b9bd4e1f3a8 docker.io/library/nginx:latest nginx -g daemon o... 10 seconds ago Up 10 seconds 0.0.0.0:8080->80/tcp web

Hit the published port from the host:

Terminal
curl -I http://localhost:8080
output
HTTP/1.1 200 OK
Server: nginx/1.27.4

The response confirms that the user-mode network stack forwarded the request into the container correctly. Stop and remove the container with:

Terminal
podman rm -f web

Configure Registries

By default Podman searches a configured list of registries when you run podman pull nginx (without a registry prefix). The list lives in /etc/containers/registries.conf. To prefer Docker Hub, edit the file:

Terminal
sudo nano /etc/containers/registries.conf

Set the unqualified-search registries:

/etc/containers/registries.conftoml
unqualified-search-registries = ["docker.io", "quay.io"]

Save and exit. From now on podman pull alpine resolves to docker.io/library/alpine:latest, which matches Docker’s default behavior. Save yourself confusion by always pulling with the full reference (podman pull docker.io/library/alpine) in scripts and CI pipelines.

Use Podman as a Docker Replacement

If you already have shell history, Makefiles, or scripts that call docker, install the alias package and keep them working:

Terminal
sudo apt install podman-docker

The package adds a /usr/bin/docker symlink that points to podman, plus a stub that suppresses the daemon-not-running warning. Test it:

Terminal
docker run --rm hello-world
output
Hello from Docker!

Behind the scenes, docker run invoked podman run. Most everyday commands (build, pull, push, images, ps, logs, exec) work identically. The differences show up around Docker Compose and the daemon socket; Podman 4 ships its own socket that Docker Compose can target by setting DOCKER_HOST.

Build an Image with a Dockerfile

Podman builds OCI images from a standard Dockerfile, no changes required. Create a small Flask app to demonstrate:

Terminal
mkdir hello-podman && cd hello-podman
nano app.py
app.pypy
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
 return "Hello from Podman!"


if __name__ == "__main__":
 app.run(host="0.0.0.0", port=5000)

Write the Dockerfile:

Dockerfiledockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir flask
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]

Build the image:

Terminal
podman build -t hello-podman .
output
STEP 1/6: FROM python:3.12-slim
STEP 2/6: WORKDIR /app
...
Successfully tagged localhost/hello-podman:latest

Run a container from the new image:

Terminal
podman run -d --name hello -p 5000:5000 hello-podman
curl http://localhost:5000
output
Hello from Podman!

The image and container both live in the user’s storage (~/.local/share/containers), so no sudo is involved at any step.

Run Pods of Containers

The name “Podman” comes from its support for Kubernetes-style pods: groups of containers that share a network namespace. Create an empty pod, then add containers to it:

Terminal
podman pod create --name webapp -p 8080:80
podman run -d --pod webapp --name redis redis:7
podman run -d --pod webapp --name nginx nginx

Inside the pod, nginx can reach Redis at localhost:6379 because they share the network namespace. List pods with:

Terminal
podman pod ps
output
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
8f4d7b8c0e1f webapp Running 20 seconds ago c2a8f9b1d3e4 3

The count of three includes an infra container that holds the shared namespaces.

Run Podman as a systemd Service

To start a container at boot in rootless mode, use a Quadlet container unit. Quadlet is Podman’s current systemd integration method and keeps the container definition in a small, readable file.

Terminal
mkdir -p ~/.config/containers/systemd
nano ~/.config/containers/systemd/web.container

Add the container definition:

~/.config/containers/systemd/web.containerini
[Unit]
Description=Rootless Nginx container

[Container]
Image=docker.io/library/nginx:latest
ContainerName=web
PublishPort=8080:80

[Service]
Restart=always

[Install]
WantedBy=default.target

Reload the user systemd manager, then start the generated service:

Terminal
systemctl --user daemon-reload
systemctl --user start web.service

Enable lingering so the user services keep running after logout:

Terminal
sudo loginctl enable-linger sara

The [Install] section tells the Podman systemd generator to attach the service to the user’s default target. The container now starts without root or a system-wide daemon.

Troubleshooting

Error: short-name "nginx" did not resolve to an alias
The unqualified-search-registries list is empty. Edit /etc/containers/registries.conf and add at least docker.io, or pull with the full reference (podman pull docker.io/library/nginx).

ERRO[0000] cannot find UID/GID for user
The current user has no entry in /etc/subuid or /etc/subgid. Run sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER, then podman system migrate.

Ports under 1024 refuse to bind in rootless mode
The kernel restricts low ports for non-root users. Either publish to a high port (-p 8080:80), or allow your user to bind low ports with sudo sysctl net.ipv4.ip_unprivileged_port_start=80.

FAQ

Is Podman a drop-in Docker replacement?
For everyday run, build, pull, push, and exec workflows, yes. Compose and the long-lived socket need extra setup. Most CI jobs only need the CLI and switch over without changes.

Does Podman work with Docker Compose?
Yes. Start the Podman socket with systemctl --user enable --now podman.socket, export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock, and run docker compose up.

Are Podman images compatible with Docker?
Yes. Both produce OCI-compliant images, so an image built with podman build runs unchanged under Docker, and vice versa.

Conclusion

Podman gives you the Docker workflow without the daemon and without root, which is often what you want on a multi-user server or a developer workstation. Install it from apt, alias docker if you have existing scripts, and the rest of the CLI feels familiar.

For follow-up reading, see our guides on installing Docker on Ubuntu 26.04 and building Docker images with a Dockerfile .

Bash String Manipulation: Substring, Replace, Length, and More

When you write Bash scripts, many small text tasks do not need sed, awk, or another external command. Parameter expansion can extract part of a string, replace text, strip a prefix, or change case directly inside ${}.

This guide covers the most useful string operations with practical examples for each one.

String Length

Get the number of characters in a variable using ${#variable}:

Terminal
filename="backup-2026-04-14.tar.gz"
echo "${#filename}"
output
24

This returns the character count. For strings that contain only ASCII characters, the character count and byte count are the same.

Extracting a Substring

Use ${variable:offset:length} to extract part of a string:

Terminal
str="Hello, World"
echo "${str:7:5}"
output
World

The offset is zero-based, so 7 starts at the eighth character. 5 is how many characters to return.

Omit the length to extract from the offset to the end of the string:

Terminal
echo "${str:7}"
output
World

Negative offsets count from the end of the string. Note the space before the minus sign; it is required to avoid being interpreted as the :- default value syntax:

Terminal
echo "${str: -5}"
output
World

Replacing a Substring

Use ${variable/pattern/replacement} to replace the first match of a pattern:

Terminal
path="/var/log/app/error.log"
echo "${path/log/logs}"
output
/var/logs/app/error.log

Only the first occurrence is replaced. To replace every occurrence, double the forward slash:

Terminal
echo "${path//log/logs}"
output
/var/logs/app/error.logs

Leave the replacement empty to delete a substring:

Terminal
version="v1.2.3"
echo "${version/v/}"
output
1.2.3

The pattern supports glob characters: * matches any sequence of characters, ? matches a single character, and [...] matches a character class.

Removing a Prefix

Use # to remove a prefix from the beginning of a string. The shortest matching prefix is removed:

Terminal
file="photo.backup.tar.gz"
echo "${file#*.}"
output
backup.tar.gz

Use ## to remove the longest possible matching prefix:

Terminal
echo "${file##*.}"
output
gz

A common use is stripping a directory path to get just the filename:

Terminal
filepath="/home/user/documents/report.pdf"
echo "${filepath##*/}"
output
report.pdf

Removing a Suffix

Use % to strip a suffix from the end of a string. The shortest match is removed:

Terminal
file="photo.backup.tar.gz"
echo "${file%.*}"
output
photo.backup.tar

Use %% for the longest match:

Terminal
echo "${file%%.*}"
output
photo

Combining prefix and suffix removal is a clean way to extract a bare filename from a path:

Terminal
filepath="/home/user/documents/report.pdf"
base="${filepath##*/}"
name="${base%.*}"
echo "$name"
output
report

Changing Case

Convert a string to uppercase with ^^:

Terminal
greeting="hello, world"
echo "${greeting^^}"
output
HELLO, WORLD

Convert to lowercase with ,,:

Terminal
input="Hello World"
echo "${input,,}"
output
hello world

To capitalize only the first character, use a single ^:

Terminal
word="linux"
echo "${word^}"
output
Linux

To lowercase only the first character, use a single ,:

Terminal
word="LINUX"
echo "${word,}"
output
lINUX

Case conversion requires Bash 4.0 or newer. The default shell on older macOS systems ships with Bash 3.2 and does not support these operators. As a portable alternative, use tr:

Terminal
echo "$greeting" | tr '[:lower:]' '[:upper:]'

Checking if a String Contains a Substring

Use a glob pattern inside [[ ]] to test whether a string contains another string:

Terminal
str="Running on Linux kernel 6.8"
if [[ "$str" == *"Linux"* ]]; then
 echo "Linux detected"
fi
output
Linux detected

The * wildcards match anything before and after the search pattern. The comparison is case-sensitive. To check case-insensitively, convert both sides to the same case first using ,, before comparing.

For a more detailed walkthrough of substring detection, including case-insensitive matching and edge cases, see How to Check if a String Contains a Substring in Bash . For more on [[ ]] conditionals and string comparison operators, see the guide on Bash comparison operators .

Quick Reference

For a printable quick reference, see the Bash cheatsheet .

Operation Syntax
String length ${#var}
Substring from offset ${var:offset}
Substring with length ${var:offset:length}
Replace first match ${var/pattern/replacement}
Replace all matches ${var//pattern/replacement}
Delete substring ${var/pattern/}
Remove shortest prefix ${var#pattern}
Remove longest prefix ${var##pattern}
Remove shortest suffix ${var%pattern}
Remove longest suffix ${var%%pattern}
Uppercase all ${var^^}
Lowercase all ${var,,}
Uppercase first char ${var^}
Lowercase first char ${var,}

Conclusion

Bash parameter expansion handles most day-to-day string operations without extra commands, which keeps scripts shorter and faster. For more on working with strings, see the guides on Bash string concatenation and splitting strings by delimiter .

nmcli Cheatsheet

Basic Syntax

Use this structure for most nmcli commands.

Command Description
nmcli [OPTIONS] OBJECT COMMAND General command syntax
nmcli OBJECT help Show help for an object
nmcli --version Show nmcli version
nmcli -t ... Use terse output for scripts
nmcli -f FIELD1,FIELD2 ... Select output fields
nmcli -g FIELD ... Print field values only

General Status

Check NetworkManager state and hostname settings.

Command Description
nmcli general status Show overall NetworkManager status
nmcli networking connectivity Check internet connectivity state
nmcli general hostname Show system hostname
sudo nmcli general hostname server01 Set system hostname
nmcli general permissions Show NetworkManager permissions
nmcli general logging Show logging level and domains

Devices

Inspect and control network interfaces.

Command Description
nmcli device status List devices and connection state
nmcli device show eth0 Show detailed device properties
nmcli -f GENERAL,IP4 device show eth0 Show selected device sections
sudo nmcli device connect eth0 Connect a device
sudo nmcli device disconnect eth0 Disconnect a device
nmcli device reapply eth0 Reapply active profile settings

Connection Profiles

Manage saved NetworkManager profiles.

Command Description
nmcli connection show List all profiles
nmcli connection show --active List active profiles
nmcli connection show "home-wifi" Show one profile
sudo nmcli connection up "home-wifi" Activate a profile
sudo nmcli connection down "home-wifi" Deactivate a profile
sudo nmcli connection delete "home-wifi" Delete a saved profile

Wi-Fi

Scan networks and connect to wireless profiles.

Command Description
nmcli radio wifi Show Wi-Fi radio state
sudo nmcli radio wifi on Turn Wi-Fi on
sudo nmcli radio wifi off Turn Wi-Fi off
nmcli device wifi list Scan visible Wi-Fi networks
sudo nmcli device wifi connect "SSID" password "PASSWORD" Connect with password
sudo nmcli --ask device wifi connect "SSID" Prompt for Wi-Fi password
sudo nmcli device wifi connect "SSID" password "PASSWORD" hidden yes Connect to hidden network

Static IPv4

Set or remove manual IPv4 settings on a profile.

Command Description
sudo nmcli connection modify "Wired connection 1" ipv4.method manual Use manual IPv4
sudo nmcli connection modify "Wired connection 1" ipv4.addresses 192.168.1.50/24 Set static address
sudo nmcli connection modify "Wired connection 1" ipv4.gateway 192.168.1.1 Set gateway
sudo nmcli connection modify "Wired connection 1" ipv4.dns "1.1.1.1 9.9.9.9" Set DNS servers
sudo nmcli connection up "Wired connection 1" Apply profile changes
nmcli -g IP4.ADDRESS,IP4.GATEWAY connection show "Wired connection 1" Check IPv4 settings

DHCP and DNS

Return to DHCP or adjust DNS servers.

Command Description
sudo nmcli connection modify "Wired connection 1" ipv4.method auto Use DHCP for IPv4
sudo nmcli connection modify "Wired connection 1" ipv4.addresses "" Clear static address
sudo nmcli connection modify "Wired connection 1" ipv4.gateway "" Clear static gateway
sudo nmcli connection modify "Wired connection 1" ipv4.dns "" Clear static DNS
sudo nmcli connection modify "Wired connection 1" +ipv4.dns 8.8.8.8 Append DNS server
sudo nmcli connection modify "Wired connection 1" -ipv4.dns 8.8.8.8 Remove DNS server

Add Profiles

Create new wired and wireless profiles.

Command Description
sudo nmcli connection add type ethernet ifname eth0 con-name "wired-dhcp" Add wired DHCP profile
sudo nmcli connection add type ethernet ifname eth0 con-name "wired-static" ip4 192.168.1.50/24 gw4 192.168.1.1 Add wired static profile
sudo nmcli connection add type wifi ifname wlan0 con-name "office-wifi" ssid "Office" Add Wi-Fi profile
sudo nmcli connection modify "office-wifi" wifi-sec.key-mgmt wpa-psk Set WPA-PSK security
sudo nmcli connection modify "office-wifi" wifi-sec.psk "PASSWORD" Set Wi-Fi password
sudo nmcli connection up "wired-static" Activate new profile

Scripting Output

Use stable output formats in scripts.

Command Description
nmcli -t connection show --active Terse active profile list
nmcli -t -f NAME,DEVICE connection show --active Active names and devices
nmcli -g GENERAL.STATE device show eth0 Print device state value
nmcli -g IP4.ADDRESS device show eth0 Print IPv4 address values
nmcli -f DEVICE,TYPE,STATE device status Select device status fields
nmcli -m multiline connection show "home-wifi" Multiline profile details

Monitor and Reload

Watch changes and reload profile files.

Command Description
nmcli monitor Watch NetworkManager events
nmcli device monitor eth0 Watch one device
sudo nmcli connection reload Reload profile files from disk
sudo nmcli general reload Reload NetworkManager configuration
sudo nmcli general reload dns-rc Rewrite DNS resolver configuration
journalctl -u NetworkManager -n 100 Show recent NetworkManager logs

Troubleshooting

Fast checks for common NetworkManager issues.

Issue Check
NetworkManager is not running sudo systemctl start NetworkManager
Wi-Fi list is empty sudo nmcli radio wifi on and rfkill list
Wrong profile is active nmcli connection show --active
Static IP did not apply sudo nmcli connection up "PROFILE"
DNS is not working nmcli -g IP4.DNS device show eth0
Permission denied Run the command with sudo

Related Guides

Use these articles for detailed networking workflows.

Guide Description
nmcli Command in Linux Complete nmcli command guide
Linux ip Command with Examples Interface, address, and route management
How to Configure Static IP Address on Ubuntu Netplan and desktop static IP setup
How to Use the dig Command DNS lookup and troubleshooting
SSH Command in Linux Remote shell access and options

nmcli Command in Linux: NetworkManager CLI Reference

When a Linux system needs a network change from a terminal, nmcli is usually the cleanest way to make it. It controls NetworkManager, the service that handles wired, wireless, VPN, and other connection profiles on many Linux desktops and servers.

This guide explains how to inspect device status, list and edit connections, join Wi-Fi networks, assign a static IP address, and get script-friendly output with nmcli.

Before You Begin

Before changing network settings, confirm that NetworkManager is installed and running:

Terminal
systemctl status NetworkManager

On distributions that use a different network stack, install NetworkManager from the package manager and enable the service before continuing. Most Ubuntu, Fedora, and Arch desktops have it preinstalled.

If you are connected over SSH , be careful with commands that disconnect an interface, change an address, or bring a profile down. A wrong gateway, IP address, or device name can drop the remote session.

nmcli Syntax

The general nmcli syntax is:

txt
nmcli [OPTIONS] OBJECT { COMMAND | help }

OBJECT is the part of NetworkManager you want to inspect or change. The most common objects are:

  • general - show overall NetworkManager status and hostname settings
  • networking - enable, disable, or check networking
  • radio - control Wi-Fi and WWAN radio switches
  • device - show and manage network interfaces
  • connection - show and manage saved connection profiles
  • monitor - watch NetworkManager events in real time

You can abbreviate most objects, for example nmcli con show instead of nmcli connection show, but the full form is clearer in scripts and documentation.

Show General Status

The general subcommand reports the overall NetworkManager state, the hostname, and whether networking is enabled:

Terminal
nmcli general status
output
STATE CONNECTIVITY WIFI-HW WIFI WWAN-HW WWAN
connected full enabled enabled enabled enabled

Read or change the system hostname through the same subcommand:

Terminal
nmcli general hostname
sudo nmcli general hostname server01

The second form writes the new hostname to /etc/hostname and updates the kernel value without a reboot. For more hostname options, see the guide on changing the hostname in Linux .

Manage Devices

A device in NetworkManager is a physical or virtual interface, for example eth0, wlan0, or wg0. List every known device with:

Terminal
nmcli device status
output
DEVICE TYPE STATE CONNECTION
eth0 ethernet connected Wired connection 1
wlan0 wifi connected home-wifi
lo loopback unmanaged --

Show detailed properties of a single device, including the MAC address, driver, and current IP configuration:

Terminal
nmcli device show eth0

Bring an interface up or down without changing the underlying connection profile:

Terminal
sudo nmcli device connect eth0
sudo nmcli device disconnect eth0

disconnect stops the connection but keeps the profile available for the next connect call. Do not disconnect the device that carries your current SSH session unless you have another access path.

Manage Connection Profiles

NetworkManager stores each network as a connection profile under /etc/NetworkManager/system-connections/. List every profile:

Terminal
nmcli connection show
output
NAME UUID TYPE DEVICE
Wired connection 1 3f4e... ethernet eth0
home-wifi 7a92... wifi wlan0

Activate a saved profile by name:

Terminal
sudo nmcli connection up "home-wifi"

Deactivate it without deleting the profile:

Terminal
sudo nmcli connection down "home-wifi"

Delete a profile when it is no longer needed:

Terminal
sudo nmcli connection delete "home-wifi"

Deleting a profile removes the saved NetworkManager configuration. If you only want to stop using it for the moment, bring it down instead.

Connect to a Wi-Fi Network

Scan for available networks. The radio must be on for the scan to return results:

Terminal
nmcli device wifi list

Join an open or WPA2 network in one call. NetworkManager creates a new profile and stores the password in the system keyring:

Terminal
sudo nmcli device wifi connect "home-wifi" password "secret-passphrase"

Avoid putting real Wi-Fi passwords in shell history, shared scripts, or configuration repositories. For interactive use, run sudo nmcli --ask device wifi connect "home-wifi" so nmcli prompts for the password instead.

For hidden networks, add the hidden yes flag so NetworkManager probes the SSID directly:

Terminal
sudo nmcli device wifi connect "office-hidden" password "secret-passphrase" hidden yes

Turn the radio on or off without removing saved profiles:

Terminal
sudo nmcli radio wifi off
sudo nmcli radio wifi on

Configure a Static IP Address

Static addressing is one of the most common scripted changes on a server. The example below sets a static IPv4 address, gateway, and DNS servers on the existing Wired connection 1 profile:

Terminal
sudo nmcli connection modify "Wired connection 1" \
 ipv4.method manual \
 ipv4.addresses 192.168.1.50/24 \
 ipv4.gateway 192.168.1.1 \
 ipv4.dns "1.1.1.1 9.9.9.9"

Apply the change by reactivating the profile:

Terminal
sudo nmcli connection up "Wired connection 1"

If you are changing a remote machine, confirm the address, prefix, gateway, and DNS values before running connection up. A wrong value can make the host unreachable. On Ubuntu systems that use Netplan instead, see the guide on configuring a static IP address on Ubuntu .

Switch back to DHCP at any point with:

Terminal
sudo nmcli connection modify "Wired connection 1" \
 ipv4.method auto \
 ipv4.addresses "" \
 ipv4.gateway "" \
 ipv4.dns ""
sudo nmcli connection up "Wired connection 1"

The empty values clear the manual entries that would otherwise stay on the profile.

Add a New Wired Connection

When the default profile is missing or has been deleted, create a fresh ethernet profile:

Terminal
sudo nmcli connection add type ethernet ifname eth0 con-name "wired-static" \
 ipv4.method manual \
 ipv4.addresses 192.168.1.50/24 \
 ipv4.gateway 192.168.1.1 \
 ipv4.dns 1.1.1.1
sudo nmcli connection up "wired-static"

This pattern works well in provisioning scripts when you first check whether the profile exists, or when you intentionally replace a known profile. If you only need to change an existing profile, use connection modify instead of deleting and recreating it.

Set DNS Servers

You can change DNS servers without changing the IP address method. For example, to set Cloudflare and Quad9 DNS on a wired profile:

Terminal
sudo nmcli connection modify "Wired connection 1" ipv4.dns "1.1.1.1 9.9.9.9"
sudo nmcli connection up "Wired connection 1"

To append another DNS server instead of replacing the whole list, prefix the property with +:

Terminal
sudo nmcli connection modify "Wired connection 1" +ipv4.dns 8.8.8.8

After changing DNS, verify name resolution with a lookup tool such as dig or nslookup .

Common Examples

Show only the active connections in a parseable format. The -t flag enables terse output and -f selects fields:

Terminal
nmcli -t -f NAME,DEVICE connection show --active

Watch interface events in real time, useful when a connection drops during a remote session:

Terminal
nmcli monitor

Reload all profile files from disk after editing them by hand:

Terminal
sudo nmcli connection reload

For low-level interface and route inspection outside NetworkManager, pair nmcli with the ip command .

Quick Reference

For a printable quick reference, see the nmcli cheatsheet .

Task Command
Show overall status nmcli general status
Show syntax help nmcli OBJECT help
List devices nmcli device status
List connection profiles nmcli connection show
Activate a profile sudo nmcli connection up <name>
Deactivate a profile sudo nmcli connection down <name>
Scan Wi-Fi networks nmcli device wifi list
Join a Wi-Fi network sudo nmcli device wifi connect <ssid> password <pwd>
Prompt for Wi-Fi password sudo nmcli --ask device wifi connect <ssid>
Set static IPv4 sudo nmcli connection modify <name> ipv4.method manual ipv4.addresses <ip/cidr>
Set DNS servers sudo nmcli connection modify <name> ipv4.dns "<a> <b>"
Switch back to DHCP sudo nmcli connection modify <name> ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""
Reload profile files sudo nmcli connection reload
Watch events nmcli monitor

Troubleshooting

Error: NetworkManager is not running
Start the service with sudo systemctl start NetworkManager and enable it at boot with sudo systemctl enable NetworkManager. On a fresh server install, the package may not be present yet.

Wi-Fi list is empty after nmcli device wifi list
The radio is most likely off. Run sudo nmcli radio wifi on, and check that no rfkill switch is blocking the device with rfkill list.

A new static IP does not take effect
NetworkManager applies the change only after the connection is reactivated. Run sudo nmcli connection up <name> after connection modify. If a stale DHCP lease is still in place, run sudo nmcli connection down <name> first.

Not authorized to control networking
The operation requires administrator privileges or a PolicyKit rule that allows the current user. Run the command with sudo, or ask the system administrator to grant the needed NetworkManager permissions.

A profile keeps reverting to DHCP after reboot
A second profile or a cloud-init configuration may be overwriting it. Check /etc/NetworkManager/system-connections/ and /etc/netplan/ for conflicting files.

Conclusion

nmcli gives you a scriptable way to inspect NetworkManager state, activate profiles, join Wi-Fi networks, and make persistent IP and DNS changes. For deeper network troubleshooting, use it alongside ip, ss, dig, and the system logs so you can see both the saved profile and the live kernel state.

timedatectl Command in Linux: Manage Time, Date, and Time Zone

Wrong system time can break TLS checks, make cron jobs run at the wrong hour, and leave logs with misleading timestamps. On systemd-based Linux systems, timedatectl is the standard tool for checking and changing time settings.

timedatectl is part of systemd and is available on most systemd-based distributions. It handles time zones, NTP synchronization, RTC settings, and manual time adjustments from the command line. This guide explains how to use it with practical examples.

Syntax

txt
timedatectl [OPTIONS] COMMAND

When called without any arguments, timedatectl shows the current status, which is the same as running timedatectl status.

Check Current Time and Date

Run timedatectl with no arguments to see the full time status:

Terminal
timedatectl
output
 Local time: Tue 2026-04-14 12:00:00 CEST
Universal time: Tue 2026-04-14 10:00:00 UTC
RTC time: Tue 2026-04-14 10:00:00
Time zone: Europe/Berlin (CEST, +0200)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no

The output shows the local time in your configured time zone, the same moment in UTC, and the hardware clock (RTC) time. System clock synchronized: yes means NTP has synced the clock recently. NTP service: active means systemd-timesyncd (or another NTP daemon) is running and managing synchronization.

If RTC in local TZ shows yes, the hardware clock is set to local time rather than UTC. Most Linux systems keep the hardware clock in UTC, which avoids daylight saving time complications when rebooting.

Set the Time Zone

To set the system time zone, use set-timezone followed by a time zone identifier:

Terminal
sudo timedatectl set-timezone America/New_York

After running this, timedatectl shows the new time zone immediately. The change persists across reboots.

If you do not know the exact time zone identifier, use list-timezones:

Terminal
timedatectl list-timezones

The list is long, so pipe it through grep to filter by region:

Terminal
timedatectl list-timezones | grep America
output
America/Adak
America/Anchorage
America/Anguilla
America/Antigua
America/Araguaina
America/Argentina/Buenos_Aires
...
America/New_York
...

Time zone identifiers follow the Region/City format, such as Europe/Paris, Asia/Tokyo, or Australia/Sydney. For a step-by-step walkthrough, see the guide on how to set or change the time zone in Linux .

Enable and Disable NTP Synchronization

NTP (Network Time Protocol) keeps the system clock accurate by periodically syncing it against internet time servers. timedatectl can enable or disable NTP management:

Terminal
sudo timedatectl set-ntp true
Terminal
sudo timedatectl set-ntp false

After enabling NTP, run timedatectl to confirm NTP service: active appears in the output.

Turn off NTP before setting the time manually. If NTP is active, timedatectl rejects direct clock changes.

Set the Time and Date Manually

To set the clock manually, you must first disable NTP synchronization:

Terminal
sudo timedatectl set-ntp false

Then set the time and date using the set-time subcommand. You can provide a full timestamp:

Terminal
sudo timedatectl set-time '2026-04-15 14:30:00'

To set only the time and keep the current date:

Terminal
sudo timedatectl set-time '14:30:00'

To set only the date and keep the current time:

Terminal
sudo timedatectl set-time '2026-04-15'

After setting the time, you can re-enable NTP to let the system sync automatically afterward:

Terminal
sudo timedatectl set-ntp true

Set the Hardware Clock Mode

The RTC, or hardware clock, can store time in UTC or in local time. Linux systems should normally keep the RTC in UTC because it avoids daylight saving time issues and works better on servers.

To make sure the hardware clock uses UTC, run:

Terminal
sudo timedatectl set-local-rtc 0

If you dual-boot with an operating system that expects the hardware clock to use local time, you can switch the RTC to local time:

Terminal
sudo timedatectl set-local-rtc 1

After changing the RTC mode, run timedatectl again and check the RTC in local TZ line. Use local RTC only when another operating system on the same machine requires it.

Check NTP Synchronization Status

For more detail about the NTP sync, use timesync-status. This subcommand is provided by systemd-timesyncd and shows the current server, offset, and polling interval:

Terminal
timedatectl timesync-status
output
 Server: 185.125.190.56 (ntp.ubuntu.com)
Poll interval: 34min 8s (min: 32s; max 34min 8s)
Leap: normal
Version: 4
Stratum: 2
Reference: 17.253.52.253
Precision: 1us (-19)
Root distance: 19.455ms (max: 5s)
Offset: -2.408ms
Delay: 29.124ms
Jitter: 5.716ms
Packet count: 5

Offset is how far your system clock drifted before the last sync. A small offset, under a few hundred milliseconds, is normal. A large offset can point to network problems or a misconfigured time source.

timesync-status only works when systemd-timesyncd is handling NTP. If your system uses a different NTP daemon such as chrony or ntpd, check its own status commands instead.

Machine-Parseable Output

The show subcommand prints the same information as status in a key-value format that is easier to parse in scripts:

Terminal
timedatectl show
output
Timezone=Europe/Berlin
LocalRTC=no
CanNTP=yes
NTP=yes
NTPSynchronized=yes
TimeUSec=Tue 2026-04-14 10:00:00 UTC
RTCTimeUSec=Tue 2026-04-14 10:00:00 UTC

Pass --property to extract a single value:

Terminal
timedatectl show --property=Timezone --value
output
Europe/Berlin

This is useful in automation and configuration management scripts where you need to check the current time zone without parsing the human-readable status output.

For timesync details in key-value format, use show-timesync:

Terminal
timedatectl show-timesync

This gives scripts access to systemd-timesyncd properties such as the current server name, poll interval, and root distance.

timedatectl Options

  • --no-ask-password - Do not prompt for authentication; use the invoking user’s credentials
  • --adjust-system-clock - When setting the RTC, also adjust the system clock accordingly
  • --no-pager - Print output without piping through a pager
  • -p NAME, --property=NAME - Show one named property from show output
  • -H ADDR, --host=ADDR - Execute the command on a remote host over SSH
  • -M MACHINE, --machine=MACHINE - Execute the command inside a local container

Quick Reference

For a printable quick reference, see the timedatectl cheatsheet .

Command Description
timedatectl Show current time, time zone, and NTP status
timedatectl show Show status in key-value format (script-friendly)
timedatectl list-timezones List all available time zone identifiers
sudo timedatectl set-timezone TZ Set the time zone (e.g., America/New_York)
sudo timedatectl set-ntp true Enable NTP synchronization
sudo timedatectl set-ntp false Disable NTP synchronization
sudo timedatectl set-time 'YYYY-MM-DD HH:MM:SS' Set time and date manually (NTP must be off)
sudo timedatectl set-local-rtc 0 Store the hardware clock in UTC
timedatectl timesync-status Show NTP sync detail from systemd-timesyncd
timedatectl show-timesync Show NTP sync detail in key-value format
timedatectl show --property=Timezone --value Print the current time zone name only

Troubleshooting

Failed to set time: Automatic time synchronization is enabled
NTP is active and has locked the clock. Disable NTP first with sudo timedatectl set-ntp false, then run your set-time command.

Failed to set time: Access denied
You need administrator privileges. Prepend sudo to the command.

Failed to connect to bus: No such file or directory
timedatectl communicates with systemd-logind over D-Bus. This error usually appears in minimal containers or chroot environments where systemd is not running. In those cases, set the time directly with the date command: sudo date -s '2026-04-15 14:30:00'.

timesync-status returns Failed to query server: Unit dbus-org.freedesktop.timesync1.service not found
systemd-timesyncd is not installed or not running. If your system uses chrony or ntpd instead, check their status with chronyc tracking or ntpq -p.

FAQ

What is the difference between timedatectl and the date command?
date reads and formats timestamps, and it can set the system clock with sudo date -s. timedatectl is the systemd tool that also manages time zones, the RTC, and NTP synchronization. On systemd-based systems, use timedatectl for time configuration and date for formatting timestamps.

Does timedatectl work on non-systemd distributions?
No. timedatectl is part of systemd and requires systemd-logind and D-Bus to be running. On systems without systemd (such as Alpine Linux with OpenRC), use the date command and edit /etc/localtime or /etc/timezone directly.

How do I check which NTP servers the system is using?
Run timedatectl timesync-status to see the current NTP server. To see the full list of configured servers, check /etc/systemd/timesyncd.conf. The NTP= and FallbackNTP= lines list the configured time servers.

How do I view historical time changes in system logs?
timedatectl itself does not keep a history, but the system journal does. Search for time-related events with:

Terminal
journalctl -u systemd-timesyncd

For more on reading system logs, see the journalctl guide .

Conclusion

For day-to-day administration, timedatectl is usually enough to check the current time settings, switch time zones, toggle NTP, and inspect RTC behavior. If you need distribution-specific time zone steps, see the guide on how to set or change the time zone in Linux .

How to Check CPU Usage in Linux

A Linux server tells you what its CPUs are doing through several built-in tools, each with a different angle. top and htop show the live picture. mpstat, vmstat, and sar from the sysstat package report numeric samples that suit scripts and dashboards. ps attributes usage to specific processes, and uptime summarizes the load over the last fifteen minutes.

This guide explains how to read CPU usage from each of those tools, which one to reach for in which situation, and how to interpret common output fields.

Quick Reference

Tool Best for Command
top Live snapshot, sort by CPU top
htop Interactive, per-core view htop
mpstat Per-core sampling for scripts mpstat -P ALL 1 5
vmstat System-wide CPU and memory vmstat -y 1 5
sar Current or saved CPU samples sar -u 1 5
ps Per-process attribution ps -eo pid,user,%cpu,comm --sort=-%cpu | head
uptime Load average summary uptime
nproc Logical core count nproc
lscpu CPU topology details lscpu

Method 1: top

The top command is available on most Linux systems without any extra packages. Run it without arguments to open the live view:

Terminal
top

The header summarizes the CPU lines:

output
%Cpu(s): 4.2 us, 1.3 sy, 0.0 ni, 93.8 id, 0.5 wa, 0.0 hi, 0.2 si, 0.0 st

The fields tell you where CPU time goes. us is user-space code, sy is kernel code, id is idle, wa is time waiting for I/O, and st is time stolen by the hypervisor on a virtual machine. A high wa value points at slow disks rather than a CPU bottleneck.

Press 1 while top is open to switch from the aggregate line to one row per logical CPU. Press P to sort processes by CPU usage, M to sort by memory, and q to quit.

Method 2: htop

htop is an interactive replacement for top. It is available in the default repositories of most Linux distributions, but it may not be installed by default.

On Ubuntu, Debian, and Derivatives:

Terminal
sudo apt install htop

On Fedora, RHEL, and Derivatives:

Terminal
sudo dnf install htop

Run it the same way:

Terminal
htop

htop shows a colored bar per CPU at the top, a memory and swap bar, and a sortable process table below. Click a column header or use the function keys to sort. Search for a process with F3, filter with F4, and send a signal to a selected process with F9.

Method 3: mpstat

mpstat is part of the sysstat package and is the right tool when you need numeric CPU samples per core. Install it once before using mpstat or sar.

On Ubuntu, Debian, and Derivatives:

Terminal
sudo apt install sysstat

On Fedora, RHEL, and Derivatives:

Terminal
sudo dnf install sysstat

Sample all cores at one-second intervals five times:

Terminal
mpstat -P ALL 1 5
output
03:14:00 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
03:14:01 PM all 3.27 0.00 0.75 0.00 0.00 0.00 0.00 0.00 0.00 95.98
03:14:01 PM 0 4.95 0.00 0.99 0.00 0.00 0.00 0.00 0.00 0.00 94.06
03:14:01 PM 1 1.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 98.00

The all row is the aggregate. Each numbered row is a single logical CPU. Use this output when one core is pinned at 100 percent and the others are idle, which can happen when an application is single-threaded.

Method 4: vmstat

vmstat is part of procps and combines CPU usage with memory, swap, and I/O. The first report normally shows averages since boot, so use -y when you want only interval samples. The columns at the right are the CPU summary:

Terminal
vmstat -y 1 5
output
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 7430080 311288 1843264 0 0 8 18 148 263 3 1 96 0 0
0 0 0 7430080 311288 1843264 0 0 0 0 127 226 1 0 99 0 0

r is the run queue length, which is how many processes are ready to use a CPU. A run queue consistently larger than the number of CPUs is a sign of CPU pressure. The cpu columns mirror the meaning of the top header.

Method 5: sar

sar, also from sysstat, can print current samples and read saved activity logs. On Ubuntu and Debian, enable history collection after installing sysstat:

Terminal
sudo sed -i 's/^ENABLED="false"/ENABLED="true"/' /etc/default/sysstat
sudo systemctl enable --now sysstat

The collector setup differs between distributions. If sar shows no saved history, check your distribution’s sysstat service or timer configuration.

Show current CPU samples:

Terminal
sar -u 1 5
output
03:15:01 PM CPU %user %nice %system %iowait %steal %idle
03:15:02 PM all 2.34 0.00 0.71 0.05 0.00 96.90
03:15:03 PM all 2.18 0.00 0.69 0.04 0.00 97.09

Each row is a sample interval. Combine with -f to read an older log, for example sar -u -f /var/log/sysstat/sa10 for the tenth of the month on systems that store logs in /var/log/sysstat/. This is the right tool when you need to confirm that a slowdown reported yesterday lined up with a CPU spike.

Method 6: ps

The ps command lists every process and lets you sort by CPU percentage. The pattern most admins reach for is:

Terminal
ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head
output
 PID USER %CPU %MEM COMMAND
1437 dejan 23.5 4.2 firefox
2049 dejan 11.2 2.8 node
1102 root 4.7 0.9 systemd-journal

The output is a snapshot at the moment ps ran. The %CPU value is the average CPU usage over the lifetime of the process, not an instantaneous reading. For a live view, repeat the command with watch:

Terminal
watch -n 2 'ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head'

watch reruns the command every two seconds, which gives you a refreshing top-five view without leaving the shell.

Method 7: uptime and Load Averages

The uptime command is the smallest of the lot. Without arguments it prints the system uptime and the load average:

Terminal
uptime
output
 15:04:23 up 3:17, 2 users, load average: 0.42, 0.35, 0.31

The three numbers are the average number of processes that are runnable or in uninterruptible sleep over the last 1, 5, and 15 minutes. Compare them with nproc:

Terminal
nproc
output
8

A load average that stays below the core count is healthy. A load average well above the core count means processes are queueing for CPU time. The 15-minute value is the trend; the 1-minute value reacts to short spikes.

Method 8: Capacity Context

Before you read CPU usage, know what the host has. lscpu summarizes the CPU model, sockets, cores, and threads. For a deeper hardware view, see the guide on checking CPU information in Linux .

Terminal
lscpu
output
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-9700K CPU @ 3.60 GHz
Thread(s) per core: 1
Core(s) per socket: 8
Socket(s): 1

Thread(s) per core: 1 means hyper-threading is disabled or not present, so each logical CPU is a physical core. A high CPU usage on a chip with eight physical cores is different from the same number on a four-core chip with hyper-threading.

Troubleshooting

mpstat: command not found
The sysstat package is not installed. Install it with sudo apt install sysstat on Ubuntu, Debian, and Derivatives, or sudo dnf install sysstat on Fedora, RHEL, and Derivatives.

sar reports Cannot open /var/log/sysstat/
Sample collection is not enabled. Set ENABLED="true" in /etc/default/sysstat and restart the service with sudo systemctl restart sysstat. Wait at least one collection interval before rerunning sar.

top shows more than 100% CPU for one process
The default mode shows percentage relative to a single CPU, so a multi-threaded process can exceed 100 percent. Press Shift+I to toggle Irix mode off, and the value normalizes across all CPUs.

Load average looks high but no process consumes CPU
Load includes processes in D state, which is uninterruptible sleep. Check ps -eo state,pid,comm | awk '$1 ~ /D/' to find processes blocked on I/O. The cause is usually a slow or stalled disk.

Per-core view in htop is empty
Old terminal sizes hide the CPU bar. Resize the window or set Display options inside htop with F2.

FAQ

Which tool should I check first?
Run top or htop for a live picture, then drop to mpstat and sar when you need numbers or history. ps answers the per-process question once you know there is a problem.

What does %steal mean?
%steal is the time the hypervisor took from the guest VM to run something else. A consistent non-zero %steal value on a VM means the host is over-committed and the guest cannot get the CPU it asked for.

How do I count CPUs from a script?
Use nproc for the logical CPU count, or nproc --all to include offline CPUs. Both return a single integer and are stable across distributions.

Is high CPU always bad?
No. A batch job that finishes faster on a CPU running at full capacity is healthier than the same job spread thinly over a long period. CPU is bad when the load average exceeds the core count for sustained periods or when interactive workloads time out.

Conclusion

Pick the tool that matches the question. top and htop answer “what is happening right now”. mpstat and vmstat answer “is the load even across cores”. sar answers “what happened last night”. Treat load averages as a trend, not a verdict, and always read CPU numbers in the context of the core count from nproc.

systemctl Command in Linux: Start, Stop, and Manage Services

Every modern Linux distribution runs a handful of services in the background at any given moment: an SSH daemon, a cron daemon, a web server, a database, a firewall. On systemd-based systems, the one tool you use to control all of them is systemctl.

systemctl is the command-line front end for systemd. It starts and stops services, enables them at boot, reloads unit files after an edit, and reports the status of each one. This guide walks through the commands you will reach for most often when managing services on a running system.

systemctl Syntax

The general syntax of the command is:

txt
systemctl [OPTIONS] COMMAND [UNIT...]

A UNIT is usually a service name such as nginx.service or cron.service. The .service suffix is optional when the unit type is unambiguous, so systemctl start nginx and systemctl start nginx.service do the same thing.

Most commands that change state (start, stop, enable, disable) require root privileges. Run them with sudo or as root.

Check the Status of a Service

Before you touch a service, check what it is doing. The status command prints the current state, recent log lines, the PID of the main process, and the path of the unit file:

Terminal
systemctl status nginx
output
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-04-13 09:12:04 CEST; 2h 3min ago
Docs: man:nginx(8)
Main PID: 1591 (nginx)
Tasks: 3 (limit: 4612)
Memory: 6.2M
CPU: 112ms
CGroup: /system.slice/nginx.service
├─1591 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
└─1592 "nginx: worker process"

The two lines to read first are Loaded and Active. Loaded tells you whether the unit file was found and whether it is set to start at boot (enabled, disabled, masked, or static). Active tells you what the service is doing right now (active (running), inactive (dead), failed, and so on).

Start and Stop Services

To start a service that is not running:

Terminal
sudo systemctl start nginx

The command returns silently when the service starts. Run systemctl status nginx afterwards to confirm.

To stop a running service:

Terminal
sudo systemctl stop nginx

Stopping a service only affects the current session. If the service is enabled at boot, it will come back the next time the system starts.

Restart and Reload

A restart fully stops the service and starts it again, which terminates all active connections. This is the command you use after a binary upgrade or any change that the service cannot pick up on its own:

Terminal
sudo systemctl restart nginx

A reload asks the service to re-read its configuration without dropping active connections. Whether this works depends on the service itself. Web servers such as Nginx and Apache support it well; some services do not define a reload action.

Terminal
sudo systemctl reload nginx

If the unit file supports it, reload-or-restart is a safer combination: it tries reload first and falls back to a full restart if the service does not handle reload signals.

Terminal
sudo systemctl reload-or-restart nginx

Enable and Disable at Boot

Starting a service with start does not persist across a reboot. To make a service launch automatically at boot, use enable:

Terminal
sudo systemctl enable nginx
output
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /lib/systemd/system/nginx.service.

The output hints at what is happening behind the scenes. Enabling a service creates a symlink in one of the *.wants directories so that the appropriate boot target pulls the unit in.

To disable a service so that it no longer starts at boot:

Terminal
sudo systemctl disable nginx

enable and disable only change the boot behavior. To start or stop the service in the current session as well, add the --now flag:

Terminal
sudo systemctl enable --now nginx
sudo systemctl disable --now nginx

To check whether a service is currently enabled at boot:

Terminal
systemctl is-enabled nginx

Mask and Unmask

Disabling a service prevents it from starting at boot, but another service, socket, or admin can still start it manually. mask is the stronger option. It links the unit file to /dev/null so that every attempt to start it fails, whether from the command line, from another unit, or from a boot target.

Terminal
sudo systemctl mask apache2

To undo this:

Terminal
sudo systemctl unmask apache2

Masking is useful when two services conflict over the same port (for example, Nginx and Apache both wanting port 80) and you want to make sure the one you are not using cannot come back.

Reload systemd After Unit File Changes

When you add a new unit file or edit an existing one, systemd does not pick up the changes automatically. Tell it to reload its configuration with:

Terminal
sudo systemctl daemon-reload

Run this command after:

  • Creating a new .service file under /etc/systemd/system/
  • Editing a unit file (either directly or via systemctl edit)
  • Installing a package that ships a new unit file

After daemon-reload, restart the affected service if it was already running, so it starts under the new definition.

List Services

systemctl can list every unit it knows about, which is useful when you want to see what is active, what is failing, or what is simply installed. Because the listing commands have several useful variations, they have their own dedicated guide: see listing Linux services with systemctl for filtering by state, showing inactive units, and listing unit files.

The quickest view is:

Terminal
systemctl --failed

It prints every service that has failed since boot, which is usually the first thing you want to know on a box you do not fully trust yet.

View Service Logs

systemd stores service logs in the journal, which you query with journalctl. To follow the log for a single service:

Terminal
sudo journalctl -u nginx -f

The -u flag filters by unit, and -f follows new entries as they are written. For a full walkthrough of the journal, see the journalctl command guide.

Reboot, Power Off, and Suspend

systemctl also controls system power state. These commands do not require a service name:

Terminal
sudo systemctl reboot
sudo systemctl poweroff
sudo systemctl suspend
sudo systemctl hibernate

They are equivalent to the older reboot, poweroff, and halt commands, and they go through systemd so that services shut down cleanly.

Troubleshooting

Unit nginx.service could not be found.
The service is not installed, or the unit file is in a location systemd does not scan. Install the package, or run sudo systemctl daemon-reload if you just added a unit file manually.

Failed to start nginx.service: Unit is masked.
The unit has been masked. Run sudo systemctl unmask nginx and try again.

Service keeps failing after a config change
Run systemctl status SERVICE and journalctl -u SERVICE -n 50 to read the most recent log lines. Configuration syntax errors are the most common cause.

Changes to the unit file are ignored
You forgot sudo systemctl daemon-reload. systemd caches unit files in memory and needs to be told to reread them.

Permission denied
Most state-changing commands require root. Prefix the command with sudo, or switch to the root user.

Quick Reference

For a printable quick reference, see the systemctl cheatsheet .

Task Command
Show service status systemctl status nginx
Start a service sudo systemctl start nginx
Stop a service sudo systemctl stop nginx
Restart a service sudo systemctl restart nginx
Reload configuration sudo systemctl reload nginx
Enable at boot sudo systemctl enable nginx
Enable and start now sudo systemctl enable --now nginx
Disable at boot sudo systemctl disable nginx
Check boot status systemctl is-enabled nginx
Reload unit files sudo systemctl daemon-reload
Show failed units systemctl --failed
Follow service logs sudo journalctl -u nginx -f

FAQ

What is the difference between enable and start?
start launches the service in the current session only. enable creates the symlinks that make systemd start the service at every boot. The two are independent, which is why --now exists to do both in one command.

What is the difference between disable and mask?
disable removes the service from boot targets, but it can still be started manually or pulled in by another unit. mask links the unit file to /dev/null so every start attempt fails.

How do I know if my system uses systemd?
Run ps -p 1 -o comm=. If the output is systemd, your init system is systemd and systemctl applies. Virtually every mainstream distribution has used systemd as the default since around 2015.

Does reload work for every service?
No. It depends on whether the unit file defines an ExecReload command and whether the service itself supports reloading. If you are unsure, use reload-or-restart, which falls back to restart when reload is not available.

What happens to running connections when I restart a service?
They are dropped. A full restart kills the process and starts a new one, so anything holding a connection will see it close. Use reload or reload-or-restart when the service supports it.

Conclusion

Most day-to-day work with systemctl comes down to status, start, stop, restart, enable, and disable. Keep daemon-reload in mind whenever you edit a unit file, and pair systemctl with journalctl when something fails to start.

Bash Best Practices: Writing Safer, Cleaner Scripts

Bash scripts tend to start small and grow over time. A helper you wrote in five minutes to copy a few files can end up running in a cron job, a deployment pipeline, or a server startup script. When that happens, small oversights such as an unquoted variable or an ignored error become real outages.

This guide covers practical Bash best practices that keep scripts predictable, safe to re-run, and easy for other people to read.

Start With a Clear Shebang

Every Bash script should begin with a proper shebang line. The portable choice for Bash is:

~/script.shsh
#!/usr/bin/env bash

Using /usr/bin/env bash lets the script find Bash through the PATH, which matters on systems where Bash is installed outside /bin, such as macOS with Homebrew. If you rely on features that exist only in Bash, do not use #!/bin/sh. On many distributions /bin/sh points to dash or another minimal shell, and arrays, [[ ]], and local will not behave the same way.

Enable Strict Mode

By default, Bash ignores many errors. A command can fail, a variable can be unset, and the script will keep running as if nothing happened. Enabling strict mode at the top of the script changes that behavior.

~/script.shsh
#!/usr/bin/env bash
set -euo pipefail

Here is what each flag does:

  • -e - Exit immediately if any command returns a non-zero status.
  • -u - Treat unset variables as an error and exit.
  • -o pipefail - Make a pipeline fail if any command in it fails, not only the last one.

A common extra is IFS=$'\n\t', which prevents word splitting on spaces and avoids surprises when iterating over filenames that contain spaces.

~/script.shsh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Strict mode is not magic. It will not catch logic errors, and in some scripts you will need to opt out locally with || true for commands that are allowed to fail. The point is to flip the default from “keep going on errors” to “stop on errors”, which is almost always the safer choice. For a closer look at each option on its own, see our bash set command guide.

Always Quote Your Variables

Unquoted variables in Bash are expanded and then word-split. If a variable contains spaces, wildcards, or is empty, the result may be very different from what you expect.

Terminal
file="My Report.txt"
printf '<%s>\n' $file

Because $file is not quoted, Bash passes two arguments to printf: My and Report.txt. Quoting fixes it and keeps the filename as one value:

Terminal
printf '<%s>\n' "$file"

The same rule applies inside tests, loops, and command substitutions. When in doubt, quote. The only common case where you intentionally leave a variable unquoted is when you want word splitting, which should be rare and deliberate.

Prefer [[ ]] Over [ ]

Bash supports two test constructs: the older [ ] (a synonym for the test command) and the newer [[ ]] keyword. For Bash scripts, prefer [[ ]]:

sh
if [[ -f "$config" && "$env" == "production" ]]; then
 echo "Loading production config"
fi

[[ ]] handles empty variables safely even without quotes, supports pattern matching with == and regex with =~, and allows logical operators such as && and || inside the test. Only fall back to [ ] when writing a portable POSIX script that must run under sh or dash.

Use Functions for Repeated Logic

Once a script has more than a couple of steps, group related commands into functions. Functions make scripts easier to read, test, and refactor.

~/backup.shsh
log() {
 printf '[%s] %s\n' "$(date +'%F %T')" "$*"
}

backup_dir() {
 local src="$1"
 local dst="$2"

 log "Backing up $src to $dst"
 rsync -a "$src/" "$dst/"
}

backup_dir "/var/www" "/backup/www"

Declare variables inside functions with local so they do not leak into the rest of the script. Pass input as positional parameters, and return output via echo or printf rather than global variables when possible.

See Bash functions for more details on arguments, return values, and scoping.

Handle Errors Explicitly

Strict mode stops the script on errors, but it does not tell the user what went wrong. Adding a trap on ERR gives you a single place to log failures.

~/script.shsh
#!/usr/bin/env bash
set -Eeuo pipefail

on_error() {
 local exit_code=$?
 local line=$1
 echo "Error on line $line (exit code $exit_code)" >&2
 exit "$exit_code"
}

trap 'on_error $LINENO' ERR

The -E option makes the ERR trap inherit into functions and subshells. Without it, a failure inside a helper function may exit the script without running your error handler.

For cleanup tasks such as removing temporary files, use trap on EXIT:

~/script.shsh
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

The EXIT trap runs whether the script exits normally, hits an error under set -e, or is interrupted with Ctrl+C. It is the safest way to guarantee cleanup.

Validate Input Early

Scripts that take arguments should fail fast when those arguments are missing or invalid. Check them before doing any work:

~/deploy.shsh
if [[ $# -lt 2 ]]; then
 echo "Usage: $0 <environment> <version>" >&2
 exit 1
fi

env="$1"
version="$2"

case "$env" in
 staging|production) ;;
 *)
 echo "Unknown environment: $env" >&2
 exit 1
 ;;
esac

For scripts with several flags, use getopts instead of parsing $@ manually. It handles short options, arguments, and error reporting in a consistent way.

Avoid Parsing the Output of ls

A common beginner mistake is looping over ls output:

sh
for f in $(ls /var/log); do
 echo "$f"
done

This breaks on filenames with spaces, newlines, or special characters. Use a glob or find instead:

sh
for f in /var/log/*; do
 echo "$f"
done

For recursive traversal, use find with -print0 and xargs -0, or a while read loop with IFS= and -d '' to handle unusual filenames correctly.

Keep Scripts Idempotent When Possible

A script you can safely re-run is much easier to work with than one that breaks on the second attempt. Aim for idempotent behavior: check before creating directories, use mkdir -p instead of mkdir, use ln -sf instead of ln -s, and guard commands with if blocks when they would otherwise fail on re-run.

Terminal
mkdir -p /opt/app/config

The same pattern applies to package installation, user creation, and configuration edits. Tools like Ansible enforce idempotency by design; in Bash, you have to add those checks yourself.

Use Shellcheck

Shellcheck is a static analyzer for shell scripts. It catches unquoted variables, unsafe for loops, and many other common mistakes before you hit them in production.

Install it from your package manager:

Terminal
sudo apt install shellcheck

Then run it against your script:

Terminal
shellcheck script.sh

Fix the warnings it raises, or document with an inline comment why a specific check is being ignored. Making shellcheck part of your editor or pre-commit hook removes an entire class of Bash bugs.

Prefer Readable Over Clever

Bash is full of shortcuts and obscure expansions. They can make scripts shorter, but they also make them harder to read six months later. Choose the clearer form, even when it is a few characters longer.

sh
if [[ -z "$name" ]]; then
 name="anonymous"
fi

is almost always easier to maintain than the golfed equivalent:

Terminal
: "${name:=anonymous}"

Aim for scripts that a teammate can read once and understand, not ones that require a reference manual.

Quick Reference

For a printable quick reference, see the Bash cheatsheet .

Before committing a Bash script, confirm the following:

  • #!/usr/bin/env bash shebang is present.
  • set -euo pipefail is enabled.
  • All variable expansions are quoted.
  • Tests use [[ ]] instead of [ ].
  • Functions replace long repeated blocks, with local variables.
  • trap handles errors and cleanup.
  • Input is validated before work begins.
  • No parsing of ls output.
  • shellcheck reports no warnings, or they are explicitly ignored.

FAQ

Is set -e enough on its own?
No. set -e stops on many errors, but it misses failures in pipelines and ignores unset variables. Combine it with -u and -o pipefail to cover those cases.

Does strict mode break existing scripts?
It often surfaces bugs that were already there, such as unset variables and ignored errors. The fix is to quote variables, use default values like ${VAR:-}, and handle expected failures with || true.

Should I write scripts in Bash or switch to Python?
Bash is a good fit for short glue scripts that mostly call other commands. Once a script grows past a few hundred lines, has complex data structures, or needs proper error handling and testing, Python or another language is usually a better choice.

Where can I learn more safe scripting patterns?
Keep the Shellcheck wiki open while you write, and read through the official Bash manual section on shell builtins. Both cover the reasoning behind each rule, not only the rule itself.

Conclusion

Strict mode, quoting, and a few well-placed traps cover most of the risk in Bash scripting. Make these practices your defaults, run shellcheck on every change, and your scripts will behave the same way on your laptop, in CI, and on a production server.

❌
❌