
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-Dependshere!)debian/changelog— the version comes from here; edit withdch -idebian/rules— the build makefile; with modern debhelper it is 4 linesdebian/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 --buildorfpm— 10 minutes - Published software?
debhelper+lintianclean + repository signed withreprepro - Configuration in
/etcalways inconffiles; system user inpostinst; systemd unit throughdh_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.