Creating .deb packages for Debian and derivatives

Mascote do LinuxPro fechando com fita uma caixa marcada com a espiral do Debian na esteira de empacotamento

Distributing software for Debian, Ubuntu, Mint, and derivatives without a .deb is doing the job halfway: no dependency management, no clean upgrades, no removal without leftovers. The good news: creating a decent .deb package is simpler than its reputation suggests. In this complete guide you go from a handcrafted package with dpkg-deb to a professional package with debhelper, covering maintainer scripts, lintian, and your own APT repository.

Anatomy of a .deb

A .deb is just an ar archive with three members: debian-binary (format version), control.tar.zst (metadata and scripts), and data.tar.zst (the files to install, with their final paths). You can inspect any system package:

ar t /var/cache/apt/archives/htop_*.deb   # lista os 3 membros
dpkg-deb --info htop_*.deb                # metadados (control)
dpkg-deb --contents htop_*.deb            # arquivos e destinos

Method 1: a quick package with dpkg-deb

Perfect for distributing your own binary (a Go program, a script) inside the company. Let us package a fictional utility meuapp:

mkdir -p meuapp_1.0.0-1_amd64/{DEBIAN,usr/bin,usr/share/man/man1,etc/meuapp}
cp meuapp meuapp_1.0.0-1_amd64/usr/bin/
gzip -9 -c meuapp.1 > meuapp_1.0.0-1_amd64/usr/share/man/man1/meuapp.1.gz
cp meuapp.conf meuapp_1.0.0-1_amd64/etc/meuapp/

The heart of the package is DEBIAN/control:

cat > meuapp_1.0.0-1_amd64/DEBIAN/control << 'EOF'
Package: meuapp
Version: 1.0.0-1
Section: utils
Priority: optional
Architecture: amd64
Depends: libc6 (>= 2.34), ca-certificates
Maintainer: Nilton Oliveira 
Homepage: https://www.linuxpro.com.br
Description: Utilitário de exemplo do LinuxPro
 Linha longa da descrição: cada linha do corpo
 começa com um espaço. Uma linha só com " ." separa parágrafos.
EOF

Files in /etc deserve configuration treatment — declared in conffiles, dpkg preserves the admin's edits during upgrades:

echo "/etc/meuapp/meuapp.conf" > meuapp_1.0.0-1_amd64/DEBIAN/conffiles

Need to create a system user or enable a service? That is what maintainer scripts are for (preinst, postinst, prerm, postrm), all executable:

cat > meuapp_1.0.0-1_amd64/DEBIAN/postinst << 'EOF'
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
    getent passwd meuapp >/dev/null || useradd --system --no-create-home --shell /usr/sbin/nologin meuapp
fi
EOF
chmod 755 meuapp_1.0.0-1_amd64/DEBIAN/postinst

Build the package ( --root-owner-group eliminates the need for fakeroot) and test:

dpkg-deb --build --root-owner-group meuapp_1.0.0-1_amd64
sudo apt install ./meuapp_1.0.0-1_amd64.deb   # apt resolve as dependências
dpkg -L meuapp                                # confere o que foi instalado
sudo apt remove meuapp

Method 2: a professional package with debhelper

For a proper release — with a changelog, compilation from source, and Debian policy compliance — the approach is debhelper. Install the toolset:

sudo apt install build-essential devscripts debhelper dh-make lintian

At the root of your source code (e.g.: meuapp-1.0.0/):

cd meuapp-1.0.0
dh_make --single --native --packagename meuapp_1.0.0 --email nilton@example.com
rm debian/*.ex debian/*.EX   # limpa os exemplos

The generated debian/ directory has four essential files:

  • debian/control — source and binary metadata (Build-Depends here!)
  • debian/changelog — the version comes from here; edit with dch -i
  • debian/rules — the build makefile; with modern debhelper it is 4 lines
  • debian/copyright — license in DEP-5 format

A debian/rules file is typically just this today ( dh figures out the rest — Makefile, CMake, Go, Python…):

#!/usr/bin/make -f
%:
	dh $@

If upstream has no build system, debian/install maps files to destinations:

echo "meuapp usr/bin" > debian/install
echo "meuapp.conf etc/meuapp" >> debian/install

Have a systemd unit? Name it debian/meuapp.service and dh_installsystemd installs, enables, and restarts it automatically on upgrade. Build and validate:

dpkg-buildpackage -us -uc -b   # sem assinatura, só binário
lintian ../meuapp_1.0.0_amd64.changes   # o "linter" oficial da política Debian

Treat lintian warnings like a code review: each tag has an explanation with lintian-explain-tags <tag>.

Automatic dependencies

Never guess library versions. In the binary's debian/control , use the magic variables — dpkg-shlibdeps calculates the actual dependencies by analyzing ELF files:

Depends: ${shlibs:Depends}, ${misc:Depends}

Static Go/Rust binaries have almost no Depends — one of the reasons I package my tools in Go.

Versions and upgrades

The format is [epoch:]versão-upstream[-revisão-debian] — e.g.: 1.0.0-2 is the second packaging revision of the same 1.0.0 version. dpkg compares with dpkg --compare-versions; test if unsure:

dpkg --compare-versions 1.0.0-2 lt 1.0.1-1 && echo "upgrade ok"

Your own APT repository

Distributing standalone .deb downloads is half the journey — the rest is a repository so that apt upgrade works. The simple modern approach with reprepro:

sudo apt install reprepro
mkdir -p /srv/repo/conf
cat > /srv/repo/conf/distributions << 'EOF'
Codename: stable
Suite: stable
Components: main
Architectures: amd64 arm64
SignWith: SEU_KEY_ID_GPG
EOF
reprepro -b /srv/repo includedeb stable meuapp_1.0.0-1_amd64.deb

Serve /srv/repo with nginx and, on the client:

curl -fsSL https://repo.example.com/chave.gpg | sudo tee /usr/share/keyrings/meurepo.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/meurepo.gpg] https://repo.example.com stable main" | 
  sudo tee /etc/apt/sources.list.d/meurepo.list
sudo apt update && sudo apt install meuapp

Shortcut: fpm

For internal use, fpm creates a .deb (and .rpm) from a directory in one command — no Debian policy, but it gets the job done quickly:

gem install fpm
fpm -s dir -t deb -n meuapp -v 1.0.0 --description "Meu utilitário" 
  --depends ca-certificates meuapp=/usr/bin/meuapp

Practical summary

  • Internal tool? dpkg-deb --build or fpm — 10 minutes
  • Published software? debhelper + lintian clean + repository signed with reprepro
  • Configuration in /etc always in conffiles; system user in postinst; systemd unit through dh_installsystemd

In the next post of this pair: the same approach for the Red Hat family with .rpm packages — spec file, rpmbuild, mock, and createrepo.