📦 apt Package Manager Guide

A hands-on manual for the package manager of Debian and Ubuntu: update the lists first, look before you install, install and remove cleanly, upgrade safely, read the history, add repositories, and hold a package back. Includes the errors and what they mean, and a table next to dnf.

Video: apt Package Manager Guide (Sysadmin video)
Prefer to watch? This short video runs this page's main commands in a real lab, with on-screen captions and a synthetic voiceover (an AI voice trained on the author's own recording). Click to play, or watch it on YouTube.
sudo apt update                # download the package lists
sudo apt install tree          # install a package and what it needs
sudo apt upgrade               # bring the system up to date

apt is the tool behind every install, update and removal on Ubuntu, Debian and the systems built on them. This guide goes through what you use every day, in the order you meet it: the package lists, finding a package, installing it, removing it, upgrading the machine, reading the history, and the repositories everything comes from.

Then the parts that rescue you: holding a package at one version, going back a version, repairing an interrupted install, reading the error when a name or a repository is wrong. A table at the end sets apt next to dnf.

Every example ran for real, as an ordinary user with sudo, on Ubuntu 24.04 (apt 2.8.3) in a clean container. The output you see is the output the machine gave.

What apt is, and the lab

apt is the package manager of Debian, Ubuntu, Linux Mint and the systems built on them. It installs, removes and upgrades software, works out which other packages each one needs, downloads everything from the repositories you have switched on, and checks the signature on the package lists before it trusts them. Under the hood it installs .deb files, the same format the packaging guides in this series build.

Every example below ran for real on Ubuntu 24.04, as an ordinary user with sudo, in a clean container made from the ubuntu:24.04 image. Debian and the other Ubuntu releases use the same commands.

Start by asking the machine what it is and which apt it has:

$ grep PRETTY_NAME /etc/os-release
PRETTY_NAME="Ubuntu 24.04.5 LTS"
$ apt --version
apt 2.8.3 (amd64)
$ ls -l /usr/bin/apt /usr/bin/apt-get /usr/bin/apt-cache
-rwxr-xr-x 1 root root 18824 Oct 22  2024 /usr/bin/apt
-rwxr-xr-x 1 root root 88544 Oct 22  2024 /usr/bin/apt-cache
-rwxr-xr-x 1 root root 51680 Oct 22  2024 /usr/bin/apt-get

Three programs, not links to each other. apt is the one to type at a keyboard: it shows a progress bar and asks questions. apt-get and apt-cache are the older, steadier programs for scripts, because their output does not change between releases. When apt is not writing to a terminal it says so:

$ apt list --installed 2>&1 | head -3

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

That warning is why scripts should use apt-get. This guide uses apt, and says so where a script wants apt-get.

apt needs root to change anything, so the commands that install, remove or upgrade start with sudo. The commands that only look (search, show, list, policy) work as an ordinary user.

Update the package lists first

apt does not look at the internet when you ask for a package. It reads local lists of what each repository offers. In a fresh container there are none:

$ apt search tree 2>/dev/null | head -3
Sorting...
Full Text Search...
$ sudo apt install -y tree

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package tree
(exit status 100)

The second command fails, because the package is "not found" in lists that are empty. The fix is the command you will type more than any other:

$ sudo apt update 2>&1 | tail -8
Get:17 http://archive.ubuntu.com/ubuntu noble-backports/main amd64 Packages [49.0 kB]
Get:18 http://archive.ubuntu.com/ubuntu noble-backports/multiverse amd64 Packages [669 B]
Get:19 http://archive.ubuntu.com/ubuntu noble-backports/universe amd64 Packages [36.0 kB]
Fetched 33.7 MB in 4s (8381 kB/s)
Reading package lists...
Building dependency tree...
Reading state information...
8 packages can be upgraded. Run 'apt list --upgradable' to see them.

