dpkg internals
dpkg is the low-level engine under apt. A .deb file is a two-archive bundle: control.tar with metadata and maintainer scripts, data.tar with the actual files. dpkg -i/-l/-L/-S query and manipulate the local package database at /var/lib/dpkg.
Halfway through a sudo apt install the network drops. Now dpkg says the package is “half-configured” and every subsequent apt command fails with “dpkg was interrupted, you must manually run ‘sudo dpkg —configure -a’”. Or you find a file on the filesystem and need to know which package owns it. Or a vendor ships a .deb with no apt repository. In all three cases you are working with dpkg directly — apt is not in the picture. Understanding dpkg is the difference between being stuck and being two commands from fixed.
After this lesson you can query the dpkg database with -l, -L, and -S, install a standalone .deb with -i, understand what a .deb archive contains at the binary level, explain what half-configured means and how to recover, and explain why apt calls dpkg rather than replacing it.
dpkg is the engine; apt is the front-end. dpkg manages the local package database and installs/removes files. It knows nothing about repositories, mirrors, or dependency resolution across packages it has not yet downloaded. apt adds those layers on top: it fetches indexes, resolves the full dependency graph, downloads .deb files, and then hands them to dpkg in the correct order.
# Installing without apt — dpkg only
# (all dependencies must already be installed; dpkg will refuse if they are not)
sudo dpkg -i ./mypackage_1.2.3_amd64.deb
# dpkg -i error when a dependency is missing:
# dpkg: dependency problems prevent configuration of mypackage:
# mypackage depends on libfoo3 (>= 3.1); libfoo3 is not installed.
# Use apt to resolve: sudo apt -f installapt -f install (“fix broken”) tells apt to look at the current dpkg state, find missing dependencies, fetch them from a repository, and hand everything back to dpkg to complete configuration. This is the correct recovery path after a partial install.
The dpkg database lives in /var/lib/dpkg/ and is plain text. The most important files:
# The status file — one entry per installed/known package
cat /var/lib/dpkg/status | head -40
# info/ directory — per-package metadata and maintainer scripts
ls /var/lib/dpkg/info/nginx.list # list of all files installed by nginx
cat /var/lib/dpkg/info/nginx.postinst # post-install script (runs after dpkg unpacks)
# Query the database: list installed packages
dpkg -l # all installed packages
dpkg -l 'nginx*' # filter by pattern
dpkg -l | grep '^ii' # only fully installed (ii = installed, not half-configured)The status field prefix in dpkg -l output reads as two letters: the first is the desired state (i=install, r=remove, p=purge), the second is the current state (i=installed, c=config-files only, H=half-installed). A line starting with iH means dpkg tried to install but got interrupted before configuration finished.
A .deb archive is an ar archive containing three members. You can inspect it without installing:
# Examine a .deb without installing it
ar t nginx_1.18.0-6ubuntu14_amd64.deb
# Output:
# debian-binary ← "2.0\n" — format version marker
# control.tar.xz ← metadata, dependency declarations, maintainer scripts
# data.tar.xz ← the actual filesystem tree to unpack
# Extract and inspect control.tar
ar x nginx_1.18.0-6ubuntu14_amd64.deb control.tar.xz
tar --list -f control.tar.xz
# ./control ← package name, version, arch, dependencies
# ./conffiles ← list of config files that survive purge
# ./preinst ← runs BEFORE unpacking
# ./postinst ← runs AFTER unpacking (typically starts the service)
# ./prerm ← runs BEFORE removal
# ./postrm ← runs AFTER removalThe maintainer scripts (pre/post install/remove) are shell scripts that run as root. This is why package authenticity (GPG signing) matters — a malicious postinst in a tampered package runs arbitrary code with root privileges at install time.
dpkg -L and dpkg -S let you navigate the ownership graph. These are the two most useful dpkg queries for incident investigation:
# List all files installed by a package
dpkg -L nginx
# /usr/sbin/nginx
# /etc/nginx/nginx.conf
# /lib/systemd/system/nginx.service
# ...
# Find which package owns a file
dpkg -S /usr/sbin/nginx
# nginx: /usr/sbin/nginx
# Check if a file on disk belongs to any package
dpkg -S /etc/nginx/nginx.conf.bak
# dpkg-query: no path found matching pattern /etc/nginx/nginx.conf.bak
# (it was created by the operator, not a package — safe to edit or delete)
# Verify package integrity: check installed files against expected checksums
dpkg -V nginx
# (no output = all files match; differences printed per file)dpkg -V runs md5 checksums of every file listed in the package manifest against what is on disk. A modified config file will show up — useful for detecting unauthorized changes to system files.
The half-configured state is the most common dpkg emergency. It happens when an install is interrupted after unpacking but before the postinst script completes. The package is on disk but dpkg has not registered it as fully configured.
# Symptoms — any apt command fails with:
# E: dpkg was interrupted, you must manually run:
# sudo dpkg --configure -a
# Fix: tell dpkg to finish configuring anything that was interrupted
sudo dpkg --configure -a
# If postinst itself fails (e.g. the service fails to start), you may need:
sudo dpkg --configure -a --force-confdef --force-confnew
# Nuclear option: remove the half-installed package and start over
sudo dpkg --remove --force-remove-reinstreq badpackage
sudo apt install badpackage # re-fetch and install cleanlyThe --force-remove-reinstreq flag removes a package even though dpkg considers it in a reinstreq (reinstall required) state. Use it only when --configure -a fails — it can leave files on disk that you then need to clean up manually.
Diagnose a broken package state after a network drop during apt install.
A junior engineer ran sudo apt install postgresql-15 and the network cut mid-install. Now every apt or dpkg command fails.
# Step 1: see what state dpkg thinks things are in
dpkg -l 'postgresql*'
# ii postgresql-client-15 ... ok installed
# iH postgresql-15 ... half-installed ← the problem package
# Step 2: look at the error in the status file
grep -A5 'Package: postgresql-15' /var/lib/dpkg/status
# Status: install half-configured
# ...
# Step 3: attempt to finish configuration (downloads nothing, just runs postinst)
sudo dpkg --configure -a
# If postinst starts the postgres cluster and it fails due to data dir issues:
# dpkg: error processing package postgresql-15 (--configure):
# subprocess installed post-installation script returned error exit status 1
# Step 4: check the postinst failure
journalctl -u postgresql@15-main.service --since "5 minutes ago"
# Confirm it's a startup issue, not a broken package
# Step 5: fix the underlying startup issue (e.g. data dir ownership)
sudo chown -R postgres:postgres /var/lib/postgresql/15/
sudo dpkg --configure -a # retry — now succeeds
dpkg -l 'postgresql-15' # ii = fully installedKey insight: dpkg --configure -a does not re-download anything. It only runs the postinst script of packages stuck in the half-configured state. The real error is almost always in the postinst script, and the fix is almost always to resolve the underlying service startup issue.
▸Common mistake
Never edit files in /var/lib/dpkg/ by hand to “fix” a bad package state. The format looks simple but the fields are interdependent — a status file with an invalid entry will cause every dpkg and apt command to fail completely, and there is no built-in repair tool for a corrupt dpkg database. If you genuinely need to remove a stale entry, use dpkg --remove --force-remove-reinstreq <package> which at least goes through the proper state machine. Manual edits to the status file are a last resort for offline recovery from a backup.
▸Why this works
Why do maintainer scripts run as root? dpkg was designed in the 1990s for system-wide installation. The postinst often needs to create system users (useradd --system), set file permissions (chmod 700 /etc/postgresql), or start services (systemctl enable postgresql). All of those require root. This is also why package signing is non-negotiable: postinst has the same power as sudo rm -rf /. The attack surface of a rogue .deb is the entire root filesystem.
You find a binary at /usr/bin/pg_dump and want to know which package installed it and whether it has been tampered with. Which pair of commands gives you this information?
dpkg is the low-level engine; apt builds on top of it. A .deb file is an ar archive with control.tar (metadata + maintainer scripts preinst/postinst/prerm/postrm) and data.tar (the filesystem tree). The dpkg database at /var/lib/dpkg/ tracks every installed package’s state. Key queries: -l lists packages (prefix iH = half-configured), -L lists a package’s files, -S finds the package owning a file, -V verifies file checksums. A half-configured state (interrupted install) is fixed with dpkg --configure -a. Never edit /var/lib/dpkg/status by hand.
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.