Beginner

Networking Packages and Automation Basics

networking · packages · automation · web · servers · systems · security · software · dnf · tasks · automating · repositories · shell · apt · bash · hat · installing · linux · managing · n...

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

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 segmentMeaning
security.ubuntu.comThe repository host
nobleThe Ubuntu release codename (24.04)
universeThe repository component (as opposed to main, restricted, or multiverse)
binary-amd64Built 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:

  1. Read through the package index and resolve any necessary dependencies.
  2. Show you what will change and how much disk space the operation will consume.
  3. Download the package from the location recorded in the index files.
  4. 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
FieldPurpose
TypesPackage type (deb for binaries, deb-src for source packages)
URIsThe online home of the repository
SuitesThe release (“noble”) plus variants like noble-updates (patched packages) and noble-backports (newer, non-standard versions for special use cases)
Componentsmain, universe, restricted, multiverse — the last two contain software some users avoid because of non-free licensing or limited support
Signed-ByThe 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.

FormatSandboxing modelPrimary use caseTypical ecosystem
SnapFull sandbox, own dependencies bundledGeneral-purpose apps and servicesUbuntu / Canonical
FlatpakFull sandbox, own dependencies bundledDesktop applicationsRed Hat / Fedora primarily
AppImageSelf-contained; no installation required at allPortable, single-file desktop appsDistribution-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
CommandPurposePrecision
apt search <term>Full-text search across names and descriptionsLow — many false positives
apt search --names-only <term>Search names onlyMedium
apt search --names-only '^term$'Regex-anchored exact name matchHigh
apt show <name>Full metadata for a known package nameN/A — requires exact name
apt list <name>Quick installed/available statusHigh, 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.

OperationAPT commanddpkg equivalent
Installapt install <pkg>dpkg -i <file>.deb
Remove (keep config)apt remove <pkg>dpkg -r <pkg>
Remove (with config)apt purge <pkg>dpkg -P <pkg>
Reinstallapt reinstall <pkg>
Clean up orphansapt autoremove
Apply all upgradesapt 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:

  1. No default “It works!” welcome page is created for you.
  2. 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"]
RepositorySourceRequires subscription?Package overlap risk
Base Red Hat repoRed HatYes (built in)
Code Ready BuilderRed HatYesEnables building/dependency packages
EPEL (community)Fedora projectNoConflicts with Extensions if both enabled
Red Hat ExtensionsRed HatYes (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
FlagPurpose
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 --querytagsList 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 from 1.1.1.1 to 255.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/24 and 192.168.2.0/24 are two distinct networks).

Because the number of internet-connected devices vastly exceeds 4 billion, two innovations addressed IPv4 exhaustion:

  1. 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.
  2. 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:

ProtocolCharacteristicsTypical use
ICMPLightweight, connectionlessQuick connectivity diagnostics (ping)
TCPReliable, connection-orientedWeb browsing, most application traffic
UDPFast, connectionless, no delivery guaranteeVideo 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.

PortService
22SSH
53DNS
80HTTP (unencrypted)
443HTTPS (encrypted)
631CUPS 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 nmcli command-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
SettingMeaning
nameserver 127.0.0.53Requests are handled by the local systemd-resolved DNS resolver
edns0Enables modern DNS extensions requiring more resources than legacy defaults assumed
trust-adIf 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.

ToolBest for
hostQuick name → IP lookup
pingBasic reachability (can give false negatives if ICMP is firewalled)
digAuthoritative-style DNS record lookups, works even when ICMP is blocked
nslookupSimilar to dig, slightly more verbose, clearly labels non-authoritative answers
resolvectl statusShows which DNS servers are actually being consulted per interface
resolvectl flush-caches / show-cacheDiagnose and clear stale cached DNS results
journalctl -u systemd-resolvedDNS-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 quotesExpanded, 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:

FileScope
~/.bashrcEvery non-login shell for that user (e.g., a terminal opened from a desktop GUI)
~/.profileLogin sessions for that user
/etc/bash.bashrcAll 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) equals 0 — the only user with that ID is root. If the script runs via sudo, this condition is met; otherwise the else branch runs. fi closes the block.
  • The for loop uses * (a glob) to iterate over every item in the current directory. COUNT starts at 0 and increments (((COUNT++))) each pass; once it reaches 5, break exits the loop early.
  • The disk usage section runs df -h / (human-readable output, root partition only), pipes it to awk, 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_name displays 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 touch line follows the same brace-expansion pattern to create three placeholder files in one command.
  • The heredoc (<< EOL ... EOL) writing README.md works 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
FieldValue in exampleMeaning
Minute3030th minute of the hour
Hour88 AM (24-hour clock; 16 would mean 4 PM)
Day of month*Every day of the month
Month*Every month
Day of week1-5Monday through Friday
Command/home/user/myscript.shFull 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
FieldMeaning
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 identifierA name for the job (used for anacron’s own bookkeeping)
CommandWhat 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
SettingPurpose
DescriptionHuman-readable label
After=network.targetEnsures the network is up before this unit starts (add other required units/resources here too)
Type=oneshotThe service runs a single command and exits, rather than staying resident
ExecStartFull path to the script or binary to execute
UserThe 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
SettingPurpose
Unit (implicit, matches file name)Refers to backup.service
OnCalendar=dailyRuns at midnight every day (more specific calendar expressions are supported)
Persistent=trueRuns any missed executions (e.g., after the system was shut down at the scheduled time) — comparable to anacron’s role for cron
OnBootSec=5minRuns 5 minutes after boot if a scheduled run was missed
WantedBy=timers.targetRegisters 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"]
AspectCronSystemd timer
ConfigurationSingle-line crontab syntaxTwo unit files (.service + .timer)
Missed-run recoveryRequires anacronBuilt in via Persistent=true
LoggingScattered across syslog/cron logsCentralized, filterable via journalctl -u
Dependency awarenessNone built inNative (After=, other systemd dependencies)
Legacy familiarityVery highLower, 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
ToolTimingRepeats?Shows in atq?
at <time>Exact specified timeNo — one-time onlyYes, while pending
batchFirst available idle momentNo — one-time onlyNo (often runs immediately)
cron / crontab -eRecurring, clock-based scheduleYesN/A
systemd timerRecurring, calendar or boot-relative scheduleYesN/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 repoquery interrogates 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, and nmap provide broader visibility into what’s actually happening on the network, while dig, nslookup, and resolvectl are 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

TaskDebian/UbuntuRed Hat family
Sync package indexapt updatednf (implicit on most operations)
Install a packageapt install <pkg>dnf install <pkg>
Search for a packageapt search --names-only '^name$'dnf repoquery <name>
Show package detailsapt show <pkg>dnf repoquery -i <pkg> / rpm -qi
Remove a packageapt remove <pkg>dnf remove <pkg>
Purge config tooapt purge <pkg>(reinstall/manual config cleanup)
Clean up orphan dependenciesapt autoremove(handled automatically on remove)
Apply all upgradesapt upgradednf update
Manual package file installdpkg -i file.debrpm -i file.rpm
List files in a packagedpkg -L <pkg>dnf repoquery -l <pkg> / rpm -ql
Find package providing a filedpkg -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 .repo file) before adding it.
  • Periodically audit and remove unused packages (apt autoremove, review dnf installed 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/resolvectl rather than ping alone when diagnosing DNS, since ICMP can be blocked independently of DNS health.
  • Quote shell variables in scripts ("$variable") to avoid word-splitting surprises.
  • Remember export is 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.