Skip to main content

Linux command: apt

19 August 2026

Every Debian or Ubuntu server you have ever administered was built, updated and repaired with one command. apt installs your web server, patches the security hole you read about this morning, and quietly removes the library nothing needs any more. Most people learn three of its subcommands and stop, which is a shame, because the parts they skip are exactly the parts that explain why a server sometimes refuses to upgrade a package, why the disk filled up, and why the same command produced different results on two machines that should be identical.

1. The Basics

The job of apt is to get software onto your system and keep it current, without you having to work out what else that software needs. You ask for one package; apt works out the twenty others it depends on, downloads them all, and installs them in the right order.

$ sudo apt update
$ sudo apt install nginx

Those two lines are the whole daily workflow, and the order matters. They do completely different things, and mixing them up is the most common beginner mistake with apt:

  • apt update refreshes the catalogue. It downloads the current list of available packages and versions from your configured repositories. It installs nothing.
  • apt install and apt upgrade act on that catalogue. They download and install actual software.

Think of update as reading the latest price list and install as placing the order. Order from last year's list and you will be told the item does not exist.

Underneath, apt is not the program that unpacks files onto your disk. That is dpkg, the Debian package tool, and it has been there since long before apt. dpkg installs one .deb file and complains if something is missing. apt sits on top, works out which files are needed, fetches them over the network, and hands them to dpkg in a workable order.

you  ->  apt        decides what is needed, downloads it
         |
         v
         dpkg       unpacks and configures individual .deb files
         |
         v
         your filesystem

The command that turns "install a web server" into a solved dependency graph. Behind two subcommands sit repositories, cryptographic signatures, pinning, phased rollouts, and a package database with 3,000 entries on an ordinary desktop.

This article starts with installing and removing software and builds up to repository configuration, signing keys, held packages, and the reasons apt sometimes declines to do what you asked.

The right mental model: apt is a dependency solver with a downloader attached. dpkg does the installing. Almost every confusing apt message is apt telling you that the graph it was asked to solve has no acceptable solution.

Back to top

2. Where the Name Comes From

APT stands for Advanced Package Tool.

apt  =  Advanced Package Tool

"Advanced" was a claim about its predecessor rather than marketing. Before APT, installing software on Debian meant dpkg (Debian PacKaGe) for single files and a menu-driven front end called dselect that people remember without affection. The advance was automatic dependency resolution over a network.

The confusing part is not the name but the family. Several commands share it, and they are genuinely different programs:

CommandWhat it isWhen to use it
dpkg The low-level tool that installs one .deb and tracks what is installed Installing a downloaded .deb, or querying which package owns a file
apt-get The original APT command line, with a deliberately stable interface Scripts, Dockerfiles, automation
apt-cache The query half of the original interface Searching and inspecting package metadata in scripts
apt A friendlier front end combining the most-used parts of both, added in 2014 Typing at a terminal, interactively
aptitude A separate, more powerful front end with a full-screen interface Untangling hard dependency conflicts; not installed by default

They are not aliases of each other. apt and apt-get are separate binaries with different sizes and different behaviour, and section 7.1 shows a difference you can see for yourself.

The short version of when to use which: type apt, script apt-get. The reason for that rule is one of the more interesting things about the tool, and section 7.1 explains it.

Back to top

3. A Short History

APT is younger than Debian and younger than dpkg. Debian's release manager, Brian White, commissioned the work in 1997 because installing software was the distribution's weakest point. The first working code was a test program called dpkg-get, and much of the early development happened on IRC, which is why the record is patchy.

The Debian changelog still carries the very first entry, and it is charmingly small:

apt (0.0.1) unstable; urgency=low

  * Initial Release.

 -- Scott K. Ellis <This email address is being protected from spambots. You need JavaScript enabled to view it.>  Tue, 31 Mar 1998 12:49:28 -0500

The next entry, three days later, notes that 0.0.1 "was released only on IRC". APT first shipped to users in Debian 2.1, released on 9 March 1999, and it changed how the distribution felt to use.

For the next sixteen years the command you typed was apt-get. The plain apt command arrived only in April 2014, and the release that introduced it is named after the anniversary:

apt (1.0) unstable; urgency=low

  The "Happy birthday and 10000b years in the making" release
  ...
  [ Michael Vogt ]
  * install apt binary
  * add apt.8.xml manpage

 -- Michael Vogt <This email address is being protected from spambots. You need JavaScript enabled to view it.>  Tue, 01 Apr 2014 15:48:46 +0200

