A practical reference covering the core tools for Linux system administration: acquiring and managing software packages on both Debian/Ubuntu and Red Hat family distributions, understanding how a Linux machine interacts with a network environment, automating repetitive tasks with Bash scripts, and scheduling those scripts so they run on their own.
Table of Contents
- Module 1: Managing Software on Debian and Ubuntu Systems
- Understanding the APT Repository Workflow
- Installing Software Manually with dpkg
- Controlling APT Software Sources
- Adding Third-Party and Custom Repositories
- Working with Alternative Software Formats: Snap, Flatpak, and AppImage
- Searching for Software Availability
- Maintaining, Updating, and Removing Packages
- Module 2: Managing Software on Red Hat Enterprise Linux Systems
- Module 3: Working with Linux Networking Tools
- Module 4: Automating Tasks Using Shell Scripts
- Module 5: Scheduling Automated Tasks
- Summary
Module 1: Managing Software on Debian and Ubuntu Systems
The Linux ecosystem is rich in powerful, reliable, and free software. That richness isn’t just about the sheer number of packages available — it’s really about the online software repositories that curate, secure, provide, and manage all of those packages on our behalf. Debian, Ubuntu, and related distributions rely on the APT (Advanced Package Tool) repository system, while Red Hat, Fedora, and their relatives use the RPM/DNF system covered in Module 2.
Understanding the APT Repository Workflow
Before doing anything involving APT, you should make sure that the local copy of the repository index is synchronized with the online sources. Syncing the index is done through apt update, and like almost everything involving APT, it requires sudo privileges.
sudo apt update
The synchronized indices live in /var/lib/apt/lists/, where you’ll find files with long, structured names. A typical file name might look like this:
security.ubuntu.com_ubuntu_dists_noble_universe_binary-amd64_Packages
That name tells you a lot on its own:
| Name segment | Meaning |
|---|---|
security.ubuntu.com | The repository host |
noble | The Ubuntu release codename (24.04) |
universe | The repository component (as opposed to main, restricted, or multiverse) |
binary-amd64 | Built for AMD64-compatible hardware |
Opening one of these index files reveals the metadata for every package it covers — useful for confirming the authenticity of an installed version, troubleshooting problems, or reporting bugs.
If a package from an existing, enabled repository isn’t yet installed, you can install it with apt install and the package name:
sudo apt install tree
tree is a small utility that prints the contents of nested file-system hierarchies. When you run the install command, APT will:
- Read through the package index and resolve any necessary dependencies.
- Show you what will change and how much disk space the operation will consume.
- Download the package from the location recorded in the index files.
- Install the package and make sure everything is wired up so it works immediately.
flowchart TD
A["apt install package"] --> B["Read package index"]
B --> C["Resolve dependencies"]
C --> D["Show changes + required disk space"]
D --> E["Download package from repo"]
E --> F["Install + configure package"]
F --> G["Package ready to use"]
To confirm where the new binary landed, use which:
which tree
# /usr/bin/tree
Because /usr/bin is already part of the user’s PATH, running tree just requires typing its name:
tree /home
Installing Software Manually with dpkg
APT packages are distributed in a standardized format using the .deb file extension. APT is usually the most convenient way to manage installations, but you aren’t restricted to it — the lower-level dpkg tool can handle a .deb file manually, and it’s also useful for related administrative tasks.
Listing installed packages and filtering for a specific one:
dpkg -l | grep tree
Listing every file that came with a package, including where each one was installed:
dpkg -L tree
Checking the current install status of a package:
dpkg -s tree
dpkg becomes especially useful when you need software that doesn’t come from the official repositories, or when you need a custom version of a package that’s newer than what your distribution’s release cadence provides. APT ties version updates to your particular distribution schedule to guarantee that dependencies are met and security concerns are addressed — but that also means packages on an older, long-term-support release can fall behind cutting-edge upstream versions. A common example is needing a newer PHP release than an LTS Ubuntu version supports.
Here’s a worked example: downloading and manually installing the cowsay package directly from its .deb URL on archive.ubuntu.com (the same package is also available through the regular repo via apt install cowsay, but downloading it manually demonstrates how dpkg works):
wget http://archive.ubuntu.com/ubuntu/pool/universe/c/cowsay/cowsay_3.7.0+dfsg2-7_all.deb
ls
sudo dpkg -i cowsay_3.7.0+dfsg2-7_all.deb
The binary is written to /usr/games in this case. Running it confirms the install:
cowsay "Hello from dpkg!"
flowchart LR
subgraph APT["APT (front end)"]
A1["apt update"] --> A2["apt install"] --> A3["apt remove / purge / autoremove"]
end
subgraph DPKG["dpkg (low level)"]
D1["dpkg -i file.deb"] --> D2["dpkg -l / -L / -s"] --> D3["dpkg -r / -P"]
end
APT -->|"drives"| DPKG
ExternalDeb[".deb file downloaded manually"] --> D1
Controlling APT Software Sources
APT’s software sources are controlled by settings inside /etc/apt/. Historically, /etc/apt/sources.list contained a flat list of enabled repositories, edited by adding or removing lines. Modern Ubuntu releases instead use the deb822 format, with settings living in /etc/apt/sources.list.d/, typically in a file such as ubuntu.sources. Instead of single-line entries, repositories are declared as key/value blocks:
Types: deb
URIs: http://archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
| Field | Purpose |
|---|---|
Types | Package type (deb for binaries, deb-src for source packages) |
URIs | The online home of the repository |
Suites | The release (“noble”) plus variants like noble-updates (patched packages) and noble-backports (newer, non-standard versions for special use cases) |
Components | main, universe, restricted, multiverse — the last two contain software some users avoid because of non-free licensing or limited support |
Signed-By | The encryption key used to verify that downloaded packages weren’t intercepted or tampered with in transit |
Unless you have specific organizational restrictions, most default installs enable all four components. The signing-key requirement matters: without a valid key, you can never be fully certain that downloaded software wasn’t manipulated over the network — a risk that has been demonstrated in the wild, such as a past brief compromise of Linux Mint installer downloads.
A second section of the sources file typically covers the dedicated security-update repository (noble-security above), which is where urgent patches arrive.
Adding Third-Party and Custom Repositories
Beyond the official repositories, you can add external repos created by private application developers or in-house teams. Once added, software from those repos is managed by APT alongside official packages — but because Ubuntu’s repository managers can’t vouch for third-party sources, you take on the responsibility of confirming that an unofficial repo is legitimate, secure, and compatible with your system.
A common example: the standard gcc package is available in main, but a newer version may only exist in a PPA (Personal Package Archive). Here’s the workflow for adding one:
sudo apt update
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
This creates a new file in /etc/apt/sources.list.d/ containing the repo address and its metadata, and you’ll be prompted with details before the change is applied.
sudo apt update
sudo apt install gcc-13
Because of the many dependencies involved, an operation like this can consume a couple hundred megabytes of disk space and take a minute or two. Confirm success by checking the version:
gcc-13 --version
To remove a PPA, either delete the file that was created in /etc/apt/sources.list.d/, or re-run the same command with --remove:
sudo add-apt-repository --remove ppa:ubuntu-toolchain-r/test
flowchart TD
A["Need software not in default repos"] --> B{"Trustworthy third-party source?"}
B -->|No| C["Do not add — find alternative"]
B -->|Yes| D["sudo add-apt-repository ppa:owner/name"]
D --> E["sudo apt update"]
E --> F["sudo apt install package"]
F --> G["Use package like any other"]
G --> H["No longer needed?"]
H -->|Yes| I["sudo add-apt-repository --remove ppa:owner/name"]
Working with Alternative Software Formats: Snap, Flatpak, and AppImage
Ubuntu has pushed the use of snap packages for about a decade, and modern Ubuntu installs come with the snapd service active and at least a few packages already installed from snap rather than APT.
The key difference from traditional packages: snaps are self-contained mini-environments bundling their own dependencies, libraries, and binaries. When a snap runs, it doesn’t touch software or configuration resources from the rest of the Linux system — everything runs inside the snap sandbox. This has real security advantages and simplifies package management in general, but it can introduce a performance cost and some behavioral surprises.
List installed snaps:
snap list
A fresh system typically shows just snapd itself and a base runtime such as core22 (providing a runtime environment for applications, even ones targeting an older Ubuntu release than the host).
Install a snap — for example, wormhole, a simple tool for copying files between Linux machines with minimal overhead:
sudo snap install wormhole
Much like APT, snap verifies dependencies and package availability before completing the install. Confirm the binary location:
which wormhole
# /snap/bin/wormhole
Note that the binary is not written to /usr/bin — it lives in the non-standard /snap/bin directory. This once caused problems when that path wasn’t included in users’ PATH, but modern distributions handle it correctly by default.
Version checking works the same way as for APT-based software:
wormhole --version
Every snap application maintains its own subdirectories under the root-level /snap directory (creating the sandboxed environment) as well as a separate snap directory inside each user’s home directory.
| Format | Sandboxing model | Primary use case | Typical ecosystem |
|---|---|---|---|
| Snap | Full sandbox, own dependencies bundled | General-purpose apps and services | Ubuntu / Canonical |
| Flatpak | Full sandbox, own dependencies bundled | Desktop applications | Red Hat / Fedora primarily |
| AppImage | Self-contained; no installation required at all | Portable, single-file desktop apps | Distribution-agnostic |
Searching for Software Availability
Even when you have a rough idea of what a package should be called, you may not know the exact name apt install expects. apt help gives a helpful overview pointing to apt list, apt search, and apt show.
Searching broadly for php returns far too many hits to be useful — including packages that merely depend on PHP or that provide libraries used by it:
apt search php | wc -l
# 2600+
Restricting the search to just the package name (not the description) helps, but still isn’t precise enough:
apt search --names-only php
A regular-expression anchor narrows things down to exactly the package named php, nothing more:
apt search --names-only '^php$'
Once you know the exact name, apt show prints the full package details — the version currently available in the repository, the package size, and a description:
apt show php
apt list (with no arguments) prints brief details for every package on the system, installed or not — useful when you’re fairly sure of a name and just want quick confirmation:
apt list ssh
apt list php
| Command | Purpose | Precision |
|---|---|---|
apt search <term> | Full-text search across names and descriptions | Low — many false positives |
apt search --names-only <term> | Search names only | Medium |
apt search --names-only '^term$' | Regex-anchored exact name match | High |
apt show <name> | Full metadata for a known package name | N/A — requires exact name |
apt list <name> | Quick installed/available status | High, if name is already known |
Maintaining, Updating, and Removing Packages
Good system hygiene means running nothing that isn’t necessary. Periodic audits that identify unused software save storage and memory and reduce the attack surface. Once you and your team agree a package is no longer needed, remove it.
Removing vs. purging. apt remove deletes binaries and other files that came with a package, but normally leaves user configuration files behind (in case you reinstall later):
sudo apt remove gcc-13
If you want a completely fresh install — for example because configuration files became corrupted — use apt purge instead, which also deletes the configuration:
sudo apt purge gcc-13
Cleaning up orphaned dependencies. apt autoremove scans the index and recent transactions to identify packages that are still installed but no longer needed by anything else, reclaiming disk space (in one walkthrough, roughly 188 MB):
sudo apt autoremove
This is worth running periodically even on systems that aren’t especially busy.
Applying upgrades. Even more important than autoremove is applying available upgrades as soon as they exist — it’s an unusual week without a serious bug or vulnerability disclosed in some popular package, and attackers watch for exactly these windows. Syncing the index and then running apt upgrade applies every upstream upgrade available for every installed package:
sudo apt update
sudo apt upgrade
Reinstalling without losing configuration. If a package’s program files (not its configuration) are the problem, apt reinstall replaces the original package files while leaving existing configuration in place:
sudo apt reinstall tree
Doing the same with dpkg. dpkg -r removes a package (config kept); dpkg -P purges it (config removed too):
sudo dpkg -r tree # remove
sudo dpkg -P tree # purge
After removal, the command will fail (though the shell still “remembers” where the binary used to live via its hash table until the next lookup).
Removing and refreshing snaps. Snap-installed software is not managed by apt upgrade — it has its own update path:
sudo snap remove wormhole
sudo snap refresh
A note on apt-get. APT itself is a friendlier front end for the older apt-get command. apt-get is no longer the go-to tool day to day, but it’s still there under the hood, and older documentation demonstrating it on the command line will generally still work.
| Operation | APT command | dpkg equivalent |
|---|---|---|
| Install | apt install <pkg> | dpkg -i <file>.deb |
| Remove (keep config) | apt remove <pkg> | dpkg -r <pkg> |
| Remove (with config) | apt purge <pkg> | dpkg -P <pkg> |
| Reinstall | apt reinstall <pkg> | — |
| Clean up orphans | apt autoremove | — |
| Apply all upgrades | apt upgrade | — |
Module 2: Managing Software on Red Hat Enterprise Linux Systems
The Red Hat Enterprise Linux family — including Fedora, CentOS, AlmaLinux, and Rocky Linux — uses two related but distinct tools for package management: RPM and DNF.
RPM, DNF, and the Legacy of YUM
RPM is the low-level manager that handles installing, updating, and removing software packaged in the .rpm format. RPM works directly with individual packages and their dependencies but doesn’t handle automatic dependency resolution or repository connections on its own.
DNF is the front end to RPM: it resolves dependencies automatically and interfaces with software repositories, using RPM under the hood to perform the actual install/removal work while adding better performance, a cleaner interface, and improved dependency solving.
Most day-to-day work happens through DNF, but long-time admins may remember yum. Until 2015, Red Hat used yum as its front-end package manager; because so many admins and scripts relied on the name, yum was preserved as a symbolic link to the DNF binary:
which yum
# /usr/bin/yum
ls -la /usr/bin/yum
# /usr/bin/yum -> dnf-3
ls -la /usr/bin | grep dnf
# /usr/bin/dnf -> dnf-3
Whether admins realize it or not, everyone running yum today is really running DNF.
flowchart TD
Y["yum (legacy command)"] -->|"symlink"| D3["dnf-3"]
DNF["dnf (command)"] -->|"symlink"| D3
D3 --> RPM["RPM (low-level engine)"]
RPM --> PKG[".rpm package files"]
Installing Software with DNF
Installing software with DNF is straightforward once you know the exact package name. If you don’t, search the repositories with a keyword:
sudo dnf search httpd
Because sudo was used, DNF first refreshes the remote repository index in addition to the local one. The exact match appears at the top of the results (the x86_64 architecture suffix in the package name usually isn’t needed when installing).
sudo dnf install httpd
DNF confirms your Red Hat subscription is valid, then heads to the repo for an installation candidate. Adding -y skips the confirmation prompts for scripted use:
sudo dnf install -y httpd
Along with the main package, DNF lists every dependency that will be installed — for example, the openssl package used by Apache to implement server encryption. Having this list is valuable for troubleshooting if something goes wrong.
Red Hat’s install behavior differs from Debian/Ubuntu in two important ways. The install process creates the web root (/var/www with an html subdirectory), but:
- No default “It works!” welcome page is created for you.
- The service is not started automatically — that’s left up to you.
curl http://localhost
# curl: (7) Failed to connect to localhost port 80: Connection refused
Create a minimal home page:
sudo nano /var/www/html/index.html
<html>
<body>
<h1>Hello from my Red Hat web server!</h1>
</body>
</html>
Trying again may surface an unrelated but interesting error about registering mlkem512 — an indication that post-quantum cryptography support isn’t fully wired up yet on the system. Since the site hasn’t been configured for any encryption, this doesn’t affect basic functionality.
Start (and verify) the service using systemctl:
sudo systemctl start httpd
sudo systemctl status httpd
curl http://localhost
# <html><body><h1>Hello from my Red Hat web server!</h1></body></html>
Because the HTTP port (80) hasn’t been opened in the firewall, the page still would not be reachable from outside the machine at this point.
sequenceDiagram
participant Admin
participant DNF
participant Systemd
participant Firewall
Admin->>DNF: dnf install httpd
DNF-->>Admin: package + dependencies installed
Note over Admin: /var/www/html created, no default page
Admin->>Admin: create index.html manually
Admin->>Systemd: systemctl start httpd
Systemd-->>Admin: service running
Admin->>Firewall: (not yet) open port 80
Note over Firewall: External access still blocked
Working with Repositories: EPEL and Red Hat Extensions
By default, Red Hat comes with just one active repository, covering the basics. Repository configuration lives in /etc/yum.repos.d/ — a directory name still using the legacy “yum” naming for the sake of stability and compatibility with existing scripts. A single redhat.repo file there contains the package profile for each configured repository.
dnf repolist
This can reveal repositories such as Code Ready Builder, which wouldn’t appear on a completely fresh install but may already be enabled if the machine has been used for other purposes previously.
Trying to install a package that isn’t in the base repo — for example htop, a process-monitoring tool — will fail:
sudo dnf install htop
# Error: Unable to find a match: htop
The fix, on Red Hat, is enabling EPEL (Extra Packages for Enterprise Linux). The exact steps vary by release; a typical Red Hat 10 workflow looks like this:
# Enable the matching Code Ready Builder repo (architecture pulled dynamically)
sudo subscription-manager repos --enable "codeready-builder-for-rhel-10-$(arch)-rpms"
# Install the community EPEL release package
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
Confirm the new repo is active:
dnf repolist | grep -i epel
New files also appear in /etc/yum.repos.d/, containing URL endpoints and the encryption key information that keeps transfers secure.
sudo dnf install htop
htop
The EPEL release just installed is the Fedora-affiliated community version. There is also an official, Red-Hat-signed counterpart: the Red Hat Extensions repository, which requires an active subscription:
sudo subscription-manager repos --enable "rhel-10-for-x86_64-extensions-rpms"
dnf repolist
A free developer-level subscription is available for up to 16 machines per account, sufficient for lab and learning environments.
Important: never enable both the community EPEL repo and the official Extensions repo at the same time — many packages exist in both under identical names, which can cause conflicts. Remove the community version once the official one is enabled:
sudo dnf remove epel-release
dnf repolist
On other RHEL-family distributions such as AlmaLinux, enabling EPEL can be even simpler:
sudo dnf install epel-release
sudo crb enable
dnf repolist
crb is a small helper command that enables the Code Ready Builder–equivalent repo in one step, rather than the separate subscription-manager invocation used on Red Hat itself.
flowchart TD
Base["Default single base repo"] --> Need{"Package not found?"}
Need -->|"e.g. htop"| Choice{"Which EPEL source?"}
Choice -->|"Community"| CRB["Enable Code Ready Builder (subscription-manager)"] --> EPELpkg["dnf install epel-release-latest-*.rpm"]
Choice -->|"Official (subscription required)"| EXT["subscription-manager repos --enable ...-extensions-rpms"]
EPELpkg --> Warn["Never enable both EPEL community + Extensions together"]
EXT --> Warn
Warn --> Clean["dnf remove epel-release if switching to Extensions"]
| Repository | Source | Requires subscription? | Package overlap risk |
|---|---|---|---|
| Base Red Hat repo | Red Hat | Yes (built in) | — |
| Code Ready Builder | Red Hat | Yes | Enables building/dependency packages |
| EPEL (community) | Fedora project | No | Conflicts with Extensions if both enabled |
| Red Hat Extensions | Red Hat | Yes (active subscription) | Conflicts with EPEL if both enabled |
Querying Installed Packages with RPM
While DNF handles most day-to-day operations, RPM is useful for two more hands-on purposes: querying installed packages for their profile, and verifying that nothing is broken or suspicious about their current configuration.
Find which package a binary belongs to:
which nano
# /usr/bin/nano
rpm -qf /usr/bin/nano
This confirms the package name, its target architecture (x86_64), the distribution it was built for (Enterprise Linux 10), and its version.
Get full package details (“query info”):
rpm -qi nano-8.1.3.el10.x86_64
This nicely formats the version, release, and architecture information decoded from the file name, the open-source license in use (for example GPLv3), and the encryption signature produced when the package was downloaded — which can be compared to the official published signature to confirm you received exactly what you requested.
Verify that a binary isn’t missing any dependencies:
rpm -Vf /usr/bin/nano
No output means no problems — “no news is good news.”
Querying Repositories with DNF Repoquery
Where rpm -q reports on local resources, dnf repoquery reports on remote repositories, with far more control over the results.
Query a specific package name:
dnf repoquery perl
This returns the date of the most recent metadata check and, potentially, several matching packages for your specific release and architecture.
Use wildcards to find related packages:
dnf repoquery '*perl*' | grep -i cgi
Find every package that depends on a given package (useful before considering a removal):
dnf repoquery --whatrequires openssl
Because openssl sits at the heart of most security/encryption operations on the system, this list is typically long — a strong signal that removing it would be a bad idea.
Get detailed information for every match:
dnf repoquery -i perl
List every file that comes with a package:
dnf repoquery -l perl
A “contains no files” result often indicates the package is a meta-package that only depends on the contents of other packages.
Reverse-search: find which package provides a specific file:
dnf repoquery -f /usr/bin/bash
This is useful when a binary is misbehaving and you want to reinstall the package that provides it.
List every available query tag for even finer-grained control:
dnf repoquery --querytags
Tags include things like download size, package group, and recommends — used to suggest optional dependencies that enhance functionality without being strictly required:
dnf repoquery --qf '%{recommends}' httpd
| Flag | Purpose |
|---|---|
dnf repoquery <name> | Basic lookup for a package name |
dnf repoquery '*name*' | Wildcard search |
dnf repoquery --whatrequires <name> | List dependents (reverse dependency search) |
dnf repoquery -i <name> | Detailed information |
dnf repoquery -l <name> | List files provided by the package |
dnf repoquery -f <path> | Find which package provides a file |
dnf repoquery --querytags | List all available query tags |
Updating and Removing Packages with DNF
Check for available updates to a specific package:
sudo dnf update httpd
DNF surveys the installed software and its dependencies, then checks the remote repositories. If nothing is pending, you’ll simply be told there’s nothing to do.
Note that updating with DNF does not guarantee the absolute latest upstream version — you get the latest version available in the configured repository, which may lag behind bleeding-edge releases. This is by design: repository maintainers prioritize making sure updates don’t break your system, since new and lightly tested features can produce unexpected results. If you need a newer version regardless, you can install it manually, accepting a greater risk of something breaking.
Check for (and apply) all pending updates:
sudo dnf update
You’ll see a complete list of upgradable packages, their new versions, and sizes. When updating, you may be prompted to approve or reject the encryption key profile used for a particular repository’s downloads — worth taking seriously for production systems, even if it’s fine to accept quickly in a lab environment.
Remove a package and its now-unused dependencies:
sudo dnf remove httpd
Any customizations made to the removed package’s configuration are lost, but DNF ensures dependencies aren’t shared with other still-installed packages before deleting them.
Module 3: Working with Linux Networking Tools
Modern Linux systems, run with typical defaults, won’t function properly without full connectivity — not just to other machines on a local network, but to the internet at large. That connectivity cuts both ways: while it lets you reach vast resources instantly, it also means remote users can, in theory, reach yours. Configuring network connectivity so that only approved people and processes have access has never been more important.
IP Addressing Fundamentals
Whenever multiple devices are connected together, you need a reliable way to tell them apart. The Internet Protocol (IP) determines how each device is assigned either a 32-bit or 128-bit address.
- IPv4 addresses are 32 bits, written as 4 octets (for example
192.168.1.10), ranging in theory from1.1.1.1to255.255.255.255— over 4 billion possible addresses, though many are reserved for special purposes. - Typically, the three leftmost octets identify a network shared by multiple hosts, leaving up to 254 usable host addresses per network segment (for example,
192.168.1.0/24and192.168.2.0/24are two distinct networks).
Because the number of internet-connected devices vastly exceeds 4 billion, two innovations addressed IPv4 exhaustion:
- NAT (Network Address Translation) reserves specific IPv4 ranges exclusively for private networks that don’t connect to the internet directly. Devices on a private NAT network share a single public IP address; translation software converts a private address to the public one (and back) as traffic crosses the boundary. This lets countless private devices share a small number of public addresses.
- IPv6 addressing uses eight groups of four hexadecimal digits separated by colons, providing a practically unlimited address space — no need to reserve blocks.
flowchart TD
subgraph Private["Private network (NAT)"]
H1["Host 192.168.1.10"]
H2["Host 192.168.1.11"]
H3["Host 192.168.1.12"]
end
Private --> NAT["NAT gateway/router"]
NAT -->|"single public IP"| Internet["Public Internet"]
subgraph IPv6["IPv6 world"]
V1["2001:db8::1"]
V2["2001:db8::2"]
end
IPv6 -->|"globally unique, no NAT required"| Internet
View your machine’s own address with the ip command:
ip address
# equivalently:
ip addr
ip a
A machine typically has one primary interface (historically named eth0, though modern systemd-based naming conventions often produce predictable names like enp8s0) carrying both an IPv4 and an IPv6 address.
Domain Names, Ports, and Protocols
Beyond numeric/hexadecimal IP addresses, sites are identified by human-readable domain names, resolved through DNS, the Domain Name System, which maps names to IP addresses.
host example.com
Most devices receive their IP address automatically from a DHCP server active on the network. Manual static addressing is possible but requires care to avoid address conflicts and to stay within the correct network block — DHCP is almost always the safer default.
Transmission protocols define how different classes of data move across a network:
| Protocol | Characteristics | Typical use |
|---|---|---|
| ICMP | Lightweight, connectionless | Quick connectivity diagnostics (ping) |
| TCP | Reliable, connection-oriented | Web browsing, most application traffic |
| UDP | Fast, connectionless, no delivery guarantee | Video streaming, DNS requests |
Ports provide further addressing granularity: a port is a number between 0 and 65535. Appending a port to an address routes the request to a specific service instead of, say, the default site.
http://localhost:631
As long as the CUPS printer service is installed, this reaches the CUPS admin page; http://localhost alone would fail if nothing is listening on the default port for that request.
| Port | Service |
|---|---|
| 22 | SSH |
| 53 | DNS |
| 80 | HTTP (unencrypted) |
| 443 | HTTPS (encrypted) |
| 631 | CUPS printing service |
There are roughly a thousand such “well-known” ports that should be avoided for private/custom services.
Firewalls combine addresses, protocols, and ports to actively filter which traffic is permitted. Red Hat and Fedora typically use firewalld; Ubuntu users often use UFW (Uncomplicated Firewall). Under the hood, both rely on the Linux kernel’s netfilter framework.
flowchart LR
App["Application traffic"] --> Rules{"Firewall rule engine (netfilter)"}
Rules -->|"firewalld (Red Hat/Fedora)"| RH["firewall-cmd"]
Rules -->|"ufw (Ubuntu)"| UB["ufw allow/deny"]
RH --> Kernel["Linux kernel packet filtering"]
UB --> Kernel
Assessing Network Connectivity Step by Step
When connectivity fails, the network stack has many moving parts, and figuring out which one is at fault requires a methodical approach: start locally and move outward, ruling out your own machine before examining routers, cabling, and providers.
1. Confirm the local interface is healthy.
ip address
Look for a valid IP address and a device state of UP. If there’s no connectivity, you might not see a valid address at all, or the interface entry might be missing entirely. Interface naming has evolved: older systems numbered them eth0, eth1, eth2; the modern systemd-based convention (e.g., enp8s0) ties names permanently to specific hardware, which is more predictable for scripting and automation.
2. Confirm the route to the gateway/router.
ip route show
The default route shows the address traffic takes to reach the outside world. If the local machine is 10.0.3.126 and the default route points to 10.0.3.1, the .1 host on the same network is acting as the router — that’s where internet connectivity originates. A missing or unhealthy default route points to a problem with the router itself: a dropped cable, a lost Wi-Fi signal, or simply an unplugged power cord.
3. Ping your own machine (loopback).
ping 127.0.0.1
ping sends a small connection request and reports whether a reply arrived — the quickest way to establish an active connection. Press Ctrl+C to stop and see a summary of packet loss and latency. No errors and no packet loss confirms the basic networking stack itself is working.
4. Ping the router.
ping 10.0.3.1
5. Ping an external address.
ping 8.8.8.8
8.8.8.8 (Google’s public DNS server) is a common target simply because it’s easy to remember and type. Reliable access to the router but failure here points to a problem somewhere between the router and the internet.
6. Test DNS specifically.
ping google.com
If pinging an IP address works but pinging a domain name doesn’t, nothing is translating machine addresses to human-readable ones — a DNS problem (covered in more depth later in this module).
7. Trace the path hop by hop.
sudo apt install inetutils-traceroute
traceroute 8.8.8.8
traceroute reports each hop a packet takes along its route. inetutils-traceroute uses ICMP under the hood, which tends to be more reliable in environments where firewalls block other traceroute implementations. Each hop can be identified in turn — the first hop to the local network’s router, then the ISP’s own equipment, and so on until the destination responds. A failure at a specific hop narrows the problem down to that segment of the path (for example, a failure at the ISP stage points squarely at their infrastructure).
flowchart TD
Start(["Connectivity problem reported"]) --> S1{"ip address: interface UP with valid IP?"}
S1 -->|No| Fix1["Fix local interface / driver / cabling"]
S1 -->|Yes| S2{"ip route show: valid default route?"}
S2 -->|No| Fix2["Investigate router / gateway"]
S2 -->|Yes| S3{"ping 127.0.0.1 succeeds?"}
S3 -->|No| Fix3["Local network stack is broken"]
S3 -->|Yes| S4{"ping router succeeds?"}
S4 -->|No| Fix4["Cable / Wi-Fi / router power issue"]
S4 -->|Yes| S5{"ping 8.8.8.8 succeeds?"}
S5 -->|No| Fix5["Problem between router and ISP/internet"]
S5 -->|Yes| S6{"ping google.com succeeds?"}
S6 -->|No| Fix6["DNS resolution problem"]
S6 -->|Yes| S7["traceroute to pinpoint exact failing hop, if any issue remains"]
Understanding Your Network Environment
ss shows which network ports are active on a machine, along with the protocol and the service each one targets:
ss -tulnp
A result such as 0.0.0.0:53, 0.0.0.0:22, or 0.0.0.0:80 shows a service listening for requests from any address (DNS, SSH, and HTTP respectively). This kind of output helps spot unexpected or unauthorized network activity.
Testing DNS and the application layer together with curl:
curl -I http://example.com
# HTTP/1.1 301 Moved Permanently
A predictable HTTP response (even a redirect) confirms DNS is working and that the application layer is reachable.
Linux networking can be managed by more than one underlying service, which affects how you troubleshoot:
- NetworkManager — designed for interactive systems like laptops and desktops; exposes the
nmclicommand-line tool. - systemd-networkd — commonly used on servers and containers instead of NetworkManager.
nmcli device status
This lists every network interface on the system — physical Ethernet ports, the loopback device (127.0.0.1), and virtual interfaces created by container/VM tooling such as Docker or virsh/LXD networks.
Nmap is a powerful network discovery and auditing tool:
sudo apt install nmap
Discover every device registered on a local subnet:
nmap -sn 10.0.3.0/24
The /24 netmask defines the first three octets as the network; the 0 in the last octet represents the full range of 255 possible host addresses. This is an excellent way to understand what’s actually running on a network — including devices you might not expect, like printers, cameras, or smartphones.
Audit which services are listening on a specific device (scanning just the first 1000 ports, for speed):
nmap --top-ports 1000 10.0.3.227
A result showing most ports closed but a handful open (for example 22, 80, 139, 445, and 631) reveals SSH, HTTP, Samba file/printer sharing (139 for the older protocol, 445 for a more secure implementation), and CUPS printing — a quick way to assess a device for unauthorized or vulnerable services.
flowchart LR
subgraph Discover["Discovery"]
A["nmap -sn 10.0.3.0/24"] --> B["List of live hosts on subnet"]
end
subgraph Audit["Service auditing"]
C["nmap --top-ports 1000 <host>"] --> D["Open ports + likely services"]
end
B --> C
D --> E["Assess for unauthorized/vulnerable services"]
Troubleshooting DNS Issues
There’s a well-worn joke in system administration: whatever the malfunction, “check your DNS.” DNS may not cause quite as many problems as the joke implies, but it’s a frequent contributor. A classic scenario: a multi-tiered web application suddenly stops completing transactions; the backend database is healthy, but nothing connects — until the frontend is reconfigured to use the database’s IP address directly instead of its domain name, and everything works again. It was a DNS issue all along.
Basic tools for identifying DNS problems from the command line:
host example.com
Returns the IP address(es) associated with the domain — giving you two separate ways to reach a service (name and address).
ping example.com
ping 93.184.216.34
Careful interpretation matters. If pinging both the domain name and the raw IP address fail, that’s a false negative for DNS — it suggests something else is blocking the traffic entirely (for example, ICMP requests blocked by a firewall for security reasons), not that name resolution failed.
dig reliably tests DNS even when ICMP is blocked:
dig 93.184.216.34
# ;; ANSWER: 0
Running dig with no flags looks for an A record matching the argument; a raw IP address doesn’t serve that function, so zero answers is expected there. Query the domain name instead:
dig example.com
This returns an actual answer section, listing both the domain name and its associated IP address — confirming DNS is working.
nslookup provides similar information with a bit more typing:
nslookup example.com
A “non-authoritative answer” means the reply came from a resolver relying on cached data rather than being the definitive source for that zone — the answer is still correct, just indirect.
Although no longer used for manual, everyday configuration, basic DNS settings on a modern Linux system are referenced in:
cat /etc/resolv.conf
# This file is managed by systemd-resolved(8).
nameserver 127.0.0.53
options edns0 trust-ad
| Setting | Meaning |
|---|---|
nameserver 127.0.0.53 | Requests are handled by the local systemd-resolved DNS resolver |
edns0 | Enables modern DNS extensions requiring more resources than legacy defaults assumed |
trust-ad | If the upstream resolver sets the “authenticated data” bit, trust and pass it through to applications |
Check the live resolver state:
resolvectl status
This lists each network link (its active protocols) and, most usefully for troubleshooting, the actual DNS server references in use. A machine on a private LXD/container network, for example, might show its gateway (10.0.3.1) as the DNS server — meaning all real resolution work is delegated to whatever DNS service that gateway machine itself consults. Running resolvectl status on the gateway machine in turn reveals the “real” upstream DNS servers (often Google’s 8.8.8.8, or another public resolver such as NextDNS).
sequenceDiagram
participant Client as Container (10.0.3.126)
participant GW as Gateway (10.0.3.1)
participant Upstream as Upstream DNS (e.g. 8.8.8.8)
Client->>Client: resolvectl status shows DNS = 10.0.3.1
Client->>GW: DNS query forwarded
GW->>GW: resolvectl status shows real DNS servers
GW->>Upstream: forwards query
Upstream-->>GW: DNS answer
GW-->>Client: DNS answer relayed
Additional resolvectl capabilities (see man resolvectl for the full list):
resolvectl statistics
sudo resolvectl flush-caches
resolvectl show-cache
flush-caches is useful when you suspect stale, historical results are being returned instead of current data. show-cache reveals exactly what’s currently cached — for example, an MX (mail exchange) record and an A record for a given domain.
Finally, don’t forget journalctl for DNS-related errors, filtered to the relevant systemd unit:
journalctl -u systemd-resolved
A healthy system typically shows nothing of concern here.
| Tool | Best for |
|---|---|
host | Quick name → IP lookup |
ping | Basic reachability (can give false negatives if ICMP is firewalled) |
dig | Authoritative-style DNS record lookups, works even when ICMP is blocked |
nslookup | Similar to dig, slightly more verbose, clearly labels non-authoritative answers |
resolvectl status | Shows which DNS servers are actually being consulted per interface |
resolvectl flush-caches / show-cache | Diagnose and clear stale cached DNS results |
journalctl -u systemd-resolved | DNS-related error logs |
Working with IPv6 Tools
IPv6 addresses look quite different from IPv4 to monitoring tools, so many commands accept a -6 flag to request IPv6-specific output:
ip -6 address # only IPv6 addresses on interfaces
ip -6 route # only the IPv6 routing table
ss -6 # IPv6 socket/port information
dig -6 example.com # force IPv6 transport for the query
traceroute6 example.com
traceroute6 may need to be installed as a separate package from the regular traceroute/inetutils-traceroute tool.
Module 4: Automating Tasks Using Shell Scripts
Becoming efficient at the command line can be the difference between operations that succeed and those that fail — especially once you start assembling multiple commands into a script meant to run without direct involvement.
Creating and Invoking Shell Variables
Consider a recurring task: generating customized files and directories for each new client contract your organization takes on. Rather than typing every detail repeatedly, start with shell variables — a value assigned to a name that gets expanded anywhere the name is referenced.
project_name=client-123-site
client=123-inc
country=Canada
base_dir="$HOME"
$HOME expands to the home directory of whichever user is currently active — this both saves typing and makes the command portable across users.
Print variable values with echo. Inside double quotes, $variable is expanded to its value:
echo "Project name: $project_name"
echo "Client: $client"
echo "Country: $country"
echo "Base directory: $base_dir"
Inside single quotes, the entire string — including any $variable reference — is treated as a literal:
echo 'Project name: $project_name'
# Project name: $project_name (not expanded)
| Quoting style | $variable behavior |
|---|---|
"double quotes" | Expanded to the variable’s value |
'single quotes' | Treated literally, never expanded |
| No quotes | Expanded, but subject to word-splitting/globbing — riskier for values containing spaces |
Combine variables to build a full path:
project_directory="$base_dir/$project_name-$country"
echo "$project_directory"
Create the directory (and any missing parent directories) with mkdir -p:
mkdir -p "$project_directory"
tree "$base_dir"
Create a standard set of subdirectories in one command using brace expansion:
cd "$project_directory"
mkdir -p {css,js,images,docs}
Create placeholder files, including a hidden .gitignore (its name starts with a dot, so it won’t display in a basic listing):
touch {index.html,README.md,.gitignore}
Populate a file with expanded variable content using a heredoc:
cat > README.md << EOL
# $project_name
Client: $client
Country: $country
EOL
tree afterward confirms the full hierarchy, and cat README.md shows the expanded variable values baked into the file.
flowchart TD
A["Set shell variables: project_name, client, country, base_dir"] --> B["Build project_directory from combined variables"]
B --> C["mkdir -p project_directory"]
C --> D["mkdir -p {css,js,images,docs}"]
D --> E["touch {index.html,README.md,.gitignore}"]
E --> F["cat heredoc > README.md (variables expanded)"]
F --> G["tree confirms full structure"]
Exporting Shell Variables and Variable Scope
By default, a shell variable only exists within the shell where it was created. Exiting that shell — or spawning a new one — makes the variable inaccessible:
exit
echo "$project_name"
# (nothing printed — variable is gone)
Even launching a fresh interactive shell of the same interpreter behaves the same way, with no visible indication anything changed:
bash
echo "$project_name"
# (still empty)
Compare a plain assignment to an exported one:
project0=client0
export project1=client1
Both are accessible in the current shell. But start a new child shell:
bash
echo "$project0"
# (empty — not exported)
echo "$project1"
# client1 (exported variables propagate to child shells)
export makes a variable available not just in the current shell, but in any child shell spawned afterward.
flowchart TD
subgraph Parent["Parent shell"]
P0["project0=client0 (plain)"]
P1["export project1=client1 (exported)"]
end
Parent -->|"spawn child shell (bash)"| Child["Child shell"]
Child --> C0["echo $project0 → empty"]
Child --> C1["echo $project1 → client1"]
To make a variable available across shells and sessions persistently, add it to a startup file instead of setting it interactively:
| File | Scope |
|---|---|
~/.bashrc | Every non-login shell for that user (e.g., a terminal opened from a desktop GUI) |
~/.profile | Login sessions for that user |
/etc/bash.bashrc | All users, for non-login sessions or SSH sessions (may not apply to scripts or system services — test before relying on it) |
For example, .bashrc already defines useful built-in variables such as HISTSIZE:
less ~/.bashrc
echo "$HISTSIZE"
Custom variables can be appended the same way — simply add an assignment line (optionally with export) to the relevant file.
Writing a Simple Bash Script
A Bash script is just a sequence of familiar commands strung together. This example, simple_script.sh, performs three things: reports whether it’s running as root or as a regular user, prints the names of the first five files in the current directory, and reports root-partition disk usage.
#!/bin/bash
if [[ "$EUID" -eq 0 ]]; then
echo "You are running this script as root."
else
echo "You are running this script as a regular user."
fi
echo "Showing the first 5 files in the current directory:"
COUNT=0
for FILE in *; do
echo " - $FILE"
((COUNT++))
if [[ "$COUNT" -ge 5 ]]; then
break
fi
done
DISK_USAGE="$(df -h / | awk 'NR==2 {print $5}')"
echo
echo "Disk usage for / : $DISK_USAGE"
Key elements:
- The shebang line (
#!/bin/bash) identifies the interpreter that should execute the script. It’s not strictly required, and for most simple scripts there’s no practical difference between shell interpreters, but it’s good practice. - The if/else block checks whether the effective user ID (
$EUID) equals0— the only user with that ID isroot. If the script runs viasudo, this condition is met; otherwise theelsebranch runs.ficloses the block. - The for loop uses
*(a glob) to iterate over every item in the current directory.COUNTstarts at0and increments (((COUNT++))) each pass; once it reaches5,breakexits the loop early. - The disk usage section runs
df -h /(human-readable output, root partition only), pipes it toawk, and extracts column 5 (the usage percentage) from the second row (skipping the header row).
df -h /
# Filesystem Size Used Avail Use% Mounted on
# /dev/sda1 50G 12G 36G 25% /
Make the script executable and run it:
chmod +x simple_script.sh
./simple_script.sh
sudo ./simple_script.sh
Running it without sudo correctly reports “regular user”; running it with sudo correctly reports “root” — along with the same file listing and disk usage output either way.
flowchart TD
Start(["Script starts"]) --> Check{"EUID == 0?"}
Check -->|Yes| Root["Print: running as root"]
Check -->|No| User["Print: running as regular user"]
Root --> Loop
User --> Loop["For each file in *, print name, COUNT++"]
Loop --> Break{"COUNT >= 5?"}
Break -->|No| Loop
Break -->|Yes| Disk["df -h / | awk column 5"]
Disk --> End(["Print disk usage, script ends"])
Writing an Interactive Bash Script
Building on the shell-variable exercise, create_env.sh makes the whole workflow interactive and repeatable — prompting for a project name, client, and country, then generating the same environment infrastructure automatically. For an organization repeating this process weekly, this saves significant manual effort and avoids typos.
#!/bin/bash
read -p "Enter the project name: " project_name
read -p "Enter the client name: " client
read -p "Enter the client country: " country
base_dir="$HOME/Projects/web"
echo "Project name: $project_name"
echo "Client: $client"
echo "Country: $country"
echo "Base directory: $base_dir"
project_directory="$base_dir/$project_name-$country"
mkdir -p "$project_directory"
mkdir -p "$project_directory"/{css,js,images,docs}
touch "$project_directory"/{index.html,README.md,.gitignore}
cat > "$project_directory"/README.md << EOL
# $project_name
Client: $client
Created in: $country
Technology stack: HTML, CSS, Vanilla JavaScript
EOL
echo "Your environment has been created. Have a great day!"
Notable syntax details:
read -p "prompt text: " variable_namedisplays a prompt and stores whatever the user types in the named variable.- The nested-directory
mkdir -p "$project_directory"/{css,js,images,docs}requires the leading/so Bash knows the new directories belong under the project root, and the curly braces with commas tell Bash this represents several separate directory names rather than one literal name containing braces. - The
touchline follows the same brace-expansion pattern to create three placeholder files in one command. - The heredoc (
<< EOL ... EOL) writingREADME.mdworks identically whether run interactively from the command line or from inside a script.
Run it:
chmod +x create_env.sh
./create_env.sh
Enter the project name: proj1
Enter the client name: Acme
Enter the client country: US
Project name: proj1
Client: Acme
Country: US
Base directory: /home/user/Projects/web
Your environment has been created. Have a great day!
Confirm the result:
tree "$HOME/Projects/web"
cat "$HOME/Projects/web/proj1-US/README.md"
flowchart TD
A(["Run create_env.sh"]) --> B["read -p prompts: project_name, client, country"]
B --> C["Build base_dir + project_directory"]
C --> D["Print all values back for confirmation"]
D --> E["mkdir -p project root + {css,js,images,docs}"]
E --> F["touch {index.html,README.md,.gitignore}"]
F --> G["Heredoc writes README.md with expanded variables"]
G --> H(["Print success message"])
Module 5: Scheduling Automated Tasks
Even before logging into a brand-new Linux machine for the first time, dozens of processes are already scheduled to fire off autonomously through one automation system or another. Understanding each of these systems helps you monitor system state — and lets you automate your own recurring processes reliably and efficiently.
Automating Tasks with Cron and Anacron
Cron is the classic Unix/Linux tool for scheduling recurring tasks. Although systemd timers (covered next) were designed to eventually replace it, cron remains the preferred tool for many administrators and shows no signs of disappearing.
Edit your personal crontab:
crontab -e
The first time you run this, you’ll be asked to choose a text editor. To set a persistent default editor, add an entry to ~/.bashrc:
export EDITOR=nano
Apply it immediately without logging out:
source ~/.bashrc
A crontab entry has five time/date fields followed by the command to run:
30 8 * * 1-5 /home/user/myscript.sh
| Field | Value in example | Meaning |
|---|---|---|
| Minute | 30 | 30th minute of the hour |
| Hour | 8 | 8 AM (24-hour clock; 16 would mean 4 PM) |
| Day of month | * | Every day of the month |
| Month | * | Every month |
| Day of week | 1-5 | Monday through Friday |
| Command | /home/user/myscript.sh | Full path to the script to run |
Save and exit, and the crontab confirms the job was created. If something goes wrong with scheduled execution, check the system logs — it’s good practice to verify a new job’s log entries a few days after creating it:
grep -i cron /var/log/syslog
ls /var/log | grep -i cron
The cron system extends well beyond the per-user crontab -e file. Related configuration lives in /etc:
ls /etc | grep -i cron
/etc/anacrontab solves a specific problem: what happens if the machine isn’t running at a job’s scheduled time? Rather than relying on wall-clock time, anacron tracks elapsed time since boot:
# period-in-days delay-in-minutes job-identifier command
1 5 cron.daily run-parts /etc/cron.daily
7 25 cron.weekly run-parts /etc/cron.weekly
@monthly 45 cron.monthly run-parts /etc/cron.monthly
| Field | Meaning |
|---|---|
| Period (days) | How many days between executions (1 = daily, 7 = weekly, @monthly = once a month) |
| Delay (minutes) | Minutes to wait after a fresh boot before running, if a scheduled run was missed |
| Job identifier | A name for the job (used for anacron’s own bookkeeping) |
| Command | What actually runs |
Because anacron works off “minutes since boot” rather than clock time, it guarantees a missed job still eventually runs — invaluable for laptops, workstations, or any machine that isn’t always powered on. It’s often not installed by default on lean, cloud-oriented containers, since such systems are designed to run continuously and rarely reboot.
The jobs anacron actually triggers live in /etc/cron.daily, /etc/cron.weekly, and /etc/cron.monthly — directories of scripts run at the implied cadence. For example, a dpkg maintenance script in cron.daily might first check that systemd isn’t already managing the equivalent task (to avoid overlap/conflicts) before running a database backup routine.
Finally, /etc/crontab (distinct from the per-user file created by crontab -e) defines system-wide jobs that execute as root, controlling the scheduling for those cron.daily/cron.weekly/cron.monthly directories among other things.
flowchart TD
User["crontab -e (per-user jobs)"] --> Cron["cron daemon"]
SystemCrontab["/etc/crontab (root jobs)"] --> Cron
Cron --> Daily["/etc/cron.daily/*"]
Cron --> Weekly["/etc/cron.weekly/*"]
Cron --> Monthly["/etc/cron.monthly/*"]
Anacrontab["/etc/anacrontab"] -->|"minutes-since-boot logic"| Daily
Anacrontab --> Weekly
Anacrontab --> Monthly
Logs["/var/log/syslog"] -.->|"troubleshooting"| Cron
Automating Tasks with Systemd Timers
Systemd timers provide an interface for reliably scheduling process executions, much like cron jobs, but fully integrated into the systemd infrastructure — making it easier to tie scheduling directly to the architecture of the processes themselves.
Example: automating a small script, backup.sh, located in /usr/local/bin (its contents aren’t important here — it can be as simple as an echo message for demonstration purposes).
A service unit defines what to run. Create /etc/systemd/system/backup.service:
[Unit]
Description=Backup script
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=root
| Setting | Purpose |
|---|---|
Description | Human-readable label |
After=network.target | Ensures the network is up before this unit starts (add other required units/resources here too) |
Type=oneshot | The service runs a single command and exits, rather than staying resident |
ExecStart | Full path to the script or binary to execute |
User | The account the script runs as (use a regular user instead of root if elevated privileges aren’t required) |
Output generated by the script is captured automatically by journald.
A timer unit defines the schedule. Create /etc/systemd/system/backup.timer:
[Unit]
Description=Run backup.service daily
[Timer]
OnCalendar=daily
Persistent=true
OnBootSec=5min
[Install]
WantedBy=timers.target
| Setting | Purpose |
|---|---|
Unit (implicit, matches file name) | Refers to backup.service |
OnCalendar=daily | Runs at midnight every day (more specific calendar expressions are supported) |
Persistent=true | Runs any missed executions (e.g., after the system was shut down at the scheduled time) — comparable to anacron’s role for cron |
OnBootSec=5min | Runs 5 minutes after boot if a scheduled run was missed |
WantedBy=timers.target | Registers the timer to activate as part of the standard timers target |
Activate the new unit files:
sudo systemctl daemon-reload
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
sudo systemctl status backup.timer
List every active timer on the system:
systemctl list-timers
This typically shows a mix of logging-related timers (such as logrotate.timer) and an anacron-equivalent systemd timer that powers the Persistent=true behavior, alongside the new backup.timer entry.
Manually trigger the underlying service (for testing) and inspect its logs:
sudo systemctl start backup.service
journalctl -u backup.service
The full systemd integration means journalctl output can be filtered precisely to just this one service — a real advantage over less structured logging approaches.
Disable the timer once it’s no longer needed:
sudo systemctl disable backup.timer
flowchart LR
Timer["backup.timer (schedule: OnCalendar, Persistent, OnBootSec)"] -->|"triggers"| Service["backup.service (Type=oneshot, ExecStart)"]
Service --> Script["/usr/local/bin/backup.sh"]
Script --> Journal["journald / journalctl -u backup.service"]
Timer -.->|"registered via"| Target["timers.target"]
| Aspect | Cron | Systemd timer |
|---|---|---|
| Configuration | Single-line crontab syntax | Two unit files (.service + .timer) |
| Missed-run recovery | Requires anacron | Built in via Persistent=true |
| Logging | Scattered across syslog/cron logs | Centralized, filterable via journalctl -u |
| Dependency awareness | None built in | Native (After=, other systemd dependencies) |
| Legacy familiarity | Very high | Lower, but growing |
Running One-Off and Idle-Time Jobs with At and Batch
Two much older Unix tools remain useful for cases cron and systemd timers don’t fit well: at (run a job exactly once, at a specific future time) and batch (run a job at the first moment system load allows, without a fixed schedule).
Using at:
sudo apt install at
at 4:30am tomorrow
This drops you into the at interface, which expects the file-system location of the script or binary to run:
at> /usr/local/bin/backup.sh
Press Ctrl+D to exit the interface. You’ll get confirmation along with a job number in the queue.
View the current queue:
atq
Remove a pending job by its number:
atrm 1
Using batch:
batch
Unlike at, batch doesn’t ask for a time — it runs as soon as system resources are available:
at> /usr/local/bin/backup.sh
Ctrl+D exits the interface the same way. Because at and batch share the same underlying job-queue system, a batch job gets the next sequential job number. On a lightly loaded system, a batch job may execute immediately (which also means it won’t appear in atq, since it’s already finished).
Reading job output via mailutils. The at command has decades-old built-in integration with local mail delivery via the mailutils package:
sudo apt install mailutils
mail
Subject: Output from your job
Read a specific message by its number:
? 1
The message body reflects exactly what the script did — for a script that only echoes a message with no other side effects, the mail content will simply confirm that outcome.
flowchart TD
Need{"When should the job run?"}
Need -->|"Exact future time, once"| At["at <time>\n(enter script path, Ctrl+D)"]
Need -->|"As soon as resources allow, once"| Batch["batch\n(enter script path, Ctrl+D)"]
At --> Queue["atq shows pending jobs"]
Queue --> Cancel["atrm <job#> to cancel"]
At --> Mail["mailutils delivers job output to local mailbox"]
Batch --> Mail
| Tool | Timing | Repeats? | Shows in atq? |
|---|---|---|---|
at <time> | Exact specified time | No — one-time only | Yes, while pending |
batch | First available idle moment | No — one-time only | No (often runs immediately) |
cron / crontab -e | Recurring, clock-based schedule | Yes | N/A |
| systemd timer | Recurring, calendar or boot-relative schedule | Yes | N/A (systemctl list-timers) |
Summary
This course walked through the essential toolkit for managing a Linux system end to end:
- Debian/Ubuntu package management revolves around APT as the friendly front end and dpkg as the low-level engine underneath it. Repository configuration lives in
/etc/apt, using the modern deb822 format; third-party sources like PPAs extend what’s available, while snaps (and, to a lesser extent, Flatpak and AppImage) offer a fully sandboxed alternative delivery model. Searching (apt search/show/list) and maintenance (autoremove,upgrade,purge,reinstall) round out the day-to-day workflow. - Red Hat Enterprise Linux family package management is built on RPM (the low-level engine) and DNF (the dependency-resolving front end that replaced yum, though yum is preserved as a symlink for compatibility). Repository configuration lives in
/etc/yum.repos.d; EPEL (community) and Red Hat Extensions (subscription-based) extend the default single repo, but should never both be enabled simultaneously.rpm -q*inspects local packages;dnf repoqueryinterrogates remote repository metadata with far more flexibility. - Networking fundamentals cover IPv4/NAT/IPv6 addressing, DNS resolution, protocols (ICMP/TCP/UDP), well-known ports, and firewalls (firewalld/UFW over netfilter). A disciplined, “local outward” troubleshooting method — interface, route, loopback, gateway, external address, DNS, then hop-by-hop tracing — resolves most connectivity problems methodically. Tools like
ss,curl,nmcli, andnmapprovide broader visibility into what’s actually happening on the network, whiledig,nslookup, andresolvectlare the go-to tools for DNS-specific issues. - Shell automation starts with variables (mind the scope difference between plain and
exported variables, and between double- and single-quoted expansion) and builds toward complete, reusable Bash scripts — from simple conditional/loop logic to fully interactive scripts that prompt for input and generate consistent, repeatable infrastructure. - Scheduling rounds out the automation story: cron (with anacron as its missed-job safety net) remains a widely used legacy standard; systemd timers offer tighter integration, built-in missed-run recovery, and centralized logging; and the ancient but still handy at/batch commands cover one-off and idle-time execution needs.
Quick-Reference Command Table
| Task | Debian/Ubuntu | Red Hat family |
|---|---|---|
| Sync package index | apt update | dnf (implicit on most operations) |
| Install a package | apt install <pkg> | dnf install <pkg> |
| Search for a package | apt search --names-only '^name$' | dnf repoquery <name> |
| Show package details | apt show <pkg> | dnf repoquery -i <pkg> / rpm -qi |
| Remove a package | apt remove <pkg> | dnf remove <pkg> |
| Purge config too | apt purge <pkg> | (reinstall/manual config cleanup) |
| Clean up orphan dependencies | apt autoremove | (handled automatically on remove) |
| Apply all upgrades | apt upgrade | dnf update |
| Manual package file install | dpkg -i file.deb | rpm -i file.rpm |
| List files in a package | dpkg -L <pkg> | dnf repoquery -l <pkg> / rpm -ql |
| Find package providing a file | dpkg -S <file> | dnf repoquery -f <file> / rpm -qf |
Checklist for Ongoing System Hygiene
- Sync the package index before installing or upgrading anything.
- Prefer official repositories; vet any third-party repo (PPA, custom
.repofile) before adding it. - Periodically audit and remove unused packages (
apt autoremove, reviewdnfinstalled lists). - Apply security upgrades promptly — don’t leave systems unpatched.
- Never enable both EPEL community and Red Hat Extensions repos simultaneously.
- Work from the local machine outward when troubleshooting connectivity: interface → route → loopback → gateway → external address → DNS.
- Use
dig/resolvectlrather thanpingalone when diagnosing DNS, since ICMP can be blocked independently of DNS health. - Quote shell variables in scripts (
"$variable") to avoid word-splitting surprises. - Remember
exportis required for a variable to reach child shells or scripts; persistent variables belong in.bashrc/.profile//etc/bash.bashrc. - Choose the right scheduler for the job: recurring and simple → cron; recurring with strong systemd integration/logging needs → systemd timer; one-off at a specific time →
at; one-off whenever resources allow →batch.
Search Terms
networking · packages · automation · web · servers · systems · security · software · dnf · tasks · automating · repositories · shell · apt · bash · hat · installing · linux · managing · network · querying · red · removing · rpm