apt update downloads the lists, it does not upgrade anything. The lines start with Get (a list was downloaded), Hit (it has not changed since the last time) or Ign (ignored). Each repository's list is checked against its signature before it is kept, which is why the signature work in the signed-repository guide matters. The last line says how many packages could be upgraded; the lists live in /var/lib/apt/lists:

$ ls /var/lib/apt/lists | head -4
archive.ubuntu.com_ubuntu_dists_noble-backports_InRelease
archive.ubuntu.com_ubuntu_dists_noble-backports_main_binary-amd64_Packages.lz4
archive.ubuntu.com_ubuntu_dists_noble-backports_multiverse_binary-amd64_Packages.lz4
archive.ubuntu.com_ubuntu_dists_noble-backports_universe_binary-amd64_Packages.lz4
$ ls /var/lib/apt/lists | wc -l
22

Look before you install: search, show, policy, apt-file

Search by word. Without options apt looks through names and descriptions, which finds a lot; --names-only narrows it to the names. The 2>/dev/null hides the warning from section 1:

$ apt search --names-only nmap 2>/dev/null | head -12
Sorting...
Full Text Search...
libcoq-mathcomp-finmap/noble 2.0.0-2build5 amd64
  finite sets and maps extension for Mathematical Components

libnmap-parser-perl/noble 1.37-2 all
  module to parse nmap scan results with perl

librust-cpp-synmap-dev/noble 0.3.0-1 amd64
  Sourcemap and full crate parsing support for `cpp_syn` - Rust source code

nmap/noble 7.94+git20230807.3be01efb1+dfsg-3build2 amd64
$ apt search --names-only '^nmap$' 2>/dev/null
Sorting...
Full Text Search...
nmap/noble 7.94+git20230807.3be01efb1+dfsg-3build2 amd64
  The Network Mapper

Show the details of one package: version, size, what it depends on, a description, the repository:

$ apt show nmap 2>/dev/null | head -22
Package: nmap
Version: 7.94+git20230807.3be01efb1+dfsg-3build2
Priority: extra
Section: universe/net
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Debian Security Tools <team+pkg-security@tracker.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 4169 kB
Depends: nmap-common (= 7.94+git20230807.3be01efb1+dfsg-3build2), libc6 (>= 2.38), libgcc-s1 (>= 3.3.1), liblinear4 (>= 2.01+dfsg), liblua5.4-0 (>= 5.4.6), libpcap0.8t64 (>= 1.5.1), libpcre2-8-0 (>= 10.32), libssh2-1t64 (>= 1.2.9), libssl3t64 (>= 3.0.0), libstdc++6 (>= 11), zlib1g (>= 1:1.1.4)
Suggests: ncat, ndiff, zenmap
Homepage: https://nmap.org/
Download-Size: 1694 kB
APT-Sources: http://archive.ubuntu.com/ubuntu noble/universe amd64 Packages
Description: The Network Mapper

policy says which versions exist, which one would be installed, and which repository each comes from. The number in the first column is the priority:

$ apt policy nmap 2>/dev/null
nmap:
  Installed: (none)
  Candidate: 7.94+git20230807.3be01efb1+dfsg-3build2
  Version table:
     7.94+git20230807.3be01efb1+dfsg-3build2 500
        500 http://archive.ubuntu.com/ubuntu noble/universe amd64 Packages

Find out which package supplies a file or a command. dpkg -S answers only for packages that are installed. For the rest, install apt-file, which keeps a list of every file in every package:

$ sudo apt install -y apt-file 2>&1 | tail -2
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
Processing triggers for man-db (2.12.0-4build2) ...
$ sudo apt-file update | tail -2
Fetched 213 MB in 20s (10.5 MB/s)
Reading package lists...
$ apt-file search --regexp '/bin/dig$'
bind9-dnsutils: /usr/bin/dig

The answer to "dig: command not found" is bind9-dnsutils. (Without --regexp, apt-file matches the start of a path and lists everything that begins with /usr/bin/dig.) List what is installed, and count what is available:

