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.
Table of Contents
- 1. How APT Resolves and Installs Packages
- 2. apt, apt-get, and dpkg: Understanding the Layered Architecture
- 3. Repository Sources: sources.list and sources.list.d in Detail
- 4. APT Pinning: Fixing Packages and Preventing Unwanted Upgrades
- 5. Adding Third-Party Repositories Safely
- 6. The APT Cache: How It Works and Where It Lives
- 7. Cleaning the Cache: apt clean, autoclean, autoremove
- 8. Common Errors and How to Recognize Them
- 9. APT Tools Compared
- 10. Summary
- 11. FAQ
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.