Build a .deb Package
Step-by-step guide to building a .deb package using dh_make, debian/control, debian/rules, lintian quality checks, and clean chroot builds with sbuild.
Before you start
- ▸A Debian or Ubuntu system with sudo access
- ▸Source code or a tarball of the software you want to package
- ▸Basic familiarity with Makefiles and compiling C or similar projects
- ▸Internet access for sbuild chroot creation
Building a .deb package lets you distribute software cleanly across Debian-based systems, manage dependencies formally, and integrate with apt. The toolchain—dh_make, the debian/ directory, and sbuild—has a reputation for complexity, but each piece has a clear job. This guide walks from source tarball to a lintian-clean package built inside a clean chroot.
Prerequisites and Tooling
Work on Ubuntu 24.04 LTS or Debian 12. Install the packaging toolchain:
sudo apt install build-essential devscripts dh-make lintian sbuild schroot debootstrap
Add yourself to the sbuild group so you can use the build chroot without sudo:
sudo adduser $USER sbuild
newgrp sbuild
Step 1: Prepare the Source Tarball
Debian packaging expects a specific naming convention: package_version.orig.tar.gz. If you are packaging something you wrote, produce the tarball yourself. If it is third-party, download the upstream release.
mkdir ~/packaging && cd ~/packaging
# Example: a small C program called 'hellocli' version 1.0
mkdir hellocli-1.0
tar czf hellocli_1.0.orig.tar.gz hellocli-1.0/
The underscore before the version and .orig before the extension are mandatory—tools like dpkg-source key on them.
Step 2: Run dh_make to Scaffold the debian/ Directory
Enter the unpacked source directory and run dh_make. The -p flag sets the package name and version explicitly; -s requests a single binary package.
cd hellocli-1.0
dh_make -p hellocli_1.0 -s -f ../hellocli_1.0.orig.tar.gz
dh_make creates a debian/ subdirectory with template files. Most of the .ex and .EX example files can be deleted if you do not need them:
rm debian/*.ex debian/*.EX
Step 3: Edit debian/control
debian/control is the central metadata file. It has two stanzas: one for the source package and one for each binary package produced. Open it in your editor and fill in the fields accurately.
Source: hellocli
Section: utils
Priority: optional
Maintainer: Ada Lovelace <[email protected]>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://example.com/hellocli
Package: hellocli
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: A minimal CLI greeting tool
A one-line greeting utility demonstrating Debian packaging.
This is the long description; indent continuation lines with a space.
- Build-Depends: list everything needed to compile.
debhelper-compat (= 13)pins the debhelper compatibility level and replaces the olddebian/compatfile. - Depends:
${shlibs:Depends}is a substitution variable filled automatically bydpkg-shlibdeps; leave it unless you are packaging a pure Python or shell package. - Standards-Version: use the current value from the Debian Policy Manual. Lintian will warn if it is stale.
Step 4: Edit debian/rules
debian/rules is an executable Makefile. With debhelper 13, the minimal version delegates everything to the dh command sequence:
#!/usr/bin/make -f
%:
dh $@
That single rule handles configure, build, install, and clean via debhelper helpers (dh_auto_configure, dh_auto_build, dh_auto_install, etc.). If your project uses a non-standard build system, override specific targets:
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_build:
$(MAKE) PREFIX=/usr
Make sure the file is executable:
chmod +x debian/rules
Step 5: Fill in Remaining debian/ Files
debian/changelog
dh_make creates this with a placeholder. The format must be exact—use dch to edit it safely:
dch --create -v 1.0-1 --package hellocli "Initial release"
The trailing -1 is the Debian revision. The distribution should match your target: unstable, noble, bookworm, etc.
debian/copyright
Fill in the license and upstream copyright. Lintian enforces the DEP-5 machine-readable format. A minimal example:
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: hellocli
Source: https://example.com/hellocli
Files: *
Copyright: 2024 Ada Lovelace <[email protected]>
License: MIT
License: MIT
Permission is hereby granted, free of charge ...
Step 6: Build Locally with dpkg-buildpackage (Quick Test)
Before committing to an sbuild chroot, do a fast local build to catch obvious errors:
cd ~/packaging/hellocli-1.0
dpkg-buildpackage -us -uc -b
-us -uc skips GPG signing. -b builds binary packages only. Artifacts land one level up in ~/packaging/. Fix any compiler or install errors before moving on.
Step 7: Run Lintian
Lintian is the Debian package linter. Run it against the freshly built .deb:
lintian --pedantic --display-info ../hellocli_1.0-1_amd64.deb
Common issues and fixes:
- W: no-manual-page: add a man page or suppress with
override_dh_missingif appropriate. - E: bad-distribution-in-changes-file: the changelog distribution name does not match a known suite—fix
debian/changelogwithdch -r. - W: standards-version-is-deprecated: update the Standards-Version field in
debian/control.
Aim for zero errors and zero warnings before moving to the chroot build. Pedantic tags are advisory but worth reviewing.
Step 8: Build in a Clean Chroot with sbuild
A local build uses your host's installed libraries, which can mask missing Build-Depends. sbuild builds inside a minimal chroot, matching what a Debian build server does.
Create the chroot
sudo sbuild-createchroot --include=eatmydata,ccache,gnupg \
noble /srv/chroot/noble-amd64 \
http://archive.ubuntu.com/ubuntu/
Replace noble with your target suite (bookworm for Debian 12). This takes a few minutes and requires internet access.
Run the chroot build
cd ~/packaging/hellocli-1.0
sbuild -d noble -j$(nproc) ../hellocli_1.0-1.dsc
Pass ../hellocli_1.0-1.dsc—the source control file produced by dpkg-buildpackage. If the .dsc is missing, generate it first:
dpkg-buildpackage -us -uc -S
A successful sbuild run produces .deb, .buildinfo, and .changes files. Run lintian again on the chroot-built .deb for a definitive result.
Verification
Install and smoke-test the package on a matching system:
sudo dpkg -i ../hellocli_1.0-1_amd64.deb
hellocli --version
apt-cache show hellocli
Check that dependencies resolve correctly by installing through apt if you have a local repo, or with gdebi which resolves deps for local .deb files:
sudo apt install gdebi-core
sudo gdebi ../hellocli_1.0-1_amd64.deb
Troubleshooting
- dpkg-source: error: ... about unexpected files: add them to
debian/.gitignoreordebian/source/optionswithextend-diff-ignore. - sbuild fails to resolve Build-Depends: the chroot's apt lists may be stale. Run
sudo sbuild-update -udcar nobleto refresh it. - dh_auto_install fails: your upstream Makefile's install target may not respect
DESTDIR. Overridedh_auto_installindebian/rulesand passDESTDIR=$(CURDIR)/debian/hellocliexplicitly. - Lintian E: debian-changelog-has-wrong-version: the version in
debian/changelogmust exactly match the version used in the.orig.tar.gzfilename.
Frequently asked questions
- What is the difference between the source package version and the Debian revision?
- The version string is split at the last hyphen: everything before it is the upstream version, everything after is the Debian revision. For '1.0-1', upstream is 1.0 and the Debian revision is 1. Increment the Debian revision when you change packaging without changing upstream source.
- Why use sbuild instead of just dpkg-buildpackage?
- dpkg-buildpackage uses your host's installed packages, so a missing Build-Depends will succeed locally but fail on a clean system. sbuild builds inside a minimal chroot that contains only essential packages plus your declared Build-Depends, catching dependency omissions before they reach users.
- Can I package a Python or Go program the same way?
- The debian/ directory structure is identical, but Build-Depends and the dh sequence change. For Python use dh-python and add python3-setuptools to Build-Depends. For Go use dh-golang. The ${shlibs:Depends} substitution variable is typically not needed for pure interpreted or statically linked packages.
- How do I handle a package that installs a systemd service?
- Place the unit file at debian/packagename.service. debhelper's dh_installsystemd helper picks it up automatically at compat level 13 and adds the correct postinst/prerm snippets to enable and start the service on install.
- What does lintian E: vs W: vs I: mean?
- E (error) indicates a policy violation or a definite bug that will likely cause installation or runtime failures. W (warning) is a probable issue worth fixing. I (info) and P (pedantic) are advisory. Packages uploaded to official Debian repos must have zero errors and ideally zero warnings.
Related guides
AI and Artificial-Life Tools on Linux
Set up open-source AI/ML and artificial-life toolkits on Linux: PyTorch, JAX, DEAP, Avida, NetLogo, and RL environments with GPU driver guidance.
Assembly Language on Linux: A Starter Guide
Write x86-64 assembly on Linux from scratch: install NASM and GAS, learn syscalls, assemble and link a working program, then inspect and debug it.
How to Benchmark Disk Performance with fio
Learn to benchmark Linux disk performance with fio: writing job files, testing latency and throughput, and interpreting IOPS and percentile output correctly.
The Linux Boot Process Explained
Trace the full Linux boot sequence from UEFI firmware through GRUB2, the kernel, initramfs, and systemd to your login prompt — with diagnostics at each stage.