I’ve managed Debian and Ubuntu servers for years now, and apt is one of those commands I use so often that typing it feels automatic. But when I actually sat down to catalog everything it can do — beyond the handful of commands most people learn on day one — I realized how much of its depth goes unused by most admins. This guide covers apt from the basics through to the details that actually matter in production environments.
What apt Is and How It Fits Into the Debian Package Ecosystem
apt (Advanced Package Tool) is the high-level, user-friendly command-line interface to Debian’s package management system. Underneath it sits dpkg, the low-level tool that actually installs, removes, and tracks individual .deb package files on disk. apt adds dependency resolution, repository management, and network-based package retrieval on top of what dpkg alone can do — dpkg can install a .deb file you already have, but it has no concept of “go fetch this package and its dependencies from the internet.” That’s entirely apt‘s job.
I confirmed the version on my system while writing this:
apt --version
apt 2.8.3 (amd64)
apt itself is relatively modern, introduced to unify and simplify the previously separate apt-get and apt-cache tools into a single command with friendlier default output, colorized progress bars, and more sensible defaults for interactive use, while apt-get/apt-cache remain available and are still preferred inside scripts for their more stable output format.
Basic Syntax
apt [options] command
Here are the most-used commands, straight from apt‘s own help output:
list - list packages based on package names
search - search in package descriptions
show - show package details
install - install packages
reinstall - reinstall packages
remove - remove packages
autoremove - automatically remove all unused packages
update - update list of available packages
upgrade - upgrade the system by installing/upgrading packages
full-upgrade - upgrade the system by removing/installing/upgrading packages
edit-sources - edit the source information file
satisfy - satisfy dependency strings
Updating the Package Index
The very first command I run on any system, and the one that has to run before installing anything meaningfully new:
sudo apt update
This refreshes the local package index by downloading the latest Packages files from each configured repository listed in /etc/apt/sources.list and /etc/apt/sources.list.d/*.list. It doesn’t install or upgrade anything itself — it just makes sure apt knows what’s currently available and at what versions.
Upgrading Installed Packages
sudo apt upgrade
This upgrades all currently installed packages to their latest available versions, but specifically avoids installing new packages or removing existing ones, even if that would technically be required to satisfy a dependency change. If a particular upgrade would require removing a package, apt upgrade holds that package back rather than removing anything.
sudo apt full-upgrade
This is the more aggressive version — it will add or remove packages as needed to bring everything to the latest available version, handling more complex dependency changes (like a package being renamed or split across a major version bump). I use full-upgrade for routine maintenance on servers where I trust the process, and plain upgrade when I want more conservative, predictable behavior, particularly right before a scheduled maintenance window where I want to review exactly what’s changing.
Searching for Packages
apt search nginx
Searches package names and descriptions for a keyword. This doesn’t require an internet connection beyond the initial apt update, since it searches the locally cached index.
apt list --installed
Lists every currently installed package. I tested this directly:
apt list --installed 2>/dev/null | head -3
adduser/noble,now 3.137ubuntu1 all [installed,automatic]
adwaita-icon-theme/noble,now 46.0-1 all [installed,automatic]
The 2>/dev/null in that example suppresses a warning apt prints about its CLI interface being unstable for scripting — worth knowing since it’s a reminder that for anything you actually intend to parse programmatically, apt-get/dpkg-query with structured output is the more stable choice.
apt list --upgradable
Shows exactly which installed packages have newer versions available, without installing anything — I run this before deciding whether an upgrade is even worth doing right now.
Getting Package Details
apt show nginx
Displays detailed metadata about a package — version, dependencies, description, maintainer, install size — whether or not it’s currently installed. I use this constantly to check a package’s exact version before deciding to install it, or to confirm what a dependency chain looks like before committing to an install.
Installing Packages
sudo apt install nginx
The most common command in the entire tool. It resolves dependencies, downloads what’s needed, and installs everything in the correct order.
Installing a Specific Version
sudo apt install nginx=1.24.0-1
Useful when you need to pin to a known-good version rather than whatever’s latest, particularly relevant when a newer version has introduced a regression you’re avoiding.
Installing Multiple Packages at Once
sudo apt install nginx postgresql redis-server
Simulating an Install Before Committing
apt install --dry-run nginx
This shows exactly what would happen — what would be installed, upgraded, or removed — without actually doing anything. I run this reflexively before any install on a production system I’m not 100% sure about, since it surfaces unexpected side effects (like an unrelated package getting pulled in as a new dependency) before they actually happen.
Removing Packages
sudo apt remove nginx
Removes the package but leaves configuration files behind in case you reinstall later and want your settings preserved.
sudo apt purge nginx
Removes the package and its configuration files. I use purge specifically when I want a truly clean uninstall, particularly before reinstalling a service from scratch to rule out a corrupted config as the source of a problem.
Cleaning Up Unused Dependencies
sudo apt autoremove
Removes packages that were originally installed as dependencies of something else, but are no longer required by anything currently installed — the classic case being leftover kernel headers or libraries after you remove the software that originally pulled them in.
sudo apt autoremove --purge
Combines both behaviors — removes unused dependencies and their configuration files in one pass.
Reinstalling Packages
sudo apt reinstall nginx
Forces a fresh reinstall of the currently installed version, useful when files have been accidentally modified or deleted and you want to restore them from the package without changing versions.
Managing Repository Sources
sudo apt edit-sources
Opens /etc/apt/sources.list in your configured default editor for direct editing, with built-in validation of the file format before saving.
A typical sources.list entry looks like:
deb http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
deb-src http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
Modern Ubuntu releases have shifted toward the newer .sources deb822 format in /etc/apt/sources.list.d/ubuntu.sources, which uses a more structured, multi-line key-value format instead of the single-line classic format, though both remain supported.
Dependency Satisfaction
apt satisfy "libssl-dev (>= 3.0), python3"
The satisfy command lets you express a raw dependency string and have apt figure out how to fulfill it, which is genuinely useful when building custom packages that need specific version constraints resolved on the fly, without wrapping the requirement in a real .deb package first.
Holding Packages at a Fixed Version
Sometimes you deliberately want to prevent a specific package from being upgraded, for compatibility reasons:
sudo apt-mark hold nginx
sudo apt-mark unhold nginx
I use this on servers running software with strict version compatibility requirements against other components, where an automatic upgrade could break something downstream.
apt-mark showhold
Lists everything currently held, which I check periodically to make sure I haven’t forgotten why something is pinned.
Cleaning Package Caches
Downloaded .deb files accumulate in /var/cache/apt/archives/. Over time on a long-running server, this can consume meaningful disk space:
sudo apt clean
Removes everything from the local cache of downloaded package files.
sudo apt autoclean
Removes only the packages that can no longer be downloaded (because they’re no longer in the repository, typically superseded by a newer version), keeping the cache for currently-available packages intact in case you need to reinstall without re-downloading.
Automation and Non-Interactive Usage
For scripts and CI pipelines, I always add flags to avoid interactive prompts:
sudo DEBIAN_FRONTEND=noninteractive apt install -y nginx
-y auto-confirms prompts, and DEBIAN_FRONTEND=noninteractive prevents any package’s post-install scripts from trying to open an interactive configuration dialog (like the classic tzdata timezone selector), which would otherwise hang a script indefinitely waiting for input that never comes.
A Realistic Maintenance Script
Here’s a pattern I use on servers for routine automated maintenance:
#!/bin/bash
set -euo pipefail
echo "Updating package index..."
apt update
echo "Listing upgradable packages..."
apt list --upgradable
echo "Applying upgrades..."
DEBIAN_FRONTEND=noninteractive apt upgrade -y
echo "Removing unused packages..."
apt autoremove --purge -y
echo "Cleaning package cache..."
apt autoclean
echo "Maintenance complete."
Comparison with apt-get and apt-cache
I get asked which one is “correct” fairly often. The honest answer: apt is meant for interactive human use — nicer progress bars, a smaller and more approachable command set, colorized output. apt-get and apt-cache are meant for scripting — their output format is considered stable across versions in a way apt‘s explicitly is not (it even warns you about this if you try to parse its output). My rule of thumb: type apt when I’m sitting at a terminal doing something myself, use apt-get/apt-cache/apt-mark when writing anything that needs to run unattended or be parsed programmatically.
Troubleshooting Common Issues
“Unable to locate package” — almost always means apt update hasn’t been run recently, or the package genuinely isn’t in any configured repository; check apt search <name> and verify your sources.list includes the right repository component (main, universe, multiverse, etc.).
“Could not get lock /var/lib/dpkg/lock-frontend” — another apt/dpkg process is currently running, possibly an automatic unattended-upgrade in the background; wait and retry, or check with ps aux | grep -i apt to confirm what’s holding the lock before considering anything more drastic.
Held-back packages during upgrade — run apt full-upgrade instead of apt upgrade if the held-back packages genuinely need dependency changes to resolve, after first reviewing exactly what would change.
Broken dependencies after an interrupted install — sudo apt --fix-broken install (or apt install -f) attempts to resolve and complete a partially broken package state.
Security Implications
Only add repositories you trust, since apt will happily execute post-install scripts from any package with root privileges during installation. Always verify a third-party repository’s GPG key through an official, secure channel before adding it. I keep unattended-upgrades configured on most of my servers specifically for security patches, while leaving feature upgrades to deliberate, manually-reviewed maintenance windows.
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Compatibility Notes
apt is specific to Debian and Debian-derived distributions — Ubuntu, Linux Mint, Pop!_OS, Raspberry Pi OS, and many others. RHEL-family distributions (Fedora, CentOS, Rocky Linux, AlmaLinux) use dnf/yum instead, which is conceptually similar but has an entirely different command syntax and underlying package format (RPM rather than .deb).
Understanding APT Pinning for Version Control
Beyond simple holds, apt supports a more sophisticated pinning system through /etc/apt/preferences (or files under /etc/apt/preferences.d/), letting you control which repository or version apt prefers when multiple sources offer the same package — genuinely useful when you’re mixing a stable base repository with a backports or testing repository for select packages only:
Package: nginx
Pin: release a=jammy-backports
Pin-Priority: 500
This tells apt to prefer the backports version of nginx specifically, while leaving every other package to resolve normally from the standard repositories. I use pinning like this when I need one specific newer package version without pulling my entire system onto a less stable release track just to get it.
apt-cache policy nginx
This shows exactly which version apt would currently choose for a package and why, walking through the priority calculation across every configured source — invaluable for debugging “why did it install that version and not the one I expected” situations.
Working with Third-Party Repositories and Signed Keys
Adding a third-party repository properly, with signature verification intact, looks like this on modern Ubuntu/Debian systems:
curl -fsSL https://example.com/repo/gpg-key.asc | sudo gpg --dearmor -o /usr/share/keyrings/example-repo.gpg
echo "deb [signed-by=/usr/share/keyrings/example-repo.gpg] https://example.com/repo stable main" | sudo tee /etc/apt/sources.list.d/example.list
sudo apt update
The signed-by directive scopes trust for that specific repository to that specific key, rather than adding the key to the system-wide trusted keyring (the older apt-key add approach, now deprecated specifically because it granted a key trust across every repository on the system rather than just the one it was meant for). I always use this scoped approach for any third-party repository I add today.
Summary
apt gives you a clean, sensible interface over Debian’s package ecosystem, handling dependency resolution and repository management so you rarely need to think about individual .deb files directly. The core loop — update, then install/upgrade, with autoremove and clean for housekeeping — covers the overwhelming majority of real-world usage, while commands like satisfy, apt-mark hold, and --dry-run cover the more advanced scenarios that come up as you manage more complex, long-lived systems.
References
man 8 apt- Debian Wiki: Apt (wiki.debian.org/Apt)
- Debian Administrator’s Handbook, package management chapters
- Ubuntu official documentation on package management