APT Package Management in Detail: Repos, Pins, Cache
AI generated
$
/etc
Linux · Package Management · APT · DevOps
APT Package Management in Detail
Repos, Pins, Cache

APT manages software packages on Debian and Ubuntu servers, yet most admins use only a fraction of its capabilities. This article explains how APT resolves dependencies, why apt, apt-get, and dpkg serve different purposes, how pinning prevents unwanted upgrades, how to safely add third-party repositories, and how to keep the package cache clean.

15 min read apt · apt-get · dpkg · sources.list · pinning Debian · Ubuntu · Linux

1. How APT Resolves and Installs Packages

Before APT installs a single package, it needs current information about which packages, in which versions, are available in the configured repositories. The command apt update downloads a compressed package list for every source registered in /etc/apt/sources.list and /etc/apt/sources.list.d/, then stores it unpacked in /var/lib/apt/lists/. These local indexes contain metadata for each available package such as version, dependencies, file size, and checksum, but no actual program files yet. Without a current apt update, every subsequent command operates on stale information, which can lead to errors such as 404 responses during download or incorrectly resolved version numbers.

Dependency resolution only begins with the actual apt install packagename command. APT reads the Depends, Recommends, Suggests, Conflicts, and Provides fields from the package index and calculates which additional packages must be installed, removed, or upgraded so that every constraint is satisfied simultaneously. Virtual packages such as mail-transport-agent, which multiple concrete packages like postfix or exim4 can provide, make this resolution additionally complex because APT must choose among several valid candidates. With apt install --simulate packagename, the complete installation plan can be checked in advance without actually changing anything, which is indispensable before touching production systems.


#!/usr/bin/env bash
# Refresh package indexes, then simulate an install without changing anything
$ sudo apt update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Get:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease [126 kB]
Get:3 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
Fetched 252 kB in 1s (211 kB/s)
Reading package lists... Done

$ apt install --simulate php8.4-fpm
Inst php8.4-common (8.4.6-1+ubuntu24.04.1 Ondrej PHP PPA)
Inst php8.4-fpm (8.4.6-1+ubuntu24.04.1 Ondrej PHP PPA)
Inst php8.4-opcache (8.4.6-1+ubuntu24.04.1 Ondrej PHP PPA)
Conf php8.4-fpm (8.4.6-1+ubuntu24.04.1 Ondrej PHP PPA)

# Show which candidate version would actually be picked, and why
$ apt-cache policy php8.4-fpm
php8.4-fpm:
  Installed: (none)
  Candidate: 8.4.6-1+ubuntu24.04.1
  Version table:
     8.4.6-1+ubuntu24.04.1 500
        500 https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble/main amd64 Packages

2. apt, apt-get, and dpkg: Understanding the Layered Architecture

At the very bottom of the tool chain sits dpkg, which works directly with individual .deb files. dpkg -i package.deb installs a locally available package file and registers it in the central status database /var/lib/dpkg/status, but it does not resolve dependencies and does not fetch anything from the network. If a dependency is missing, dpkg aborts with an error and leaves the package in the half-configured state. This exact gap is closed by the layer above: apt-get and apt-cache as classic, stable command-line tools that have been around for decades, know about repositories, pull in dependencies automatically, and compute the complete installation plan before dpkg does the actual work in the background.

Since APT 1.1, the apt command bundles the most commonly used functions of apt-get and apt-cache into a single, more user-friendly frontend with a progress bar, colored output, and more sensible defaults. For interactive daily use, apt is therefore the right choice. In scripts and automation, however, the apt man page explicitly warns: the command-line interface of apt is considered unstable and can change between versions, while apt-get and apt-cache have offered a stable, unchanged interface for decades. Deployment scripts and CI pipelines should therefore consistently use apt-get and apt-cache, never apt.

3. Repository Sources: sources.list and sources.list.d in Detail

The classic configuration file /etc/apt/sources.list follows the single-line format deb URI Suite Component1 Component2. The keyword deb marks a binary package source, deb-src a source-code source. The URI points to the repository server, the suite is usually the distribution codename such as noble or bookworm, extended with suffixes like -security or -updates for separate security and update pockets. Components such as main, universe, restricted, and multiverse on Ubuntu separate packages by license status and support scope, with only main and restricted receiving official Canonical support.

