One of the first things I had to unlearn when I moved from Windows to Linux was the habit of downloading .exe files from random websites. Debian-based systems like Ubuntu do things very differently, and once it clicks, it’s honestly a better system — centralized repositories, cryptographic verification, dependency resolution, and clean uninstalls. In this guide I’m going to walk through exactly how package management works on Debian and Ubuntu, from the high-level apt commands you’ll use every day down to the low-level dpkg mechanics running underneath.
The Two Layers: dpkg and apt
Debian-based package management operates on two layers, and understanding the split makes everything else make sense.
dpkg is the low-level package manager. It knows how to install, remove, and query individual .deb package files, but it has no concept of remote repositories and no automatic dependency resolution. If a package needs another package that isn’t installed, dpkg will simply complain and leave things broken until you fix it manually.
apt (Advanced Package Tool) sits on top of dpkg. It knows about remote repositories, handles dependency resolution automatically, downloads packages over the network, and calls dpkg internally to actually perform the installation. In practice, almost everything you do day-to-day goes through apt, while dpkg is reserved for specific low-level tasks like installing a standalone .deb file you downloaded manually.
You can confirm both are present and check versions with:
apt-get --version
dpkg --version
On a typical Ubuntu 24.04 system this reports:
apt 2.8.3 (amd64)
Debian 'dpkg' package management program version 1.22.6 (amd64).
How the Package Repository System Works
Before any install commands make sense, it helps to understand where packages actually come from. Your system keeps a list of repository sources in /etc/apt/sources.list and additional files under /etc/apt/sources.list.d/. Each line in these files points to a remote archive containing thousands of .deb packages along with metadata describing their versions, dependencies, and checksums.
You can view your current sources with:
cat /etc/apt/sources.list
A typical Ubuntu entry looks something like:
deb http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu noble-security main restricted universe multiverse
Each line specifies the archive URL, the distribution codename (noble is Ubuntu 24.04’s codename), and which component sections to pull from (main, restricted, universe, multiverse — these represent different levels of official support and licensing).
Updating the Package Index
Before installing anything, you need a current picture of what’s available. This is done with:
sudo apt update
This command doesn’t install or upgrade anything — it just downloads fresh metadata from your configured repositories and updates apt’s local cache of available package versions. Skipping this step means apt might try to install an outdated version or fail to find a package that was recently added.
I’d recommend running apt update as a near-automatic reflex before any install, especially on a system you haven’t touched in a while.
Installing Packages
The command you’ll type more than any other is:
sudo apt install curl
I tested this on a system that already had curl installed, and the output looked like this:
Reading package lists...
Building dependency tree...
Reading state information...
curl is already the newest version (8.5.0-2ubuntu10.8).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Notice how apt tells you exactly what it plans to do before doing it: how many packages will be upgraded, newly installed, removed, or left untouched. On a fresh install, apt shows the list of new packages it’s about to pull in (including dependencies), asks for confirmation with a Y/n prompt, then downloads and installs everything in one pass.
You can install multiple packages in a single command:
sudo apt install curl wget git vim
And you can skip the confirmation prompt for scripting purposes with -y:
sudo apt install -y curl
Checking What Would Happen Before Committing
If you want to see what apt would do without actually changing anything, use a dry run:
apt-get install --dry-run curl
This simulates the installation and prints the same summary you’d normally see, but nothing is actually downloaded or installed. This is genuinely useful in automation scripts where you want to check for pending changes before committing to them, or just when you’re cautious about a package that might pull in a large dependency chain.
Searching for Packages
If you’re not sure of the exact package name, search the local cache:
apt search nginx
This searches package names and descriptions and returns matches with a short summary line for each. For a more targeted lookup of a specific package’s metadata — version, dependencies, repository source — use:
apt-cache policy curl
On my test system this returned:
curl:
Installed: 8.5.0-2ubuntu10.8
Candidate: 8.5.0-2ubuntu10.8
Version table:
*** 8.5.0-2ubuntu10.8 500
500 http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
500 http://security.ubuntu.com/ubuntu noble-security/main amd64 Packages
100 /var/lib/dpkg/status
8.5.0-2ubuntu10 500
500 http://archive.ubuntu.com/ubuntu noble/main amd64 Packages
This output is worth understanding line by line: Installed shows what’s currently on your system, Candidate shows what apt would install/upgrade to, and the version table below shows every available version across your configured repositories along with their priority numbers. Those priority numbers (500, 100, etc.) determine which version apt picks when multiple are available — this is the foundation of “APT pinning,” a more advanced technique for locking specific package versions.
Upgrading Packages
Keeping a system updated is a two-step habit:
sudo apt update
sudo apt upgrade
apt upgrade installs the newest available versions of all currently installed packages, but it won’t remove any existing package or install new ones to satisfy changed dependencies — it plays it safe.
For a more thorough upgrade that can add or remove packages as needed to resolve dependency changes (common after a release with restructured packages), use:
sudo apt full-upgrade
For upgrading between Ubuntu releases entirely (e.g., 22.04 to 24.04), the correct tool is do-release-upgrade, not apt upgrade — that’s a separate, more involved process outside the scope of routine package management.
Removing Packages
There are two levels of removal, and the distinction matters:
sudo apt remove nginx
This removes the package’s binaries and files but leaves configuration files behind in case you reinstall later.
sudo apt purge nginx
This removes the package and its configuration files, giving you a genuinely clean slate. If you’re decommissioning software permanently rather than temporarily disabling it, purge is almost always what you actually want.
After removals, dependency packages that were auto-installed and are no longer needed by anything else can pile up. Clean those with:
sudo apt autoremove
Listing Installed Packages
To see everything currently installed on your system:
apt list --installed
Sample output:
Listing...
adduser/noble,now 3.137ubuntu1 all [installed,automatic]
adwaita-icon-theme/noble,now 46.0-1 all [installed,automatic]
apt-transport-https/noble-updates,now 2.8.3 all [installed]
apt/noble-updates,now 2.8.3 amd64 [installed]
The [installed,automatic] tag tells you this package was pulled in as a dependency of something else, rather than explicitly requested by a human — useful context when you’re auditing what’s actually on a system and why.
For a lower-level, more compact view via dpkg directly:
dpkg -l
Sample output:
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name Version Architecture Description
+++-====================================-====================================-============-================================================================================
ii adduser 3.137ubuntu1 all add and remove users and groups
ii adwaita-icon-theme 46.0-1 all default icon theme of GNOME
ii apt 2.8.3 amd64 commandline package manager
The ii at the start of each line is a status code: the first letter shows the desired state (i for install) and the second shows the actual current state (i for installed). If a package failed halfway through installation, you might see codes like iF (half-configured) or rc (removed, but config files remain) — these are your first clue when troubleshooting a broken package.
Working Directly with .deb Files and dpkg
Sometimes you’ll download a .deb file directly from a vendor’s website rather than pulling from a repository — Google Chrome and VS Code are common examples. Installing one directly uses dpkg, not apt:
sudo dpkg -i package-name.deb
The catch: dpkg doesn’t resolve or download dependencies automatically. If the package needs something you don’t have, dpkg will report a dependency error and leave the package in a “half-configured” state. The standard fix is:
sudo apt install -f
The -f flag tells apt to fix broken dependencies, which it does by identifying what dpkg needed and downloading/installing those pieces from your configured repositories, then finishing the dpkg installation cleanly.
To remove a package installed via dpkg:
sudo dpkg -r package-name
To check if a specific package is installed and see its details:
dpkg -s package-name
To list which package a specific installed file belongs to (useful for troubleshooting “where did this file come from”):
dpkg -S /usr/bin/curl
How This Works Internally
When you run apt install, several things happen in sequence under the hood. First, apt consults its local package cache (built from your last apt update) to resolve the full dependency tree for the requested package. It calculates which additional packages need to be installed or upgraded to satisfy every dependency, checks version constraints, and builds an installation plan.
Once you confirm, apt downloads the required .deb files into /var/cache/apt/archives/, verifies their integrity using cryptographic checksums against the repository’s signed metadata (this is what protects you from tampered packages — repositories are signed with GPG keys stored in /etc/apt/trusted.gpg.d/), and then hands each .deb file off to dpkg for actual installation: unpacking files into the filesystem, running pre/post-install scripts, and registering the package in dpkg‘s internal database at /var/lib/dpkg/status.
This layered design — apt for dependency resolution and network operations, dpkg for the actual on-disk work — is why you’ll occasionally see both commands referenced even for what feels like a single logical task.
Configuration Files and Package Management Behavior
The main apt configuration lives in /etc/apt/apt.conf.d/, split into numbered files that get processed in order — a Debian convention you’ll see repeated across many subsystems (cron, PAM modules, etc.). You generally won’t need to touch these unless you’re doing advanced tuning like configuring proxy settings for apt or adjusting retry behavior on flaky networks.
Repository signing keys are managed in /etc/apt/trusted.gpg.d/ and, on newer systems, referenced explicitly per-source using the signed-by option in source list entries — a security improvement introduced to move away from a single global keyring toward per-repository key scoping.
Troubleshooting Common Package Issues
“Unable to locate package” — Almost always means you haven’t run apt update recently, or the package genuinely isn’t in any of your configured repositories (it might live in a PPA or third-party repo you need to add first).
“E: Could not get lock /var/lib/dpkg/lock-frontend” — Another apt/dpkg process is currently running, often an automatic unattended-upgrade in the background. Wait a minute and retry, or check ps aux | grep apt to see what’s holding the lock. Killing the process manually is a last resort and can corrupt your package database if done carelessly.
Broken dependencies after a partial install (often from a power loss or interrupted download) — Run:
sudo dpkg --configure -a
sudo apt install -f
The first command finishes configuring any packages left in a half-installed state; the second resolves any remaining dependency gaps.
Held-back packages during upgrade — Sometimes apt upgrade reports packages it won’t touch because doing so would require installing or removing something else. This is intentional caution; run apt full-upgrade if you’re comfortable letting apt make those broader changes.
Best Practices
A few habits worth building: always run apt update before installing anything on a system you haven’t recently touched; prefer apt purge over apt remove when you’re actually done with software rather than temporarily disabling it; run apt autoremove periodically to clean up orphaned dependencies; and be cautious about adding third-party repositories or PPAs, since they extend your trust boundary — you’re trusting whoever signs those packages just as much as you trust the official Ubuntu/Debian maintainers.
For servers specifically, consider enabling unattended-upgrades for automatic security patching, but pair it with proper monitoring so you know what changed and when — silent automatic upgrades without visibility can turn into a debugging nightmare later.
Compatibility Notes
Everything covered here applies directly to Ubuntu and Debian, plus their many derivatives — Linux Mint, Pop!_OS, elementary OS, Kali Linux, and Raspberry Pi OS all use the same apt/dpkg foundation. Distributions outside the Debian family — Fedora/RHEL (which use dnf/yum and .rpm files), Arch (pacman), and openSUSE (zypper) — use entirely different package managers and formats, so none of the specific commands here transfer over, even though the underlying concepts (repositories, dependency resolution, package databases) are broadly similar across all of them.
Summary
Package management is one of the areas where Debian and Ubuntu genuinely shine compared to manual software installation. apt handles the friendly, high-level work of finding, downloading, and resolving dependencies for software, while dpkg quietly does the low-level heavy lifting underneath. Once you’re comfortable with apt update, apt install, apt remove/purge, and apt autoremove, plus knowing when to drop down to dpkg for standalone .deb files, you have essentially the entire toolkit needed for day-to-day software management on these systems.
References
- Debian Wiki — APT: https://wiki.debian.org/Apt
- Debian Administrator’s Handbook, Package Management chapter: https://debian-handbook.info/browse/stable/sect.package-meta-information.html
- Ubuntu Documentation — Package Management: https://help.ubuntu.com/community/AptGet/Howto
- dpkg man page: https://man7.org/linux/man-pages/man1/dpkg.1.html
