Skip to main content
Linux · lesson 6 of 7
Linux beginner Lesson 6 of 7

apt vs dpkg: A Real Broken Dependency, and How apt Actually Fixes It

dpkg -i installs a .deb but refuses to configure it when dependencies are missing — leaving a package that half-works. The real fix isn't reinstalling it; it's letting apt resolve what dpkg couldn't.

By SkillByExample · Published

linux apt dpkg packages

dpkg -i on a single file looks like the obvious way to install a .deb — and it works, right up until that package needs something dpkg has no way to fetch on its own.

Installing a .deb directly with dpkg

docker run -d --name pkg-lesson ubuntu:22.04 sleep infinity
docker exec pkg-lesson bash -c "apt-get update -qq && cd /tmp && apt-get download git"
docker exec pkg-lesson bash -c "cd /tmp && dpkg -i git_1%3a2.34.1-1ubuntu1.17_amd64.deb"
Selecting previously unselected package git.
(Reading database ... 4393 files and directories currently installed.)
Preparing to unpack git_1%3a2.34.1-1ubuntu1.17_amd64.deb ...
Unpacking git (1:2.34.1-1ubuntu1.17) ...
dpkg: dependency problems prevent configuration of git:
 git depends on libcurl3-gnutls (>= 7.56.1); however:
  Package libcurl3-gnutls is not installed.
 git depends on libexpat1 (>= 2.0.1); however:
  Package libexpat1 is not installed.
 git depends on perl; however:
  Package perl is not installed.
 git depends on liberror-perl; however:
  Package liberror-perl is not installed.
 git depends on git-man (>> 1:2.34.1); however:
  Package git-man is not installed.
 git depends on git-man (<< 1:2.34.1-.); however:
  Package git-man is not installed.

dpkg: error processing package git (--install):
 dependency problems - leaving unconfigured
Errors were encountered while processing:
 git

apt-get download fetched only git’s own file, with none of its dependencies — exactly what dpkg -i needs to fail this way, since dpkg unpacks whatever file you hand it without ever checking a repository for what else that file needs.

The bug: a package that’s unpacked but not configured

docker exec pkg-lesson dpkg -s git
Package: git
Status: install ok unpacked

unpacked, not installed — the files are on disk, but dpkg never finished setting the package up. Commands that don’t touch the missing libraries still work:

docker exec pkg-lesson git --version
docker exec pkg-lesson bash -c "cd /tmp && mkdir testrepo && cd testrepo && git init"
git version 2.34.1
Initialized empty Git repository in /tmp/testrepo/.git/

But git clone over HTTPS runs through a separate helper binary linked against the missing library:

docker exec pkg-lesson bash -c "git clone https://github.com/git/git.git /tmp/clonetest"
Cloning into '/tmp/clonetest'...
/usr/lib/git-core/git-remote-https: error while loading shared libraries: libcurl-gnutls.so.4: cannot open shared object file: No such file or directory

Not “command not found,” not “git: not installed” — a shared-library load failure from a helper binary the user never invokes directly. This is what a half-configured package actually looks like in practice: some subcommands work, others fail in a way that doesn’t obviously point back to a missing dependency.

The fix: let apt finish what dpkg couldn’t

docker exec pkg-lesson apt-get install -f -y
Setting up libgdbm6:amd64 (1.23-1) ...
Setting up libldap-2.5-0:amd64 (2.5.20+dfsg-0ubuntu0.22.04.1) ...
Setting up ca-certificates (20260601~22.04.1) ...
Setting up libgdbm-compat4:amd64 (1.23-1) ...
Setting up libperl5.34:amd64 (5.34.0-3ubuntu1.9) ...
Setting up libcurl3-gnutls:amd64 (7.81.0-1ubuntu1.27) ...
Setting up perl (5.34.0-3ubuntu1.9) ...
Setting up liberror-perl (0.17029-1) ...
Setting up git (1:2.34.1-1ubuntu1.17) ...

-f (“fix broken”) tells apt to look at whatever dpkg left half-done, resolve the actual dependency chain, download every missing package, and finish configuring git last. Nothing re-downloads git itself — only the six missing dependencies dpkg listed earlier.

