Dependencies, holds, and pinning
Debian packages declare Depends, Recommends, Suggests, and Conflicts. apt resolves the full dependency graph before touching disk. Holds freeze a package at its current version; pinning controls which repository version wins when multiple sources provide the same package.
You run apt upgrade and it wants to upgrade the kernel. Your production server runs a custom kernel module that is not compatible with the new version. Or you have pinned a specific version of PostgreSQL because your DBA says the minor upgrade breaks a replication feature. Or you are mixing packages from Debian stable and backports and apt keeps “helpfully” upgrading packages you wanted to leave on the stable version. All three problems are solved with the same two tools: holds and pinning. Get these wrong and your “up to date” server is either silently broken or permanently stuck on an insecure package.
After this lesson you can explain the difference between Depends, Recommends, and Suggests; read a version constraint like (>= 2.0) and (= 1.18.0-6ubuntu14); hold a package at its current version with apt-mark; unhold it safely; and write a /etc/apt/preferences.d/ pin to control which repository wins for a specific package.
Dependency types have different semantics — apt treats them very differently. Understanding the difference explains why apt install --no-install-recommends can dramatically reduce disk usage on a server.
# View all dependency fields for a package
apt-cache show nginx | grep -E '^(Depends|Recommends|Suggests|Pre-Depends|Conflicts|Breaks):'
# Depends: MUST be installed; apt will refuse to install without them
# Recommends: installed by default but skippable; --no-install-recommends skips them
# Suggests: informational only; never auto-installed
# Pre-Depends: like Depends but must be configured BEFORE this package is unpacked
# Conflicts: cannot be installed alongside the listed package
# Breaks: the listed package would malfunction if installed alongside this oneOn a minimal server, --no-install-recommends is often appropriate:
# In a Dockerfile: saves 30-60% on image size for many packages
RUN apt-get install -y --no-install-recommends nginxPre-Depends are rare but critical: they exist for packages like libc6 that must be fully configured before anything else on the system is touched, because dpkg itself depends on them.
Version constraints let package maintainers express compatibility precisely. You will see them in apt-cache show output and in your own dependency specifications:
Depends: libssl3 (>= 3.0.0), libpcre3 (>= 1:8.31), nginx-common (= 1.18.0-6ubuntu14.4)
| Constraint | Meaning |
|---|---|
(>= 1.2) | version 1.2 or any later version |
(<= 2.0) | version 2.0 or any earlier version |
(= 1.2.3-4) | exactly this version, including the Debian revision suffix |
(>> 1.0) | strictly greater than 1.0 (rare) |
(<< 2.0) | strictly less than 2.0 (rare) |
# Check if an installed package satisfies a constraint
dpkg --compare-versions "1.18.0-6ubuntu14.4" ">=" "1.18.0" && echo "satisfies" || echo "does not"
# satisfies
# This is exactly what apt runs internally when resolvingThe Debian epoch prefix (1: in 1:8.31) overrides normal version sorting. A package version 1:0.1 sorts higher than 9.99 because the epoch number wins. Epochs exist when a package’s versioning scheme changed upstream and the new version number is numerically lower than the old one.
apt-mark hold freezes a package at its installed version. apt upgrade will skip held packages entirely — they will not be upgraded even when a newer version is available.
# Hold a package at its current version
sudo apt-mark hold nginx
# nginx set on hold.
# Verify
apt-mark showhold
# nginx
# Check dpkg status — it now shows "hi" (hold + installed)
dpkg -l nginx
# hi nginx 1.18.0-6ubuntu14.4 ...
# apt upgrade will warn about held packages:
# The following packages have been kept back: nginx
# Remove the hold when you are ready to upgrade
sudo apt-mark unhold nginxThe most common use: holding the kernel on a server with an out-of-tree module until the module vendor releases a compatible build. The gotcha: a held package can block security updates for packages that depend on it. Run apt upgrade periodically and check what is being kept back — a held package should be a deliberate, reviewed decision, not something you forgot from six months ago.
Pinning controls which version apt prefers when multiple repositories provide the same package. The file /etc/apt/preferences (or files in /etc/apt/preferences.d/) assigns numeric priorities. The highest priority wins.
# /etc/apt/preferences.d/backports-pin
# Default priorities:
# 500 = packages from the target release (your main suite)
# 100 = installed packages and packages from non-target releases
# 1 = "never automatically install from this source"
# -1 = "never install, even if explicitly requested"
# Pin nginx to the backports version (priority 900 > default 500)
Package: nginx
Pin: release a=jammy-backports
Pin-Priority: 900
# Pin everything else from backports LOWER than stable (prevents mass upgrades)
Package: *
Pin: release a=jammy-backports
Pin-Priority: 100# Check effective policy for a package — shows which repo wins
apt-cache policy nginx
# Installed: 1.18.0-6ubuntu14.4
# Candidate: 1.25.3-1~jammy ← backports wins due to pin priority 900
# Version table:
# 1.25.3-1~jammy 900 ← from jammy-backports (pinned)
# ...
# 1.18.0-6ubuntu14.4 500 ← from jammy (standard priority)A pin priority above 1000 will even downgrade an installed package to the pinned version — extremely dangerous in production, used only for emergency rollbacks.
Dependency conflicts occur when two packages cannot coexist. Conflicts: means “dpkg refuses to install both simultaneously”. Breaks: means “I would work but the listed package would malfunction alongside me.”
# Example: trying to install two conflicting packages
sudo apt install apache2 nginx-full
# The following packages conflict:
# nginx-full conflicts with apache2 (both bind port 80 by default)
# Resolution: remove the conflicting package first
sudo apt remove apache2
sudo apt install nginx-full
# Simulate what apt would do without touching disk
apt-get install -s nginx-full # -s = simulate
# When apt says "held broken packages" during upgrade:
sudo apt install -f # fix broken: installs missing deps, removes conflicting onesapt install -f (fix) is the first response to a broken dependency state. It does not randomly install things — it reads the current dpkg state and installs or removes exactly what is needed to reach a consistent state. If -f itself fails, read the error carefully: it usually names a specific conflicting package.
Mix Debian stable and backports safely for a single package.
Goal: run nginx from bookworm-backports on a Debian 12 server while keeping everything else on stable. Without pinning, apt upgrade would eventually pull all backport updates.
# 1. Add backports to sources (deb822 format)
cat > /etc/apt/sources.list.d/backports.sources << 'EOF'
Types: deb
URIs: http://deb.debian.org/debian
Suites: bookworm-backports
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
# 2. Write the pin file
cat > /etc/apt/preferences.d/backports-selective << 'EOF'
# Suppress automatic backport upgrades for everything
Package: *
Pin: release a=bookworm-backports
Pin-Priority: 100
# Except nginx — prefer backports
Package: nginx nginx-common nginx-core
Pin: release a=bookworm-backports
Pin-Priority: 900
EOF
# 3. Update and verify
sudo apt update
apt-cache policy nginx
# Candidate should now show the backports version
# 4. Install (backports version wins due to priority 900)
sudo apt install nginx
# 5. Confirm holds won't interfere
apt-mark showhold
# (nothing held — pinning is different from holding)The critical distinction: a hold locks the version that is already installed. A pin controls which version apt selects as the candidate — it works even before installation.
▸Common mistake
Holding a security-critical package indefinitely is a common mistake that feels safe but is not. The hold prevents apt from automatically upgrading, but it also prevents security updates for that specific package. A held nginx sitting at a version with a known CVE will never be updated unless you explicitly unhold it. The right pattern: hold for a defined window (“until our custom module vendor ships the update”), put a calendar reminder, and unhold as soon as possible. apt-mark showhold should be part of your weekly server hygiene check, not a surprise discovery six months later.
You have pinned a package in /etc/apt/preferences.d/ with Pin-Priority: 900 from backports, and separately run apt-mark hold on the same package. apt upgrade now reports the package is 'kept back'. Which mechanism is actually preventing the upgrade?
Depends must be satisfied for an install to proceed; Recommends are auto-installed by default but skippable with --no-install-recommends; Suggests are informational only. Version constraints (>=, =, <<) let maintainers express exact compatibility. apt-mark hold freezes a package and prevents apt upgrade from touching it — use apt-mark showhold to audit held packages, which can silently block security updates. Pinning via /etc/apt/preferences.d/ assigns numeric priorities: default is 500, backports default is 100, above 500 promotes a repo’s packages. A pin above 1000 forces downgrade. Hold vs pin: a hold blocks upgrade of what is installed; a pin selects which version is the candidate.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.