10000b is binary for 16. The first release was 31 March 1998; version 1.0 landed on 1 April 2014, sixteen years and one day later. That is the entire reason a second command exists: after sixteen years of apt-get and apt-cache, the developers added one command that combined the parts people actually used, with progress bars and colour.

EraMilestone
1993 onward dpkg handles individual packages on Debian; dependencies are the user's problem
1997 Brian White commissions a better front end; the prototype is called dpkg-get
31 March 1998 apt 0.0.1, released "only on IRC" by Scott K. Ellis
9 March 1999 APT ships to users in Debian 2.1
1 April 2014 apt 1.0 installs the apt binary, "10000b years in the making"
2020 apt 2.0 adds search patterns (apt-patterns), replacing loose regex matching
2021 apt 2.1.16 adds phased updates and automatic removal of unused kernels on full-upgrade
2021 apt 2.3.12 refuses to remove essential packages at all, after a famous accident
2024 Ubuntu 24.04 moves its repository configuration to the deb822 .sources format

That 2021 entry has a story behind it, and section 7.3 tells it.

Back to top

4. Simple Use Cases

4.1 Refreshing the Catalogue

Almost every apt session starts here. apt update contacts each configured repository and downloads the current package lists.

$ sudo apt update
Get:1 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
Get:2 http://archive.ubuntu.com/ubuntu noble InRelease [256 kB]
Get:3 http://security.ubuntu.com/ubuntu noble-security/universe amd64 Packages [1522 kB]
...
Reading package lists...
Building dependency tree...
Reading state information...
3 packages can be upgraded. Run 'apt list --upgradable' to see them.

Two words in that output are worth knowing. Get: means apt downloaded a list because it changed. Hit:, which you will see on a second run, means the list was already current and nothing was transferred.

The last line is a prompt to look closer, and the command it suggests is the useful one:

$ apt list --upgradable
Listing...
libssl3t64/noble-updates,noble-security 3.0.13-0ubuntu3.12 amd64 [upgradable from: 3.0.13-0ubuntu3.11]
libsystemd0/noble-updates,noble-security 255.4-1ubuntu8.17 amd64 [upgradable from: 255.4-1ubuntu8.16]
libudev1/noble-updates,noble-security 255.4-1ubuntu8.17 amd64 [upgradable from: 255.4-1ubuntu8.16]

Each line gives the package, which repository the new version comes from, the new version, and the version you have. Seeing noble-security in that list tells you the upgrade is a security fix, which is usually enough to decide it should not wait.

4.2 Why "Unable to Locate Package" Almost Never Means What It Says

This is the error that sends new users looking for a different package name, and it is nearly always the wrong conclusion. Here is a completely fresh Ubuntu system with no package lists yet:

$ ls /var/lib/apt/lists/ | wc -l
0

$ sudo apt-get install -y tree
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package tree

The tree package exists and always has. apt simply has no catalogue to look in, so as far as it knows, no packages exist at all. One apt update fixes it.

This is why every Dockerfile you have ever read joins the two commands together. In a container the package lists are deliberately empty to keep the image small, so the update is not optional:

RUN apt-get update && apt-get install -y --no-install-recommends nginx \
    && rm -rf /var/lib/apt/lists/*

There is a second reason for the && that catches people out later. Docker caches each RUN as a layer. Split across two instructions, the update layer can be reused from cache weeks later while the install layer runs fresh, so you install today's packages against a stale catalogue and get errors about versions that no longer exist. Joined into one instruction, they are cached and invalidated together. The final rm -rf deletes the catalogue again, because keeping several hundred megabytes of package lists in a shipped image serves nobody.

4.3 Installing Software

Give apt a package name. Before it does anything it prints a summary and waits, and that summary is worth reading rather than reflexively pressing Enter:

$ sudo apt install tree
Reading package lists...
Building dependency tree...
Reading state information...
The following NEW packages will be installed:
  tree
0 upgraded, 1 newly installed, 0 to remove and 3 not upgraded.
Need to get 47.4 kB of archives.
After this operation, 111 kB of additional disk space will be used.

Read that summary line as four separate counts. Upgraded and newly installed are what you asked for. To remove is the one to check, because a package you did not mention appearing there means apt has decided something must go to satisfy your request. Not upgraded counts packages with newer versions available that this command is leaving alone.

Add -y to answer the confirmation automatically, which is what scripts do. Do not put it in your fingers for interactive use, because the confirmation prompt is the last chance to notice an unexpected removal.

4.4 Finding Software

apt search looks through package names and descriptions, and apt show prints the detail:

$ apt search '^nginx'          # anchored, or you get hundreds of matches
$ apt show tree
Package: tree
Version: 2.1.1-2ubuntu3.24.04.2
Priority: optional
Section: universe/utils
Origin: Ubuntu
Installed-Size: 111 kB
Depends: libc6 (>= 2.38)
Homepage: http://oldmanprogrammer.net/source.php?dir=projects/tree
Download-Size: 47.4 kB

Two fields there answer real questions. Section: universe/utils tells you the package comes from Ubuntu's community-maintained universe repository rather than the officially supported main, which matters for security-update guarantees on a server. Installed-Size is the disk cost after installation, as opposed to Download-Size.

4.5 Removing Software

There are two removal commands and the difference catches almost everybody. Watch the package state before and after, using dpkg's own listing:

$ dpkg -l wget
ii  wget  1.21.4-1ubuntu4.4  amd64  retrieves files from the web

$ sudo apt remove wget
$ dpkg -l wget
rc  wget  1.21.4-1ubuntu4.4  amd64  retrieves files from the web

The state changed from ii to rc. Those two letters are not arbitrary, and dpkg prints its own legend if you let it:

$ dpkg -l bash
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  bash  5.2.21-2ubuntu4  amd64        GNU Bourne Again SHell

The first letter is what you want and the second is what is. So ii means "install, installed", and rc means "remove, config-files": you asked for removal, and what remains is configuration. The same legend explains iU (unpacked but not configured) and iF (half-configured), which are the states an interrupted install leaves behind. An uppercase letter always means something needs attention.

In the rc case, the leftover really is on disk:

$ ls /etc/wgetrc
/etc/wgetrc

That is deliberate and usually helpful, because reinstalling later restores your settings. It is unhelpful when you removed the package precisely to start again from a clean configuration. purge takes the configuration too:

$ sudo apt purge wget
Purging configuration files for wget (1.21.4-1ubuntu4.4) ...

$ dpkg -l wget
dpkg-query: no packages found matching wget

A useful habit on a server: use remove when you might want the software back, and purge when you are removing something you misconfigured.

Back to top

5. Moderate Use Cases

5.1 upgrade Versus full-upgrade

Both install newer versions of what you already have. The difference is what they are willing to do to achieve it.

CommandWill install new packages?Will remove packages?
apt upgrade Yes, if needed by an upgrade No, never
apt full-upgrade Yes Yes, if that is the only way
apt-get upgrade No No
apt-get dist-upgrade Yes Yes

full-upgrade and dist-upgrade are the same operation under two names. The safety property of plain upgrade is that it never removes anything, so if the only route to a newer package is deleting another one, it declines and leaves that package alone. When it does that, it tells you clearly.

5.2 "The Following Packages Have Been Kept Back"

This message worries people, and it is usually apt behaving correctly. You can produce it deliberately by putting a package on hold, which tells apt never to change it:

$ sudo apt-mark hold libssl3t64
libssl3t64 set on hold.

$ sudo apt upgrade
The following packages have been kept back:
  libssl3t64
The following packages will be upgraded:
  libsystemd0 libudev1
2 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.

The hold survives reboots and every future upgrade until you release it:

$ apt-mark showhold          # what is currently held
libssl3t64

$ sudo apt-mark unhold libssl3t64
Canceled hold on libssl3t64.

Holds are genuinely useful on a server where one component must stay on a known version, such as a database whose upgrade needs a maintenance window. They are also easy to forget, so apt-mark showhold belongs in your checklist when a machine mysteriously stops receiving updates.

A hold is not the only cause of a kept-back package, though. On Ubuntu the most common cause is something else entirely, and section 7.2 covers it.

5.3 Cleaning Up What Nothing Needs

When apt installs a package it records whether you asked for it or whether it came along as a dependency. Remove the thing you asked for, and the dependencies stay behind, marked as automatically installed and no longer required.

The effect is larger than people expect. Installing git on a minimal system pulls in 46 packages; removing git afterwards leaves 42 of them orphaned:

$ sudo apt autoremove
0 upgraded, 0 newly installed, 42 to remove and 2 not upgraded.
Remv git-man
Remv krb5-locales
Remv less
Remv libcurl3t64-gnutls
...

Read that list before agreeing. autoremove is the apt command most likely to surprise you, because "nothing depends on this" is not the same as "you do not want this". If you installed something manually and apt thinks it arrived as a dependency, autoremove will offer to delete it. You can correct apt's opinion:

$ sudo apt-mark manual less      # I want this even if nothing depends on it
$ apt-mark showauto | wc -l      # how many packages are marked automatic

Two related commands free disk space without removing software. apt clean empties the cache of downloaded .deb files, and apt autoclean removes only the ones that can no longer be downloaded:

$ du -sh /var/cache/apt/archives/     # downloaded .deb files
3.5M    /var/cache/apt/archives/
$ du -sh /var/lib/apt/lists/          # the package catalogue itself
345M    /var/lib/apt/lists/

That second number surprises people on small servers. The catalogue is large because it describes every package in every configured repository, and it grows with each extra repository you add.

5.4 What Did I Change Last Week?

apt keeps a log of every transaction, which is the first place to look when a server starts behaving differently and nobody admits to touching it:

$ less /var/log/apt/history.log

Start-Date: 2026-08-11  09:48:42
Commandline: /usr/bin/unattended-upgrade
Upgrade: libsystemd0:amd64 (255.4-1ubuntu8.16, 255.4-1ubuntu8.17), ...
End-Date: 2026-08-11  09:48:52

Each entry records the date, the exact command line, and every package version before and after. The Commandline field is the valuable one: it tells you whether a human ran the change or whether automatic updates did it. Older logs are rotated and compressed alongside it, so zless /var/log/apt/history.log.1.gz reaches further back.

The :amd64 suffix on those package names is multiarch, Debian's support for installing packages for more than one architecture side by side. It matters when you need 32-bit libraries on a 64-bit machine, or when a dependency can only be satisfied by a specific architecture:

$ dpkg --print-architecture              # the machine's native architecture
amd64
$ dpkg --print-foreign-architectures      # extra architectures enabled, if any

Most servers have nothing in that second list, and then the suffix is just noise in the output. Once a foreign architecture is enabled, package name and version are no longer enough to identify a package and you will start seeing package:architecture in error messages.

Back to top

6. Advanced Use Cases

6.1 Where Packages Come From

apt only knows about repositories you have configured. Historically that meant one file, /etc/apt/sources.list, with one repository per line. On Ubuntu 24.04 that file is now almost empty and says so:

$ cat /etc/apt/sources.list
# Ubuntu sources have moved to /etc/apt/sources.list.d/ubuntu.sources

The replacement is the deb822 format: one block of named fields per repository, which is far easier to read and to manage with configuration tooling.

$ cat /etc/apt/sources.list.d/ubuntu.sources
Types: deb
URIs: http://nl.archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Both formats still work, and a real machine usually has a mixture, because third-party installers write whichever they were built for. The old one-line form packs the same information into a single line:

$ cat /etc/apt/sources.list.d/docker.list
deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable

The fields are worth knowing because they explain a lot of behaviour. Suites names the release and its update channels, so noble-security is where security fixes arrive. Components selects the sections: on Ubuntu, main is officially supported, universe is community-maintained, restricted and multiverse hold drivers and software with licence restrictions. If a package "does not exist", the usual cause is that its component is not enabled.

6.2 Signing Keys, and the apt-key Trap

apt does not trust a repository because you typed its address. Every repository is cryptographically signed, and apt verifies the signature against a key you have installed. This is the security boundary of the whole system: an attacker who can modify traffic to your mirror still cannot make you install their package.

The way you install those keys changed, and a great deal of published advice is now actively harmful. The old way was apt-key, which adds a key trusted for every repository on the system:

$ apt-key list
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).

The problem is scope. A key added with apt-key add can sign packages claiming to be anything, including a replacement for your kernel or your SSH server. A third-party repository for a text editor should not be able to do that.

The modern pattern stores the key in its own file and binds it to one repository with Signed-By:

$ curl -fsSL https://example.com/key.gpg | sudo tee /etc/apt/keyrings/example.asc > /dev/null
$ echo "Types: deb
URIs: https://example.com/apt/
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/example.asc" | sudo tee /etc/apt/sources.list.d/example.sources

Now that key is trusted for that repository and nothing else. If you find instructions telling you to pipe a key into apt-key add, they predate 2022 and should be translated into the form above.

6.3 What Is Actually Inside a Package

Signatures matter more than they first appear, and the reason becomes obvious once you look inside a .deb. You can download one without installing it:

$ apt-get download tree
$ ls
tree_2.1.1-2ubuntu3.24.04.2_amd64.deb

A .deb is an archive with two halves: the files to install, and a control archive describing the package. dpkg-deb -I shows the second half:

$ dpkg-deb -I tree_2.1.1-2ubuntu3.24.04.2_amd64.deb
 new Debian package, version 2.0.
 size 47360 bytes: control archive=746 bytes.
     595 bytes,    14 lines      control
     367 bytes,     6 lines      md5sums
 Package: tree
 Version: 2.1.1-2ubuntu3.24.04.2
 Architecture: amd64
 Installed-Size: 108
 Depends: libc6 (>= 2.38)

And dpkg-deb -c lists the files, with the paths they will occupy:

$ dpkg-deb -c tree_2.1.1-2ubuntu3.24.04.2_amd64.deb
drwxr-xr-x root/root         0 ./usr/bin/
-rwxr-xr-x root/root     85400 ./usr/bin/tree

That package is harmless. Now look at one that is not just files. The control archive of ca-certificates contains three executable scripts, marked with an asterisk:

$ dpkg-deb -I ca-certificates_20260601~24.04.1_all.deb
 size 139430 bytes: control archive=16630 bytes.
    9029 bytes,   163 lines   *  config               #!/bin/sh
     880 bytes,    21 lines      control
   13789 bytes,   138 lines      md5sums
    5817 bytes,   187 lines   *  postinst             #!/bin/sh
    1629 bytes,    65 lines   *  postrm               #!/bin/sh

These are maintainer scripts, and there are four possible names: preinst and postinst run before and after the files are unpacked, prerm and postrm before and after removal. They are ordinary shell scripts, and dpkg runs them as root.

Installing a package is not extracting an archive. It is running someone else's shell scripts with full privileges on your machine. That is the entire reason apt refuses to install from an unsigned repository, and the reason a third-party repository is a genuine trust decision rather than a convenience.

Those scripts also explain a line everyone has seen scroll past and nobody reads. Debian packages can register triggers, which defer shared work until the end of a transaction rather than repeating it for every package:

Processing triggers for libc-bin (2.39-0ubuntu8.8) ...
Processing triggers for man-db (2.12.0-4build2) ...

Rebuilding the manual page index once after installing twenty packages is much faster than rebuilding it twenty times. When an install seems to pause at the end having apparently finished, this is usually what it is doing.

6.4 Which Version Will I Actually Get?

When several repositories offer the same package, apt picks by priority. apt policy shows the whole picture, and with a package name it answers the specific question:

$ apt-cache policy bash
bash:
  Installed: 5.2.21-2ubuntu4
  Candidate: 5.2.21-2ubuntu4
  Version table:
 *** 5.2.21-2ubuntu4 500
        500 http://nl.archive.ubuntu.com/ubuntu noble/main amd64 Packages
        100 /var/lib/dpkg/status

Installed is what you have, Candidate is what an upgrade would give you, and the numbers are priorities. The default is 500 for a normal repository and 100 for the already-installed version, which is why apt prefers a repository version over what is on disk but will not downgrade you for fun.

This command is the fastest way to answer "why is it installing that version?" and "is this package coming from the repository I think it is?", which matters when a third-party repository shadows a distribution package.

Those version strings are structured, not arbitrary, and reading them is a useful skill:

[epoch:]upstream_version[-debian_revision]

1:2.43.0-1ubuntu7.3
|  |       |
|  |       +-- packaging revision: changes to the packaging, not the software
|  +---------- upstream version: what the software's own authors released
+------------- epoch: rarely present, and it overrides everything else

The epoch is the interesting one. Version comparison is normally mechanical, but occasionally upstream changes its numbering in a way that makes the new release sort lower than the old one. An epoch is a manual override that says "trust me, this is newer". Once a package has an epoch it keeps it forever, which is why git on Ubuntu is version 1:2.43.0-1ubuntu7.3 rather than 2.43.0-1ubuntu7.3.

When the default priorities are not what you want, you can change them. Pinning lives in /etc/apt/preferences.d/ and adjusts the priority of a repository or a package, which is how you use one package from a newer suite without dragging the whole system along:

$ cat /etc/apt/preferences.d/99-backports
Package: *
Pin: release a=noble-backports
Pin-Priority: 100

Pinning is powerful and easy to get subtly wrong, because the priority numbers carry specific meanings rather than being a simple ranking. Always check the result with apt-cache policy before trusting it, and prefer the smallest rule that solves your problem.

6.5 Recommends: The Reason Installs Are Bigger Than You Expect

Debian packages express three strengths of relationship. Depends is required. Recommends is "almost everybody wants this too", and apt installs it by default. Suggests is a hint, and apt ignores it.

Those are the three you meet daily, but the packaging format has more, and they are why dependency resolution is a solver rather than a checklist. Pre-Depends forces an ordering, requiring another package to be fully configured first. Conflicts and Breaks say two packages cannot coexist, or that this one breaks a version of another. Replaces lets a package take over files from another, and Provides lets several packages satisfy one generic name, which is how a system can offer a choice of mail server or Java runtime. apt has to find a set of versions that satisfies all of them at once.

$ apt-cache show git | grep -E '^(Recommends|Suggests):'
Recommends: ca-certificates, patch, less, ssh-client
Suggests: gettext-base, git-daemon-run | git-daemon-sysvinit, git-doc, git-email, ...

Recommends are the reason a small package can pull in a long list. The effect is easy to measure with -s (simulate), which shows what would happen without doing it:

$ apt-get install -s git | grep 'newly installed'
1 upgraded, 46 newly installed, 0 to remove and 2 not upgraded.

$ apt-get install -s --no-install-recommends git | grep 'newly installed'
0 upgraded, 23 newly installed, 0 to remove and 3 not upgraded.

Exactly half. On a container image or a small server that difference is worth having, which is why --no-install-recommends appears in almost every Dockerfile. On a desktop, leave recommends on: they are recommended for a reason, and turning them off system-wide produces software that quietly lacks features.

That -s flag deserves a place in your habits generally. It answers "what would this do?" for any apt command, with no risk at all.

6.6 Updating Without Being There

A server that only gets patched when someone remembers is a server that does not get patched. Debian and Ubuntu ship unattended-upgrades for this, and on Ubuntu it is usually installed and enabled already, which you can confirm from the history log:

$ grep -c 'unattended-upgrade' /var/log/apt/history.log
$ cat /etc/apt/apt.conf.d/50unattended-upgrades      # what it is allowed to install
$ sudo unattended-upgrade --dry-run --debug          # what it would do right now

The default policy installs security updates only, which is the right trade for most servers: real protection, minimal risk of a behaviour change breaking something at three in the morning. The setting worth deciding deliberately is Unattended-Upgrade::Automatic-Reboot, because a kernel security fix does nothing until the machine reboots, and an unplanned reboot is its own kind of outage.

Back to top

7. Something Most Users Do Not Know

7.1 apt Knows Whether You Are a Human

Here is a warning that thousands of people have seen in a CI log and never once at their own terminal:

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

It looks like it appears at random. It does not. apt checks whether its output is going to a terminal or into a pipe, and only warns in the second case. You can prove it in two commands:

$ apt list --installed | grep -c 'stable CLI interface'
1                      # output piped: warned

# same command with output going to the terminal: no warning at all

The warning is aimed precisely at the situation where it matters. When you type apt interactively, the wording of its output is your problem for the next three seconds. When something parses that output, the wording is a dependency, and the apt developers explicitly refuse to promise it will not change between releases.

That is the real difference between the two commands, and it is a promise rather than a feature list. apt-get has a stable interface that scripts may rely on; apt is free to change its output whenever a nicer format comes along. Hence: type apt, script apt-get.

The terminal check shows up elsewhere too. Since apt 1.9.11 the lock behaves differently depending on who is watching:

apt(8) now waits for the lock indefinitely if connected to a tty, or
for 120 seconds if not.

So an interactive apt waits patiently while unattended-upgrades finishes, and a scripted one gives up after two minutes rather than hanging a deployment forever.

7.2 Two Identical Servers, Different Updates

You patch two servers built from the same image on the same day. One upgrades a package, the other reports it kept back. Nothing is held, nothing is pinned, and the machines are otherwise identical. This is not a fault; it is phased updates working as designed.

Ubuntu rolls updates out gradually to catch regressions before they reach everyone. A package can carry a field saying what fraction of machines should take it yet:

Phased-Update-Percentage: 10

The clever part is how a machine decides whether it is in that ten percent, and the apt_preferences manual spells it out:

A system's eligibility to a phased update is determined by seeding
random number generator with the package source name, the version
number, and /etc/machine-id, and then calculating an integer in the
range [0, 100].  If this integer is larger than the
Phased-Update-Percentage, the version is pinned to 1, and thus held
back.

Your machine's identity is in the seed. That is why two servers disagree, and it is deliberate: the decision is stable, so a machine does not flip between offering and hiding an update on every run, but it is different per machine and per package, so the rollout spreads evenly.

It also means you can control it. To make a fleet phase together, give them the same APT::Machine-ID. To opt out of waiting, on a staging machine where you want tomorrow's updates today:

$ sudo apt-get -o APT::Get::Always-Include-Phased-Updates=true upgrade

Before you reach for that on production, remember what the delay is buying you: somebody else discovers the regression first.

7.3 The Accident That Changed apt

apt used to protect essential packages with a prompt. If a request would remove something the system cannot function without, it demanded you type a specific sentence:

You are about to do something potentially harmful.
To continue type in the phrase 'Yes, do as I say!'

In November 2021 a very widely watched video showed exactly what that prompt is worth. During a software installation challenge, the host from Linus Tech Tips attempted to install Steam on a Pop!_OS system. Due to an underlying dependency conflict, the package manager proposed a destructive solution: removing the entire desktop environment.

Despite a warning prompt requiring the exact phrase "Yes, do as I say!", the presenter entered the command without realizing the consequences. Upon rebooting, the graphical interface was gone, leaving only a bare text console. This high-profile incident proved that confirmation prompts often act as mere speed bumps, prompting developers to update the software to block the removal of essential system packages entirely.

apt changed as a result, and the release notes on your own disk record it without drama:

apt (2.3.12) unstable; urgency=medium

  The solver will no longer try to remove Essential or Protected packages,
  any dependency problem that would need such a solution will have to be
  resolved manually.

  The "Yes, do as I say" prompt for removing essential packages has been
  replaced by an error message.

  Thank you to Linus Tech Tips and System76 for bringing this issue
  to our attention.

The design lesson is worth more than the anecdote. A confirmation prompt that a determined user can get past is not a safety mechanism, it is a speed bump with paperwork. The fix was to make the dangerous operation impossible by default and require a deliberate command-line option instead. You can read that entry yourself with zless /usr/share/doc/apt/NEWS.Debian.gz, which is worth doing on any system: it is a short list of exactly the behaviour changes most likely to surprise you.

7.4 Knowing Where apt Stops

apt manages software your distribution packages. A lot of what runs on a modern server is not that.

NeedUseWhy
Install a downloaded .deb apt install ./file.deb Note: the ./ makes apt resolve the dependencies, which plain dpkg -i will not
Find which package owns a file dpkg -S Answers "what installed /usr/bin/x?" instantly
Newer software than your release ships backports, a vendor repository, or a container A stable release deliberately freezes versions; do not fight it in place
Language libraries pip, npm, composer Per-project versions that apt cannot express; keep them out of system directories
Untangle a hopeless conflict aptitude Offers alternative solutions interactively when apt just refuses
The same setup on many servers Ansible, or a container image Records what should be installed instead of relying on what someone typed once

I installed .deb packages for years using sudo dpkg -i package.deb While writing this article, I learned about sudo apt install ./package.deb. It does the same local package installation, but with one important advantage: apt can fetch and install missing dependencies automatically.

Back to top

8. Best Practices

  • Always update before install or upgrade. Acting on a stale catalogue produces both "package not found" and downloads of versions that no longer exist.
  • Read the summary line before pressing Enter. Specifically the "to remove" count. That number is where accidents live.
  • Type apt, script apt-get. Only one of them promises its output will look the same next year.
  • Simulate anything you are unsure about. -s (or --simulate, or --dry-run) works on both apt and apt-get, prints the full plan, and changes nothing.
  • Enable unattended security upgrades on every server. Then decide the reboot policy deliberately rather than leaving it at the default.
  • Bind third-party keys to their own repository with Signed-By and a file in /etc/apt/keyrings/. Never apt-key add.
  • Check apt-mark showhold when a machine stops updating. A hold set months ago during an incident is a classic cause.
  • Use --no-install-recommends for containers and minimal servers, and leave recommends enabled on desktops.
  • Read autoremove lists before agreeing. "Nothing depends on it" is not the same as "you do not need it".
  • Reach for the documentation. The manual pages are unusually good, and NEWS.Debian.gz tells you what changed in behaviour.
$ man apt                                 # the friendly command
$ man apt-get                             # the scripting interface
$ man sources.list                        # repository configuration, both formats
$ man apt_preferences                     # priorities, pinning, phased updates
$ man apt-patterns                        # the search syntax, since apt 2.0
$ zless /usr/share/doc/apt/NEWS.Debian.gz # behaviour changes worth knowing
Back to top

9. Common Mistakes

9.1 Common Myths

MythReality
"apt update updates my software." It updates the catalogue only. apt upgrade updates software.
"apt is just a shorter alias for apt-get." Separate binaries with different behaviour. Only apt-get promises a stable interface for scripts.
"'Unable to locate package' means it is not packaged." It usually means no apt update, or the component (universe) is not enabled.
"apt remove uninstalls a package completely." Configuration files stay, and dpkg lists it as rc. apt purge removes those too.
"Kept-back packages mean something is broken." Usually a hold, or Ubuntu's phased rollout deciding it is not this machine's turn yet.
"apt-key add is how you trust a repository." Deprecated, and it trusts that key for every repository. Use Signed-By with a file in /etc/apt/keyrings/.

9.2 Traps to Avoid

  • Running autoremove without reading the list. It happily removes a kernel, a driver, or the manually built tool apt believes was a dependency.
  • dpkg -i for a downloaded package. It cannot fetch dependencies and leaves the package half-configured. Use apt install ./file.deb, or apt --fix-broken install to recover.
  • Adding a repository built for another release. A focal repository on a noble machine will install packages linked against the wrong libraries, and the damage shows up later.
  • Killing apt mid-transaction. You get a lock file and a half-configured package. sudo dpkg --configure -a is usually the way back; deleting lock files by hand while apt is running is not.
  • Assuming upgrade covers everything. It never removes packages, so some upgrades wait forever. Check periodically with full-upgrade --dry-run.
  • Forgetting /boot. Kernel packages accumulate and a small /boot partition fills, after which upgrades fail confusingly. Modern apt removes unused kernels on full-upgrade, but only that command.
  • Piping apt output into a script. The wording is not stable, and apt warns you about exactly this.
  • Mixing pip or npm into system directories. Installing over files that apt owns produces breakage that no apt command can explain. Use virtual environments or per-project installs.
Back to top

10. Summary

apt looks like two commands you memorise on your first day. It is really the dependency solver, downloader and trust system that everything else on a Debian or Ubuntu machine is built on.

  • APT means Advanced Package Tool. It solves dependencies and downloads packages; dpkg underneath does the actual installing.
  • apt update refreshes the catalogue, apt upgrade installs newer versions, and full-upgrade is the same but may remove packages to get there.
  • It began as apt 0.0.1 on 31 March 1998, "released only on IRC", and shipped in Debian 2.1 in 1999. The apt command itself arrived in April 2014, "10000b years in the making".
  • "Unable to locate package" nearly always means a missing apt update or a disabled component, not a missing package.
  • remove leaves configuration files behind and dpkg marks the package rc; purge removes those too.
  • Recommends are installed by default and explain large installs. --no-install-recommends cut a git install from 46 packages to 23.
  • apt-mark hold pins a package and produces "kept back"; apt-mark showhold is the first thing to check when a machine stops updating.
  • Repositories live in /etc/apt/sources.list.d/, increasingly in the deb822 .sources format, and each should be bound to its own key with Signed-By.
  • apt only prints its "no stable CLI interface" warning when output is not a terminal. Type apt, script apt-get.
  • Ubuntu's phased updates seed a decision with the package, the version and /etc/machine-id, which is why two identical servers legitimately get different updates.
  • Since apt 2.3.12 the solver refuses to remove essential packages at all, after a public accident showed that a confirmation prompt is not a safety mechanism.
  • Installing a package runs its preinst and postinst scripts as root, which is why repository signatures and Signed-By scoping are the real security boundary.
  • Version strings are [epoch:]upstream-revision; the rare leading 1: is an epoch that overrides normal version ordering, and pinning in /etc/apt/preferences.d/ overrides repository priorities.
  • When in doubt, simulate: apt-get install -s and apt upgrade --dry-run print the entire plan and change nothing.

This is the quick reference worth keeping:

apt update                     refresh the catalogue (installs nothing)
apt upgrade                    install newer versions, never removing anything
apt full-upgrade               as above, but may remove packages to succeed
apt install PKG                install a package
apt install ./file.deb         install a downloaded .deb WITH dependencies
apt remove PKG                 uninstall, keeping configuration files
apt purge PKG                  uninstall, including configuration files
apt autoremove                 delete orphaned dependencies (read the list!)

apt search TEXT                search names and descriptions
apt show PKG                   version, size, dependencies, origin
apt list -a PKG                every version apt knows about
apt list --upgradable          what an upgrade would change
apt-cache policy PKG           which version, from which repository, and why
apt-get install -s PKG         simulate: print the plan, change nothing
apt-get download PKG           fetch the .deb without installing it
apt-config dump                apt's effective configuration
dpkg-deb -I file.deb           a package's control data and maintainer scripts
dpkg-deb -c file.deb           the files a package would install

apt-mark hold PKG              freeze a package at its current version
apt-mark showhold              list frozen packages
apt-mark manual PKG            "I want this", protect it from autoremove
dpkg -l PKG                    package state: ii installed, rc removed-but-config
dpkg -S /path/to/file          which package owns this file
dpkg --configure -a            recover from an interrupted transaction

Package management is where most server trouble either starts or gets fixed: the upgrade that was never applied, the third-party repository nobody documented, the disk that filled with old kernels.

Back to top
Linux command: apt
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.