docker exec pkg-lesson dpkg -s git
docker exec pkg-lesson bash -c "rm -rf /tmp/clonetest && git clone --depth 1 https://github.com/git/git.git /tmp/clonetest && ls /tmp/clonetest | head -3"
Status: install ok installed
Cloning into '/tmp/clonetest'...
CODE_OF_CONDUCT.md
COPYING
Cargo.toml

installed, not unpacked — and the exact clone that failed before now succeeds, using the same package that was already on disk the whole time.

Practice

1. List an installed package's version the way dpkg -l shows it.
docker exec pkg-lesson dpkg -l git
||/ Name           Version              Architecture Description
+++-==============-====================-============-===================================================
ii  git            1:2.34.1-1ubuntu1.17 amd64        fast, scalable, distributed revision control system

ii in the first column means “installed, correctly configured” — during the broken window earlier this would have shown a different pair of letters (iU, for unpacked-but-not-yet- configured), which is a faster way to spot a broken package across an entire system than checking each one individually with dpkg -s.

2. Check whether a newer version of an installed package is available before upgrading.
docker exec pkg-lesson apt-cache policy git
git:
  Installed: 1:2.34.1-1ubuntu1.17
  Candidate: 1:2.34.1-1ubuntu1.17
  Version table:
 *** 1:2.34.1-1ubuntu1.17 500
        500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages
        100 /var/lib/dpkg/status
     1:2.34.1-1ubuntu1 500
        500 http://archive.ubuntu.com/ubuntu jammy/main amd64 Packages

Installed matches Candidate here, so apt upgrade would leave git alone. The second, older version listed underneath (jammy/main, without -updates) shows exactly which repository a security or point-release update actually came from.

3. Find every binary a package actually installed, without guessing paths.
docker exec pkg-lesson bash -c "dpkg -L git | grep bin"
/usr/bin
/usr/bin/git
/usr/bin/git-shell
/usr/bin/git-receive-pack
/usr/bin/git-upload-archive
/usr/bin/git-upload-pack

dpkg -L lists every file a package owns — this is the reverse of dpkg -S <path>, which answers “what package owns this file.” Between the two, there’s rarely a need to guess where a package put something or what a package would remove.

Next: filesystems and disk usage — inodes, hard versus soft links, and what actually happens when a filesystem runs out of inodes before it runs out of space.

Frequently Asked Questions

What's the actual difference between dpkg and apt?
dpkg is the low-level tool that unpacks a single .deb file and records it in the package database — it has no idea what other packages exist or how to fetch them. apt is the higher-level tool that talks to package repositories, resolves dependency chains across many packages, downloads them, and calls dpkg to actually install each one. dpkg -i a single file is exactly the case where apt's dependency resolution is skipped.
Why did git --version and git init work even though dpkg reported git as broken?
dpkg had already unpacked git's files to disk before hitting the dependency check — the binary and its shell-invocable subcommands exist and run fine for anything that doesn't need the missing shared libraries. Only the specific code paths that link against libcurl (git's HTTPS transport, run as a separate git-remote-https helper) actually fail, which is why the break looked partial rather than total.
What does apt-get install -f actually do?
It looks at whatever dpkg left in a half-configured state, works out what's still missing from the dependency chain, downloads exactly those packages, and finishes the configuration step dpkg couldn't complete on its own. It doesn't reinstall or re-download the original package — git's own .deb was already unpacked; only its dependencies were missing.
How do I check what package a specific binary on disk came from?
dpkg -S /path/to/file reverse-looks-up which installed package owns a given file, and dpkg -L packagename lists every file a package installed — the two are inverses of each other. This is the standard way to answer 'where did this file come from' or 'what will removing this package delete' before acting.
Does apt-get and apt do the same thing?
For everyday commands like install, remove, and update, yes — apt is a newer, user-friendlier wrapper introduced to consolidate apt-get and apt-cache's most common subcommands with better default output (progress bars, color). Scripts still commonly use apt-get because its output format is considered more stable across versions; either is correct to reach for interactively.