$ apt list --installed 2>/dev/null | wc -l
109
$ apt list 2>/dev/null | wc -l
87886

Install a package

-s simulates: apt prints what it would do and does nothing. It is the safe way to look first. nmap needs a few libraries, and the plan lists every package that comes with it:

$ sudo apt install -s nmap 2>/dev/null | head -20
Reading package lists...
Building dependency tree...
Reading state information...
The following additional packages will be installed:
  adduser dbus dbus-bin dbus-daemon dbus-session-bus-common
  dbus-system-bus-common ibverbs-providers libblas3 libdbus-1-3 libexpat1
  libibverbs1 liblinear4 liblua5.4-0 libnl-3-200 libnl-route-3-200
  libpcap0.8t64 libssh2-1t64 nmap-common
Suggested packages:
  liblocale-gettext-perl cron quota ecryptfs-utils default-dbus-session-bus
  | dbus-session-bus liblinear-tools liblinear-dev ncat ndiff zenmap
The following NEW packages will be installed:
  adduser dbus dbus-bin dbus-daemon dbus-session-bus-common
  dbus-system-bus-common ibverbs-providers libblas3 libdbus-1-3 libexpat1
  libibverbs1 liblinear4 liblua5.4-0 libnl-3-200 libnl-route-3-200
  libpcap0.8t64 libssh2-1t64 nmap nmap-common
0 upgraded, 19 newly installed, 0 to remove and 8 not upgraded.
Inst adduser (3.137ubuntu1 Ubuntu:24.04/noble [all])
Conf adduser (3.137ubuntu1 Ubuntu:24.04/noble [all])
Inst libdbus-1-3 (1.14.10-4ubuntu4.1 Ubuntu:24.04/noble-updates [amd64])

Now install for real. Without -y, apt asks Do you want to continue? [Y/n]; with -y it answers yes, which is what scripts want (use apt-get -y there). The output is long, so tail shows the end of it:

$ sudo apt install -y nmap 2>&1 | tail -6
Setting up libibverbs1:amd64 (50.0-2ubuntu0.2) ...
Setting up ibverbs-providers:amd64 (50.0-2ubuntu0.2) ...
Setting up libpcap0.8t64:amd64 (1.10.4-4.1ubuntu3.1) ...
Setting up nmap (7.94+git20230807.3be01efb1+dfsg-3build2) ...
Processing triggers for man-db (2.12.0-4build2) ...
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
$ nmap --version | head -1
Nmap version 7.94SVN ( https://nmap.org )
$ dpkg -l nmap | tail -1
ii  nmap           7.94+git20230807.3be01efb1+dfsg-3build2 amd64        The Network Mapper

dpkg -l shows the status in the first column: ii means installed. dpkg and apt share one database; apt is the part that fetches and resolves, dpkg is the part that unpacks the files.

Install a .deb file you downloaded or built yourself. apt install ./file.deb resolves its dependencies from the repositories, which dpkg -i does not. apt download fetches the file without installing it:

$ mkdir ~/debs && cd ~/debs
$ apt download tree 2>/dev/null
Get:1 http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 tree amd64 2.1.1-2ubuntu3.24.04.2 [47.4 kB]
Fetched 47.4 kB in 1s (45.0 kB/s)
$ ls
debs
tree_2.1.1-2ubuntu3.24.04.2_amd64.deb
$ sudo apt install -y ./tree_*.deb 2>&1 | tail -3
Unpacking tree (2.1.1-2ubuntu3.24.04.2) ...
Setting up tree (2.1.1-2ubuntu3.24.04.2) ...
Processing triggers for man-db (2.12.0-4build2) ...
$ dpkg -L tree | head -5
/.
/usr
/usr/bin
/usr/bin/tree
/usr/share

Remove, purge, and clean up what is left

remove deletes the program but keeps its configuration files, in case you install it again. Install cron, which has a configuration file, then remove it:

$ sudo apt install -y cron 2>&1 | tail -2
Running hooks in /etc/ca-certificates/update.d...
done.
$ sudo apt remove -y cron 2>&1 | grep '^Removing'
Removing cron (3.0pl1-184ubuntu2) ...
$ ls /etc/default/cron
/etc/default/cron
$ dpkg -l cron | tail -1
rc  cron           3.0pl1-184ubuntu2 amd64        process scheduling daemon

The status rc means removed, configuration still there. purge removes the configuration too:

$ sudo apt purge -y cron 2>&1 | grep '^Purging'
Purging configuration files for cron (3.0pl1-184ubuntu2) ...
$ ls /etc/default/cron
ls: cannot access '/etc/default/cron': No such file or directory
(exit status 2)
$ dpkg -l cron | tail -1
un  cron           <none>       <none>       (no description available)

Now remove nmap, which is the package that brought in the libraries. They stay behind, because apt does not know whether you want them. Packages that came in only as dependencies of something you have since removed are called orphans, and autoremove deletes them:

$ sudo apt remove -y nmap 2>&1 | grep '^Removing'
Removing nmap (7.94+git20230807.3be01efb1+dfsg-3build2) ...
$ sudo apt autoremove -s 2>/dev/null | grep -E '^Remv (nmap-common|liblua|liblinear|libssh2)'
Remv liblinear4 [2.3.0+dfsg-5build1]
Remv liblua5.4-0 [5.4.6-3build2]
Remv libssh2-1t64 [1.11.0-4.1ubuntu0.24.04.4]
Remv nmap-common [7.94+git20230807.3be01efb1+dfsg-3build2]
$ sudo apt autoremove -y 2>&1 | grep -E '^[0-9]+ upgraded'
0 upgraded, 0 newly installed, 54 to remove and 5 not upgraded.

The simulation lists nmap's libraries. The real run says more packages go than that: this image has other automatic packages that nothing needs. On your own server, read the full list of autoremove -s before you agree to it; that is what -s is for.

apt tracks which packages you asked for (manual) and which came along (automatic); autoremove removes the automatic ones that nothing needs. apt-mark changes the label, which is how you keep a package that autoremove wants to take:

$ apt-mark showauto | head -3
adduser
bsdextrautils
ca-certificates
$ sudo apt-mark manual adduser
adduser set to manually installed.
$ apt-mark showmanual | grep adduser
adduser

Remove the other test packages, with their configuration:

$ sudo apt purge -y tree apt-file 2>&1 | tail -2
(Reading database ... 7613 files and directories currently installed.)
Purging configuration files for apt-file (3.3) ...
$ dpkg -l tree 2>&1 | tail -1
dpkg-query: no packages found matching tree

Upgrade the system

The package lists were downloaded a moment ago, and the image is older than they are, so there is something to do. First ask what is waiting:

$ apt list --upgradable 2>/dev/null | head -8
Listing...
libaudit-common/noble-updates 1:3.1.2-2.1ubuntu0.1 all [upgradable from: 1:3.1.2-2.1build1.1]
libaudit1/noble-updates 1:3.1.2-2.1ubuntu0.1 amd64 [upgradable from: 1:3.1.2-2.1build1.1]
liblzma5/noble-updates,noble-security 5.6.1+really5.4.5-1ubuntu0.4 amd64 [upgradable from: 5.6.1+really5.4.5-1ubuntu0.3]
libpcre2-8-0/noble-security 10.42-4ubuntu2.2 amd64 [upgradable from: 10.42-4ubuntu2.1]
zlib1g/noble-updates,noble-security 1:1.3.dfsg-3.1ubuntu2.3 amd64 [upgradable from: 1:1.3.dfsg-3.1ubuntu2.2]
$ apt list --upgradable 2>/dev/null | tail -n +2 | wc -l
5

Upgrade one package, to see what an upgrade looks like. --only-upgrade upgrades the package if it is installed, and does not install it if it is not. The line to read is Unpacking: the new version, then "over" the old one:

$ sudo apt install -y --only-upgrade libpcre2-8-0 2>&1 | grep -E '^(Unpacking|Setting up)'
Unpacking libpcre2-8-0:amd64 (10.42-4ubuntu2.2) over (10.42-4ubuntu2.1) ...
Setting up libpcre2-8-0:amd64 (10.42-4ubuntu2.2) ...

The -s plan shows where each update comes from. The suite is the part after the slash: noble-security carries the security fixes, noble-updates the other fixes:

$ apt -s upgrade 2>/dev/null | grep '^Inst' | head -4
Inst libaudit-common [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [all])
Inst libaudit1 [1:3.1.2-2.1build1.1] (1:3.1.2-2.1ubuntu0.1 Ubuntu:24.04/noble-updates [amd64])
Inst liblzma5 [5.6.1+really5.4.5-1ubuntu0.3] (5.6.1+really5.4.5-1ubuntu0.4 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
Inst zlib1g [1:1.3.dfsg-3.1ubuntu2.2] (1:1.3.dfsg-3.1ubuntu2.3 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
$ apt -s upgrade 2>/dev/null | grep -c noble-security
4

Upgrade everything. upgrade never removes a package and never installs a new one, so a package that needs a new dependency is "kept back"; full-upgrade (the old dist-upgrade) allows that. Run upgrade first:

$ sudo apt upgrade -y 2>&1 | tail -6
(Reading database ... 7613 files and directories currently installed.)
Preparing to unpack .../zlib1g_1%3a1.3.dfsg-3.1ubuntu2.3_amd64.deb ...
Unpacking zlib1g:amd64 (1:1.3.dfsg-3.1ubuntu2.3) over (1:1.3.dfsg-3.1ubuntu2.2) ...
Setting up zlib1g:amd64 (1:1.3.dfsg-3.1ubuntu2.3) ...
Processing triggers for man-db (2.12.0-4build2) ...
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
$ apt list --upgradable 2>/dev/null | tail -n +2 | wc -l
0

A kernel update only runs after a reboot. On a server, the package unattended-upgrades installs the security updates by itself and is the usual answer to "who patches this at 3 a.m.":

$ sudo apt install -y unattended-upgrades 2>&1 | tail -2
Processing triggers for man-db (2.12.0-4build2) ...
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
$ cat /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null | head -3
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

History: see what changed, and undo it

apt writes every transaction to /var/log/apt/history.log. Install a package, then read its record (-B1 and -A3 print the lines around the match):

$ sudo apt install -y tree 2>&1 | tail -2
Setting up tree (2.1.1-2ubuntu3.24.04.2) ...
Processing triggers for man-db (2.12.0-4build2) ...
$ grep -B1 -A3 'apt install -y tree' /var/log/apt/history.log
Start-Date: 2026-11-03  23:01:54
Commandline: apt install -y tree
Requested-By: tc (1001)
Install: tree:amd64 (2.1.1-2ubuntu3.24.04.2)
End-Date: 2026-11-03  23:01:54

Each block has the start date, the command line, who asked, and what was installed, upgraded or removed. apt 2.8 has no undo command; the undo is the opposite of the Install line. Remove what was installed, and read the new block:

$ sudo apt remove -y tree 2>&1 | tail -3
(Reading database ... 12037 files and directories currently installed.)
Removing tree (2.1.1-2ubuntu3.24.04.2) ...
Processing triggers for man-db (2.12.0-4build2) ...
$ grep -B1 -A3 'apt remove -y tree' /var/log/apt/history.log
Start-Date: 2026-11-03  23:01:56
Commandline: apt remove -y tree
Requested-By: tc (1001)
Remove: tree:amd64 (2.1.1-2ubuntu3.24.04.2)
End-Date: 2026-11-03  23:01:56

The record of what dpkg itself did is /var/log/dpkg.log, one line per step, with the package and the version:

$ grep -E ' (install|remove) tree:' /var/log/dpkg.log
2026-11-03 23:00:27 install tree:amd64 <none> 2.1.1-2ubuntu3.24.04.2
2026-11-03 23:01:15 remove tree:amd64 2.1.1-2ubuntu3.24.04.2 <none>
2026-11-03 23:01:54 install tree:amd64 <none> 2.1.1-2ubuntu3.24.04.2
2026-11-03 23:01:56 remove tree:amd64 2.1.1-2ubuntu3.24.04.2 <none>

Older history rolls into history.log.1.gz and so on; zgrep reads them. When the install was a big one, the Install line lists every package that came with it, which is what you undo. This is the answer to "I installed something yesterday and now something is broken": the history says exactly what changed and when.

Repositories: where the packages come from

Everything apt installs comes from a repository: a web folder with packages and a signed index. Ubuntu 24.04 lists them in /etc/apt/sources.list.d/ubuntu.sources, in the deb822 format, one block per source:

$ grep -v '^#' /etc/apt/sources.list.d/ubuntu.sources | grep .
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

URIs is the address, Suites are noble (the release), noble-updates and noble-backports, Components are main (supported free software), universe (community), restricted and multiverse. Signed-By names the key that must have signed the lists. The security updates are their own block, on security.ubuntu.com.

Add a component the easy way. add-apt-repository, from the package software-properties-common, edits the file for you. universe is already on in this image, and the command says so; on a system where it is off, it switches it on:

$ sudo apt install -y software-properties-common 2>&1 | tail -2
Setting up software-properties-common (0.99.49.4) ...
Processing triggers for dbus (1.14.10-4ubuntu4.1) ...
$ sudo add-apt-repository -y universe 2>&1 | tail -2
Reading package lists...
Adding component(s) 'universe' to all repositories.

Third-party repositories are the same idea with their own key. Download the key to /etc/apt/keyrings, and refer to it with Signed-By in the source file. The signed-repository guide in this series does it in full, and why apt-key and trusted=yes are never the answer. See which sources apt reads, as a one-line summary:

$ apt-cache policy | grep -E '^ *[0-9]+ http' | sort -u | head -6
 100 http://archive.ubuntu.com/ubuntu noble-backports/main amd64 Packages
 100 http://archive.ubuntu.com/ubuntu noble-backports/multiverse amd64 Packages
 100 http://archive.ubuntu.com/ubuntu noble-backports/universe amd64 Packages
 500 http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
 500 http://archive.ubuntu.com/ubuntu noble-updates/multiverse amd64 Packages
 500 http://archive.ubuntu.com/ubuntu noble-updates/restricted amd64 Packages

Hold a package, go back a version

Sometimes an upgrade is the problem. To go back a version, ask for it by name. madison lists the versions the repositories offer, and = picks one; --allow-downgrades is needed when it is older than the installed one:

$ apt-cache madison tzdata
    tzdata | 2026e-0ubuntu0.24.04.1 | http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
    tzdata | 2026e-0ubuntu0.24.04.1 | http://security.ubuntu.com/ubuntu noble-security/main amd64 Packages
    tzdata | 2024a-2ubuntu1 | http://archive.ubuntu.com/ubuntu noble/main amd64 Packages
$ sudo apt install -y --allow-downgrades tzdata=2024a-2ubuntu1 2>&1 | grep -E '^(Unpacking|Setting up)'
Unpacking tzdata (2024a-2ubuntu1) over (2026e-0ubuntu0.24.04.1) ...
Setting up tzdata (2024a-2ubuntu1) ...
$ dpkg -l tzdata | tail -1
ii  tzdata         2024a-2ubuntu1 all          time zone and daylight-saving time data

Unlike dnf, the apt repositories keep only the newest build of a package plus the one from the original release, so the choice is small. Now the machine runs the older tzdata, and the next upgrade would replace it again:

$ apt list --upgradable 2>/dev/null | grep tzdata
tzdata/noble-updates,noble-security 2026e-0ubuntu0.24.04.1 all [upgradable from: 2024a-2ubuntu1]

To keep it where it is, hold it. A held package is skipped by every upgrade, and apt says so:

$ sudo apt-mark hold tzdata
tzdata set on hold.
$ apt-mark showhold
tzdata
$ sudo apt upgrade -y 2>&1 | grep -E 'kept back|tzdata|upgraded,'
The following packages have been kept back:
  tzdata
0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.

A hold is a decision you have to remember. apt-mark showhold lists them, and unhold releases one; then the upgrade goes through:

$ sudo apt-mark unhold tzdata
Canceled hold on tzdata.
$ sudo apt install -y --only-upgrade tzdata 2>&1 | grep -E '^(Unpacking|Setting up)'
Unpacking tzdata (2026e-0ubuntu0.24.04.1) over (2024a-2ubuntu1) ...
Setting up tzdata (2026e-0ubuntu0.24.04.1) ...
$ dpkg -l tzdata | tail -1
ii  tzdata         2026e-0ubuntu0.24.04.1 all          time zone and daylight-saving time data

For more control (prefer a version, or one repository), a file in /etc/apt/preferences.d sets a priority; man apt_preferences has the details.

The cache and the configuration

apt keeps the downloaded packages in /var/cache/apt/archives, and the lists in /var/lib/apt/lists. See how much space they take:

$ sudo du -sh /var/cache/apt/archives /var/lib/apt/lists
12K	/var/cache/apt/archives
60M	/var/lib/apt/lists

The cache is empty: this container deletes packages after installing them, a setting in /etc/apt/apt.conf.d/docker-clean. On a normal server they stay until you clean. --download-only fetches without installing, so the file stays here; then apt clean empties the package cache (apt autoclean only removes packages that can no longer be downloaded):

$ sudo apt install -y --download-only sl 2>&1 | tail -2
Fetched 139 kB in 1s (171 kB/s)
Download complete and in download only mode
$ ls /var/cache/apt/archives
libgpm2_1.20.7-11_amd64.deb
libncurses6_6.4+20240113-1ubuntu2.2_amd64.deb
lock
partial
sl_5.02-1_amd64.deb
$ sudo apt clean 2>/dev/null
$ ls /var/cache/apt/archives
lock
partial

The settings live in /etc/apt/apt.conf.d, one file per topic, and apt-config shows what is in effect:

$ apt-config dump | grep -E 'Acquire::Retries|APT::Install-Recommends'
APT::Install-Recommends "1";

APT::Install-Recommends is on by default: a package's recommended packages come with it. For a small server you can switch that off for one command, without touching a file:

$ sudo apt install -s git 2>/dev/null | grep -c '^Inst'
18
$ sudo apt install -s --no-install-recommends git 2>/dev/null | grep -c '^Inst'
3

Any setting can be tried for one run with -o. Put it in a file in apt.conf.d when you want it to stay:

$ apt-config -o Acquire::Retries=5 dump | grep Acquire::Retries
Acquire::Retries "5";
CommandLine::AsString "apt-config -o Acquire::Retries=5 dump";

That was for one run. For good:

$ echo 'Acquire::Retries "3";' | sudo tee /etc/apt/apt.conf.d/80retries
Acquire::Retries "3";
$ apt-config dump | grep Acquire::Retries
Acquire::Retries "3";

apt next to apt-get, dpkg and dnf

Which command for which job:

apt install pkg           install (a person at the keyboard)
apt-get install -y pkg    the same, for scripts
apt-cache policy pkg      versions and sources
dpkg -i file.deb          unpack one file, no dependencies fetched
dpkg -l / -L / -S         list packages / the files of one / the owner of a file
dpkg -V pkg               check an installed package against its checksums

Check an installed package against the checksums it shipped with. Install tree again for this:

$ sudo apt install -y tree 2>&1 | tail -1
Processing triggers for man-db (2.12.0-4build2) ...
$ dpkg -V tree
missing     /usr/share/doc/tree/README.gz
missing     /usr/share/doc/tree/TODO
missing     /usr/share/man/man1/tree.1.gz

The three "missing" lines are the man page and the documents, which this container image leaves out on purpose; a normal server has them. Now change the program, and check again. A 5 in the third column means the checksum differs:

$ sudo sh -c 'echo x >> /usr/bin/tree'
$ dpkg -V tree | grep /usr/bin
??5??????   /usr/bin/tree

--reinstall puts the original back. The exit status of dpkg -V is not a signal to rely on, so read the output:

$ sudo apt install -y --reinstall tree 2>&1 | tail -1
Processing triggers for man-db (2.12.0-4build2) ...
$ dpkg -V tree | grep /usr/bin
(exit status 1)
$ sudo apt purge -y tree 2>&1 | tail -1
Processing triggers for man-db (2.12.0-4build2) ...

For readers who know dnf, this is the same list the other way round:

dnf makecache        apt update
dnf upgrade          apt upgrade
dnf install pkg      apt install pkg
dnf remove pkg       apt remove pkg (apt purge: and the configuration)
dnf autoremove       apt autoremove
dnf search word      apt search word
dnf info pkg         apt show pkg
dnf provides file    apt-file search file
dnf history          /var/log/apt/history.log
dnf versionlock add  apt-mark hold

Two differences are worth knowing. apt upgrade does not refresh the lists, so apt update comes first; dnf does that by itself. And apt remove keeps the configuration files where dnf remove does not have that idea; apt purge is the one that is thorough.

When something goes wrong

The common messages, with a real example of each. A name apt does not know, a typo, or lists that were never downloaded:

$ sudo apt install -y notarealpackage

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package notarealpackage
(exit status 100)

The message is E: Unable to locate package. Search for part of the name with apt search, or find the file you need with apt-file search. A repository that cannot be reached shows as an error from apt update. Make one on purpose, with a source file that points nowhere:

$ echo 'deb http://x.invalid/ubuntu noble main' | sudo tee /etc/apt/sources.list.d/broken.list
deb http://x.invalid/ubuntu noble main
$ sudo apt update 2>&1 | tail -4
Reading state information...
All packages are up to date.
W: Failed to fetch http://x.invalid/ubuntu/dists/noble/InRelease  Could not resolve 'x.invalid'
W: Some index files failed to download. They have been ignored, or old ones used instead.

"Temporary failure resolving" or "Could not resolve" names the repository that failed. The fix is to correct the address, or remove the file; apt update goes on with the others and reports the one it could not read at the end. Remove the file:

$ sudo rm /etc/apt/sources.list.d/broken.list
$ sudo apt update 2>&1 | tail -2
Reading state information...
All packages are up to date.

Two apt commands at once give a lock error (Could not get lock /var/lib/dpkg/lock-frontend); wait for the other one. A signature error (NO_PUBKEY, or "The following signatures couldn't be verified") means the key is missing or the index was changed: do not get past it with trusted=yes. The signed-repository guide shows what causes it.

If an install was interrupted, or a package is half installed, there are two commands that repair the state. First finish the configuration of anything that is unpacked, then let apt fix broken dependencies:

$ sudo dpkg --configure -a
$ sudo apt --fix-broken install -y 2>&1 | tail -2
Use 'sudo apt autoremove' to remove them.
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

Both say nothing to do on a healthy system. "404 Not Found" while installing means the lists are stale (the package was replaced on the mirror); apt update and try again.