Additional sources do not belong in the main file but as separate .list files under /etc/apt/sources.list.d/, so each package repository can be added and removed independently. Since APT 2.4 the system additionally supports the newer, key based DEB822 format with the file extension .sources, which uses multi-line, readable blocks with explicit fields such as Types, URIs, Suites, Components, and Signed-By. Ubuntu 24.04 uses this format by default for the system repositories. The advantage over the old single-line format: the signing key is referenced directly in the repository entry instead of being trusted implicitly system-wide, which makes third-party sources significantly safer.


# /etc/apt/sources.list.d/php.sources
# DEB822 format, introduced with APT 2.4, default on Ubuntu 24.04+
Types: deb
URIs: https://ppa.launchpadcontent.net/ondrej/php/ubuntu
Suites: noble
Components: main
Signed-By: /etc/apt/keyrings/ondrej-php.gpg

# The legacy one-line equivalent in a .list file looked like this:
# deb [signed-by=/etc/apt/keyrings/ondrej-php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main

4. APT Pinning: Fixing Packages and Preventing Unwanted Upgrades

APT pinning uses numeric priorities to control which available version of a package is chosen during an install or upgrade, regardless of which repository offers the newest version. It is configured through files in /etc/apt/preferences.d/, which consist of blocks with Package, Pin, and Pin-Priority. Priority 1000 even forces a downgrade to the specified version, priority 990 typically corresponds to the target suite, priority 500 is the default for all normally configured sources, and values below 0 prevent installation entirely. This can be used, for example, to permanently fix a specific PHP or MariaDB version while the rest of the system upgrades normally.

For simpler cases, apt-mark hold packagename is often enough, which excludes an already installed package from upgrades entirely, without creating a priority file. The difference matters: apt-mark hold blocks every update of the package completely, while pinning granularly prefers a specific version or repository and still allows security updates within the pinned version if the pin rule is written narrowly enough. On Magento servers this is especially relevant for PHP minor versions and the MySQL or MariaDB version, because an automatic major upgrade can trigger compatibility problems with the Magento codebase that should only be resolved during a targeted, planned upgrade.


# /etc/apt/preferences.d/php-pin.pref
# Pin PHP 8.4 packages to the Ondrej PPA, block accidental version jumps

Package: php8.4-*
Pin: origin ppa.launchpadcontent.net
Pin-Priority: 900

# Never allow the experimental PHP 8.5 packages to be installed automatically
Package: php8.5-*
Pin: release *
Pin-Priority: -1

# Force MariaDB to stay on the 10.11 LTS branch, even if 11.x becomes candidate
Package: mariadb-server mariadb-client
Pin: version 1:10.11.*
Pin-Priority: 1001

5. Adding Third-Party Repositories Safely

The command apt-key add was long the standard way to trust a third-party repository's signing key system-wide, but it has been officially deprecated since Debian 11 and Ubuntu 21.10 and will be removed in upcoming versions. The problem: a key imported via apt-key automatically applies to every repository on the system, not just the one that actually needs it. A compromised third-party key could theoretically sign packages for any source. The secure, modern approach stores keys as a dedicated file under /etc/apt/keyrings/, imported with gpg --dearmor from the official source, and explicitly references that file via the Signed-By field in the repository entry.

As a general rule: a key should never be added blindly through a curl ... | sudo bash chain without checking the downloaded key's fingerprint against the vendor's official documentation, for example with gpg --show-keys --with-fingerprint file.asc. After adding a new source, apt update should always run isolated for that one source first to catch errors early, before a global upgrade pulls in unverified foreign packages. Repositories that are no longer actively maintained after a project ends or is deprecated should be promptly removed from /etc/apt/sources.list.d/ again, because outdated signing keys and dead endpoints otherwise turn into silent security gaps.


#!/usr/bin/env bash
set -euo pipefail

# Download the vendor key and verify its fingerprint before trusting it
curl -fsSL https://packages.ondrej.dev/apt/gpg.key -o /tmp/ondrej.asc
gpg --show-keys --with-fingerprint /tmp/ondrej.asc

# Expected fingerprint (compare against the vendor's published documentation)
# 4F4EA0AAE5267A6C41AD8AF52930147C0357222E

# Convert to binary keyring format, store in a dedicated, package-specific location
mkdir -p /etc/apt/keyrings
gpg --dearmor -o /etc/apt/keyrings/ondrej-php.gpg /tmp/ondrej.asc
chmod 644 /etc/apt/keyrings/ondrej-php.gpg
rm -f /tmp/ondrej.asc

# Update only this one source first, to catch errors before a full upgrade
apt-get update -o Dir::Etc::sourcelist="sources.list.d/php.sources" \
                -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"

6. The APT Cache: How It Works and Where It Lives

APT maintains two entirely different caches that are frequently confused. The first lives in /var/lib/apt/lists/ and contains the downloaded package indexes from apt update, meaning metadata about available versions, but no executable programs. The second lives in /var/cache/apt/archives/ and contains the actually downloaded .deb files of every installed or upgraded package. By default, APT does not automatically delete these archive files after a successful installation, but keeps them in case a package needs to be reinstalled, rolled back to an older version, or needed offline on another system.

On long-lived servers that are updated regularly over years, this archive cache can reach several gigabytes, especially on systems with many large packages such as database servers or kernel images, of which every installed version is kept separately. With du -sh /var/cache/apt/archives, the current size can be checked quickly. In Docker images, this cache is particularly unwelcome because it inflates image size unnecessarily, which is why Dockerfiles typically run rm -rf /var/lib/apt/lists/* right after the apt-get install command within the same layer instruction, so the index cache never ends up in the final image at all.

7. Cleaning the Cache: apt clean, autoclean, autoremove

APT's three cleanup commands differ substantially in scope and safety, yet are often used interchangeably. apt clean deletes every single .deb file from /var/cache/apt/archives/ completely, regardless of whether the version it contains is still current or installed. apt autoclean is more cautious and only removes archive files of package versions that can no longer be downloaded from the configured repository because they have been superseded by a newer version. For servers with limited disk space and regular backups, autoclean as a cron job is the lower-risk choice, because currently installable versions remain in the cache.

apt autoremove solves a different problem: it removes packages that were originally installed automatically as a dependency of another package but are no longer required by any installed package, such as old kernel versions after several kernel updates. With the additional flag --purge, configuration files in /etc are removed as well, not just the program files, which should be checked beforehand for sensitive services. A sensible combination for a weekly maintenance cron job is apt-get autoremove --purge -y && apt-get autoclean -y, which removes orphaned dependencies and outdated cache files without deleting still-installable, current package versions from the archive cache.


# ansible/playbooks/apt-maintenance.yaml
# Weekly fleet-wide cache hygiene, orphan removal and drift reporting
- name: APT maintenance across the server fleet
  hosts: magento_servers
  become: true
  tasks:
    - name: Remove orphaned automatically-installed packages
      apt:
        autoremove: true
        purge: true

    - name: Remove outdated archive files, keep currently installable ones
      apt:
        autoclean: true

    - name: Report current archive cache size
      command: du -sh /var/cache/apt/archives
      register: cache_size
      changed_when: false

    - name: Fail loudly if the cache still exceeds 1 GiB after cleanup
      fail:
        msg: "APT cache too large: {{ cache_size.stdout }}"
      when: "'G' in cache_size.stdout and (cache_size.stdout.split('G')[0] | float) > 1.0"

8. Common Errors and How to Recognize Them

The message E: Unmet dependencies appears when an installed or to-be-installed package requires a dependency that cannot be satisfied in the current system state, often after a manual dpkg -i of an incompatible package file or after an interrupted upgrade. The command apt --fix-broken install computes a repair plan that installs missing dependencies or removes incompatible packages to restore a consistent state. If an upgrade was interrupted by a system crash or a Ctrl-C, APT frequently reports dpkg was interrupted, which can be fixed with sudo dpkg --configure -a, finishing the configuration of every half-configured package.

A second common puzzle: a package does not get upgraded despite apt upgrade, even though a newer version is available in the repository. The cause is usually a forgotten hold status or an overly aggressive pin rule. apt-mark showhold lists all held packages at a glance, and apt-cache policy packagename additionally shows the effective priority from every applicable pin rule, including the respective source. For deeper resolution problems, apt-get install packagename -o Debug::pkgProblemResolver=true delivers a detailed trace of the internal resolver algorithm, showing which candidates were rejected and for what reason, which is often the only way to the actual root cause in complex conflict chains.

9. APT Tools Compared

The choice between the various APT tools and patterns has direct consequences for security, scriptability, and traceability in server operations. The following table summarizes the key decisions that are regularly made incorrectly in practice.

Task Unsafe / Risky Recommended Pattern Benefit
Installing a package in scripts apt install (unstable CLI) apt-get install Stable interface unchanged for decades
Locking a package version apt-mark hold without documentation Pin-Priority in preferences.d Granular, traceable, version-controllable
Adding a foreign key apt-key add (deprecated) Signed-By keyring in /etc/apt/keyrings Trust applies only to the one source
Clearing the cache rm -rf /var/cache/apt/archives/* apt-get clean / autoclean APT state stays consistent
Fixing dependency errors dpkg -i --force-depends apt --fix-broken install Computes a consistent repair plan

In practice, nearly every unsafe column in the table corresponds to a pattern that works in the short term but leads to inconsistent or hard-to-trace system states over time. Anyone who consistently applies APT commands according to the recommended patterns and manages repository and pin configuration under version control in a configuration management tool like Ansible reduces drift between servers significantly and makes every packaging decision traceable after the fact.

Mironsoft

Server hardening, package management, and deployment infrastructure for Magento hosting

Reproducible server environments for your Magento stack?

We set up version-locked, traceable package management for PHP, MySQL/MariaDB, and system services, with clean repository management, targeted pinning, and automated cache hygiene for stable deployments.

Repository Hardening

Signed-By keyrings instead of apt-key, vetted third-party sources for your stack

Version Pinning

Fixing PHP, MySQL, and kernel versions deliberately, securing Magento compatibility

Server Automation

Ansible-driven, version-controlled package configuration across the whole server fleet

10. Summary

The key points on APT package management at a glance: apt update populates the local package indexes in /var/lib/apt/lists/, without installing anything itself. dpkg installs individual .deb files without dependency resolution, apt and apt-get build on top of it as frontends, with apt-get remaining the stable, script-safe interface. Pinning through /etc/apt/preferences.d/ controls package versions granularly via priorities, signed-by keyrings under /etc/apt/keyrings/ replace the deprecated apt-key, and apt clean, autoclean, and autoremove keep the package cache and the installed package list lean.

The biggest lever lies in applying these patterns consistently across the entire server fleet, rather than implementing them once manually on a single server. Repository definitions, pin files, and maintenance cron jobs belong in a configuration management tool like Ansible, so that every server instance has the same, traceable package state, and a compatibility problem does not first surface after an uncontrolled major upgrade on a single server.

APT Package Management in Detail: The Essentials at a Glance

Layered Architecture

dpkg installs .deb files without resolution, apt/apt-get resolve dependencies. apt for interactive use, apt-get for scripts.

Pinning

Pin-Priority in preferences.d: 1000 forces, 500 is default. apt-mark hold for simple cases without a file.

Third-Party Repos

Signed-By keyring instead of apt-key, verify the fingerprint against vendor documentation before adding the source.

Cache Hygiene

autoclean instead of clean for low-risk maintenance. autoremove --purge removes orphaned dependencies including configuration.

11. FAQ: APT Package Management in Detail

1What is the difference between apt and apt-get?
apt bundles the most commonly used functions of apt-get and apt-cache into a more user-friendly frontend. apt-get offers a stable interface for scripts.
2Why not use apt in scripts?
The apt man page warns that the CLI of apt is unstable and can change. apt-get and apt-cache have offered a stable interface for decades.
3How do I refresh just the package list?
With apt update. Only refreshes the indexes, without installing or changing any package.
4Pin-Priority 1000 vs. 500?
1000 even forces a downgrade. 500 is the default value for normally configured sources with no special rule.
5Protect a package from upgrades?
apt-mark hold packagename for the simple case. For finer control, create a Pin-Priority file under preferences.d.
6Why is apt-key deprecated?
An imported key applies system-wide to every source. Signed-By keyrings bind trust to a single source instead.
7apt clean vs. apt autoclean?
clean deletes everything completely. autoclean only removes archive files of superseded, no longer downloadable versions.
8When to use apt autoremove?
Regularly, to remove orphaned dependencies such as old kernel versions. With --purge, configuration files are removed too.
9Fix E: Unmet dependencies?
apt --fix-broken install computes a repair plan. For an interrupted upgrade, dpkg --configure -a also helps.
10Where does the APT cache live?
Indexes in /var/lib/apt/lists/, downloaded .deb files in /var/cache/apt/archives/. Check the size regularly with du -sh.