🍺 Buy me a beer
📦

Pacchetti .deb per Svogliati

Anatomia, maintainer scripts, dpkg-deb, debhelper, dpkg-buildpackage e repository APT firmati. Tutto quello che serve per costruire un .deb nel 2026 senza ricorrere a Snap.

"Mando il software in produzione con un tar.gz e uno script di install che chiede sudo." — il modo peggiore di distribuire software su Debian, senza eccezioni.

01 / 12

Cos'è un pacchetto .deb

Un archivio di metadati e file da copiare. Inventato nel 1995, sopravvive a tutto.

📦 La definizione tecnica

Un .deb è un archivio Unix ar (non tar!) che contiene esattamente tre file: la versione del formato, i metadati (control), e i file da copiare sul filesystem (data). Punto.

L'estensione è .deb, il MIME type è application/vnd.debian.binary-package. Il formato è stato disegnato da Bdale Garbee e Ian Murdock nel 1995 (sì, esiste da prima di JavaScript) e è rimasto sostanzialmente identico da allora — quando un formato funziona, non si tocca.

📦

.deb

Debian, Ubuntu, Mint, Pop!_OS, Devuan, Kali, Raspberry Pi OS, Proxmox, ...

🏁

.rpm

RHEL, Fedora, openSUSE, Rocky, Alma. Stesso scopo, formato diverso.

🚀

Snap / Flatpak / AppImage

"Universali", pesano 200 MB per un editor di testo, montano squashfs ad ogni avvio.

🧠 L'analogia

Un .deb è una scatola IKEA: dentro trovi i pezzi (i file da copiare), il foglietto delle istruzioni (i maintainer scripts), e l'etichetta sul cartone (il control file). Il sistema operativo è il vostro garage: apt apre la scatola, segue le istruzioni, e i pezzi finiscono al posto giusto. Senza il .deb avete un mucchio di assi e tre viti, il che è quello che ./configure && make && sudo make install vi lascia da gestire a mano.

Perché pacchettizzare invece di un installer custom? Perché apt sa: risolvere dipendenze, fare upgrade atomici, riconoscere conffile modificati, fare downgrade, rimuovere pulito, gestire alternative. Tutto questo gratis, da 30 anni. Voi dovreste reimplementarlo? Davvero?
02 / 12

Anatomia di un .deb

Tre file dentro un archivio ar. Niente di più.

🔍 Aprire un .deb a mani nude

Vi mostro l'interno di un pacchetto qualsiasi:

ispezione di un .deb
$ ar t nginx_1.24.0-2_amd64.deb
debian-binary
control.tar.xz
data.tar.xz

# Estrazione completa per ispezione
$ mkdir extract && cd extract
$ ar x ../nginx_1.24.0-2_amd64.deb
$ ls -la
# debian-binary  control.tar.xz  data.tar.xz

$ cat debian-binary
2.0
# È un file di testo con DUE byte significativi: "2.0\n"

I tre membri spiegati

MembroCosa contieneFormat
debian-binaryVersione del formato. Sempre 2.0 da decenni.testo, 4 byte
control.tar.xzFile control + maintainer scripts + md5sums + conffilestar compresso (gz/xz/zst)
data.tar.xzI file veri da installare, con percorsi assoluti relativi a /tar compresso (gz/xz/zst)
⚠️ L'ordine conta. Per spec, debian-binary deve essere il primo membro dell'archivio ar. Se lo metti in fondo, alcune implementazioni di dpkg e parser custom rifiutano il pacchetto. dpkg-deb --build lo gestisce per te — rilevante solo se costruisci a mano con ar.

Dentro control.tar.xz

control.tar.xz/ ├── control # i metadati: nome, versione, deps, descrizione ├── md5sums # hash di ogni file in data.tar (per dpkg --verify) ├── conffiles # lista path in /etc/ trattati come "conf" (opzionale) ├── preinst # script: prima di unpack (opzionale) ├── postinst # script: dopo unpack (opzionale, quasi sempre presente) ├── prerm # script: prima di remove (opzionale) └── postrm # script: dopo remove (opzionale)

Dentro data.tar.xz

data.tar.xz/ ├── usr/ │ ├── bin/ │ │ └── myapp # → finirà in /usr/bin/myapp │ ├── lib/myapp/ │ │ └── runtime.so │ ├── share/ │ │ ├── doc/myapp/ │ │ │ ├── copyright │ │ │ └── changelog.Debian.gz │ │ └── man/man1/ │ │ └── myapp.1.gz │ └── lib/systemd/system/ │ └── myapp.service └── etc/myapp/ └── config.yaml # → protetto come conffile

I percorsi sono relativi alla root /dpkg esegue l'estrazione a /. Se nel tar metti ./usr/bin/myapp finisce in /usr/bin/myapp.

03 / 12

Il file control e le dipendenze

Il documento d'identità del pacchetto. Dove dichiari chi sei, cosa ti serve, con cosa litighi.

📝 Format RFC822-like

control è un file di testo con campi Chiave: valore, ispirato al formato delle email. I campi multi-riga indentano la riga successiva con uno spazio (oppure un . singolo per indicare paragrafo vuoto nelle descrizioni).

DEBIAN/control — esempio reale
Package: myapp
Version: 1.2.3-1
Architecture: amd64
Maintainer: Tuo Nome <[email protected]>
Installed-Size: 4567
Depends: python3 (>= 3.10), nginx (>= 1.18), libssl3
Recommends: postgresql-client
Suggests: redis-server
Conflicts: myapp-legacy
Replaces: myapp-legacy
Provides: webapp-runtime
Section: web
Priority: optional
Homepage: https://myapp.example.com
Description: Web dashboard per qualcosa di utile
 Riga lunga con un paragrafo iniziale che riassume il pacchetto.
 .
 Secondo paragrafo (il punto su una riga indica riga vuota).
 Caratteri di continuazione sempre con uno spazio iniziale.

Campi obbligatori

CampoNote
PackageNome del pacchetto. Lowercase, niente spazi, alfanumerico + -+.
VersionVersione (vedi sezione 11)
Architectureamd64, arm64, i386, armhf, all (no-arch), any
MaintainerNome <email>. Anche se sei solo, scrivilo: chi debugga vuole sapere chi chiamare
DescriptionUna riga short + righe extended indentate

Le relazioni: chi dipende da chi

CampoSignificatoQuando usarlo
DependsHard requirementSe manca, apt rifiuta install
Pre-DependsDeve essere configurato prima dell'unpackSolo per cose come libc6
Recommends"Quasi sempre vuoi anche questo"Installato di default, rimovibile
Suggests"Potrebbe interessarti"Solo menzionato
Enhances"Migliora questo altro pacchetto"Inverso di Suggests
ConflictsNon puoi avere entrambi installatiApt sceglie cosa rimuovere
BreaksConflicts ma upgrade-friendly"<= versione X di X" rotta
Replaces"Sono io il successore di X"Permette di sovrascrivere file
ProvidesPacchetto virtualeEs. mail-transport-agent
💡 Operatori di versione per Depends: (>> 1.0) strettamente maggiore, (>= 1.0) maggiore o uguale, (= 1.0) esatto, (<< 2.0) strettamente minore, (<= 2.0) minore o uguale. Combinabili con virgole (AND) e pipe (OR): nginx | apache2 (>= 2.4).

Section, Priority, Essential

Section: categoria archivistica. Esempi: admin, devel, net, web, utils, libs, python, doc, misc. Se distribuisci fuori dall'archivio Debian ufficiale, è quasi cosmetico.

Priority: required (es. libc6, non puoi rimuoverlo), important, standard (parte dell'install minimale), optional (default per i tuoi pacchetti), extra (deprecato, usa optional).

Essential: yes: il pacchetto non si può rimuovere senza --force-remove-essential. Riservato a libc6, bash, coreutils. Non usarlo per app vostre, mai.

04 / 12

Filesystem Hierarchy Standard

Dove vanno i tuoi file: la convenzione che lintian ti farà rispettare anche se non vuoi.

📁 Le regole d'oro

Ogni file in un .deb deve andare al posto giusto, secondo il Filesystem Hierarchy Standard. Mettere un binario in /opt/myapp/bin/ è tecnicamente legale, ma lintian ti dirà che fai schifo, e i package reviewer ti rispediranno il pacchetto.

PathPer cosa
/usr/bin/Eseguibili per utenti normali
/usr/sbin/Eseguibili per admin (richiedono root)
/usr/lib/<pkg>/Librerie private del tuo pacchetto, supporto runtime
/usr/lib/<arch-triplet>/Librerie condivise multi-arch (es. x86_64-linux-gnu)
/usr/share/<pkg>/Dati arch-independent (template, asset, traduzioni)
/usr/share/doc/<pkg>/Obbligatorio: copyright + changelog.Debian.gz
/usr/share/man/man[1-9]/Man page (gzippate, lintian si arrabbia se non lo sono)
/etc/<pkg>/Configurazioni utente. Da listare in conffiles
/var/lib/<pkg>/Stato persistente che il pacchetto modifica
/var/log/<pkg>/Log (rotation via /etc/logrotate.d/)
/var/cache/<pkg>/Cache rigenerabili (purgable senza perdita dati)
/srv/Dati di servizio "site-specific" (web roots, mail, ...)
/lib/systemd/system/Unit files systemd
/etc/systemd/system/NO. Quello è per overrides admin.
/opt/<pkg>/Solo per software third-party non-distribuito (Chrome, Slack, ...)

Cosa fare

  • Eseguibili in /usr/bin o /usr/sbin
  • Asset statici in /usr/share/<pkg>/
  • Configurazioni in /etc/<pkg>/ + conffiles
  • Stato in /var/lib/<pkg>/
  • Unit systemd in /lib/systemd/system/
  • Documentazione in /usr/share/doc/<pkg>/

Cosa non fare

  • Mettere tutto in /opt/<pkg>/ "perché così è isolato"
  • Scrivere in /etc/systemd/system/ (è per gli admin)
  • Lasciare file world-writable (mode 0666 / 0777)
  • Installare in /root/ o /home/
  • Spedire .git/, __pycache__/, node_modules/ dentro il .deb
  • Mettere changelog non gzippato (lintian errore: changelog-not-compressed)
Test rapido: lintian mypkg.deb — ti elenca tutto ciò che hai sbagliato vs. Debian Policy. Da E: (errori) a W: (warning) a I: (info). Punta a zero errori; i warning li valuti caso per caso.
05 / 12

Maintainer scripts

I quattro script che dpkg chiama nei momenti giusti. Devono essere idempotenti e gestire tutti i casi.

⚙️ Il flusso completo

Ogni operazione su un pacchetto chiama gli script in ordine prevedibile. Per un'installazione fresca:

  1. preinst install
  2. (unpack di data.tar)
  3. postinst configure

Per un upgrade:

  1. prerm upgrade <new-version> (sul vecchio)
  2. preinst upgrade <old-version> (sul nuovo)
  3. (unpack del nuovo, rimozione vecchi file)
  4. postrm upgrade <new-version> (sul vecchio)
  5. postinst configure <old-version> (sul nuovo)

Per remove / purge:

  1. prerm remove
  2. (rimozione file)
  3. postrm remove (per remove)
  4. postrm purge (per purge: rimuove anche conffile e dati)

Esempio: postinst tipico

DEBIAN/postinst
#!/bin/sh
set -e

case "$1" in
    configure)
        # 1. Crea utente di sistema (se non esiste)
        if ! getent passwd myapp >/dev/null; then
            adduser --system --group \
                --home /var/lib/myapp \
                --no-create-home \
                --shell /usr/sbin/nologin \
                myapp
        fi

        # 2. Crea directory di stato con i permessi giusti
        install -d -m 0750 -o myapp -g myapp /var/lib/myapp
        install -d -m 0750 -o myapp -g myapp /var/log/myapp

        # 3. Genera secret se non esiste (idempotente!)
        if [ ! -f /etc/myapp/secret ]; then
            head -c 32 /dev/urandom | base64 > /etc/myapp/secret
            chmod 0640 /etc/myapp/secret
            chown root:myapp /etc/myapp/secret
        fi

        # 4. systemd: enable + (re)start
        deb-systemd-helper enable myapp.service >/dev/null || true
        if [ -d /run/systemd/system ]; then
            systemctl daemon-reload >/dev/null || true
            deb-systemd-invoke restart myapp.service >/dev/null || true
        fi
        ;;

    abort-upgrade|abort-remove|abort-deconfigure)
        # Niente da fare; un altro script gestirà il rollback
        ;;

    *)
        echo "postinst chiamato con argomento ignoto: $1" >&2
        exit 1
        ;;
esac

exit 0

Esempio: postrm tipico

DEBIAN/postrm
#!/bin/sh
set -e

case "$1" in
    purge)
        # Solo a purge: rimuovi conffile, dati, utente
        rm -rf /var/lib/myapp /var/log/myapp /etc/myapp
        if getent passwd myapp >/dev/null; then
            deluser --quiet myapp 2>/dev/null || true
        fi
        deb-systemd-helper purge myapp.service >/dev/null || true
        ;;

    remove)
        # Stop senza purge: lascia conffile e dati
        deb-systemd-helper mask myapp.service >/dev/null || true
        ;;

    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
        ;;

    *)
        echo "postrm chiamato con argomento ignoto: $1" >&2
        exit 1
        ;;
esac

exit 0
⚠️ Idempotenza obbligatoria. Gli script possono essere chiamati più volte (es. dopo un fallimento). adduser deve essere preceduto da if ! getent passwd; install -d è sicuro per directory che esistono già; systemctl deve essere wrappato con || true nel caso non sia in esecuzione (chroot, container).
📝 Permessi script. I 4 script devono essere eseguibili (chmod 0755) e iniziare con shebang (#!/bin/sh o #!/bin/bash). set -e sempre — un errore non gestito deve far fallire l'install/upgrade, non procedere silenziosamente.

Conffile: protezione di /etc/

I file listati in DEBIAN/conffiles ricevono trattamento speciale:

  • dpkg calcola l'md5 al primo install
  • Durante upgrade, se l'utente ha modificato il file e il maintainer ha cambiato il default, ti chiede cosa fare (keep / replace / show diff / 3-way merge)
  • A purge, vengono rimossi (a remove normale, restano)

Il file conffiles elenca un path assoluto per riga:

DEBIAN/conffiles
/etc/myapp/config.yaml
/etc/myapp/logging.conf
/etc/logrotate.d/myapp
/etc/cron.daily/myapp-cleanup
06 / 12

dpkg-deb — il modo manuale

Costruire un .deb senza debhelper, partendo da una directory. Per capire cosa succede sotto.

🔧 Quando usare il modo manuale

Il modo "Debian-canonico" (debhelper, sezione 07) è più corretto e portatile, ma quando vuoi capire cosa fa conviene partire da dpkg-deb --build: ti mostra la corrispondenza diretta tra una directory tree e un .deb, senza magia.

Va benissimo anche per: pacchetti privati interni, build script bash custom, tooling cross-platform, distribuzione di binari precompilati Go/Rust senza source.

Step 1: la directory tree

myapp_1.0.0/ ├── DEBIAN/ # maiuscolo, è convenzione tassativa │ ├── control │ ├── postinst # chmod 0755 │ ├── postrm # chmod 0755 │ └── conffiles ├── usr/ │ ├── bin/ │ │ └── myapp # chmod 0755 │ ├── share/doc/myapp/ │ │ ├── copyright │ │ └── changelog.Debian.gz │ └── lib/systemd/system/ │ └── myapp.service └── etc/myapp/ └── config.yaml

Step 2: DEBIAN/control

myapp_1.0.0/DEBIAN/control
Package: myapp
Version: 1.0.0-1
Section: web
Priority: optional
Architecture: amd64
Depends: python3 (>= 3.10), python3-flask
Maintainer: Selif Dev <[email protected]>
Description: Web app per fare cose
 Una piccola web app Flask che serve come esempio.
 Non fa niente di utile, ma fa qualcosa.

Step 3: build

shell
$ dpkg-deb --build --root-owner-group myapp_1.0.0
dpkg-deb: building package 'myapp' in 'myapp_1.0.0.deb'.

# Convenzione di naming:
$ mv myapp_1.0.0.deb myapp_1.0.0-1_amd64.deb
💡 --root-owner-group è cruciale. Senza, ogni file dentro il .deb ha owner = il tuo utente (es. selif:selif) e a install diventerà quello (errore di sicurezza). Con il flag, tutto è root:root — che è quello che vuoi sempre per i binari.

Step 4: ispeziona prima di installare

verifiche pre-install
# Lista file (con permessi e owner)
$ dpkg-deb -c myapp_1.0.0-1_amd64.deb

# Mostra il control
$ dpkg-deb -I myapp_1.0.0-1_amd64.deb

# Estrai data in /tmp/x
$ dpkg-deb -x myapp_1.0.0-1_amd64.deb /tmp/x

# Estrai control in /tmp/c
$ dpkg-deb -e myapp_1.0.0-1_amd64.deb /tmp/c

# Lint check (richiede "apt install lintian")
$ lintian myapp_1.0.0-1_amd64.deb

# Install + test
$ sudo apt install ./myapp_1.0.0-1_amd64.deb
# NB: "./" obbligatorio per dirgli "file locale, non repo"
⚠️ Compressione. Default: xz. Per pacchetti grandi puoi forzare zstd (decompressione molto più veloce) con dpkg-deb -Zzstd --build .... Per max compatibility: -Zgzip (formato anni '90, supportato ovunque, decompressione lenta).
07 / 12

debhelper + dh — il modo Debian

Quando vuoi un pacchetto "vero", quello che potrebbe finire nell'archivio Debian.

⚙️ Il salto di paradigma

Con dpkg-deb tu costruisci una directory tree finale a mano. Con debhelper dichiari cosa vuoi nel pacchetto e dh ricostruisce tutto a partire dal source: compila, installa, comprime, gzippa changelog, byte-compile Python, registra man page, configura systemd, e così via.

È il modo standard di Debian. Più ripido all'inizio, infinitamente meno faticoso quando il pacchetto cresce.

Layout source con debian/

myapp-1.0.0/ # source upstream (codice + Makefile) ├── myapp.py ├── requirements.txt ├── Makefile └── debian/ # <-- la directory di packaging ├── changelog # cronologia versioni (formato strict) ├── control # source + binary packages ├── copyright # formato DEP-5 ├── rules # Makefile chiamato da dpkg-buildpackage ├── install # mappature: "src dst" per riga ├── postinst # maintainer scripts (uguale a DEBIAN/) ├── postrm ├── myapp.service # systemd unit (dh la copia in lib/systemd/) ├── myapp.1 # man page (dh la gzippa) └── source/format # "3.0 (quilt)" o "3.0 (native)"

Il debian/control (source-style)

debian/control
Source: myapp
Section: web
Priority: optional
Maintainer: Selif Dev <[email protected]>
Build-Depends: debhelper-compat (= 13), dh-python, python3-all
Standards-Version: 4.6.2
Homepage: https://myapp.example.com
Rules-Requires-Root: no

Package: myapp
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends}, python3-flask
Description: Web app per fare cose
 Una piccola web app Flask...

Nota: nei sostituti automatici (${misc:Depends}, ${python3:Depends}) dh inietta in build-time le dipendenze rilevate — non scrivere a mano libc6 o version constraint.

Il debian/rules minimale

debian/rules
#!/usr/bin/make -f
# Tutto il vero lavoro lo fa dh, queste 3 righe sono il pacchetto completo.

%:
	dh $@

Sì, sono 3 righe. dh $@ espande nelle 30+ chiamate giuste a dh_install, dh_compress, dh_strip, dh_systemd_enable, dh_python3, dh_md5sums, dh_builddeb, ... a seconda dei build-deps che hai dichiarato.

Quando ti serve override, lo fai per singolo step:

debian/rules con override
#!/usr/bin/make -f

%:
	dh $@ --with python3 --buildsystem=pybuild

override_dh_auto_test:
	# Salta i test in build (tipico se servono rete/db)

override_dh_install:
	dh_install
	# Crea directory di stato vuota
	install -d -m 0750 debian/myapp/var/lib/myapp

Il debian/install

Lista sorgente destinazione, una mappatura per riga. Quello che vuoi nel .deb finale, partendo dalla source tree.

debian/install
myapp.py            usr/lib/myapp/
bin/myapp           usr/bin/
config.yaml         etc/myapp/
templates/          usr/share/myapp/
🧩 File con nomi convenzionali in debian/ sono auto-rilevati: debian/<pkg>.service → va in /lib/systemd/system/; debian/<pkg>.1 → man page in sezione 1; debian/<pkg>.cron.daily/etc/cron.daily/<pkg>; debian/<pkg>.logrotate/etc/logrotate.d/<pkg>. Niente boilerplate.
08 / 12

changelog, copyright e gli altri file pignoli

Il formato è rigido. Sgarrare significa dpkg-buildpackage che si rifiuta di costruire.

debian/changelog — il formato è sacro

debian/changelog
myapp (1.2.0-1) unstable; urgency=medium

  * Nuova feature: export PDF dei report.
  * Bugfix: timeout del client SMTP elevato a 30s.
  * Refactoring del modulo auth.

 -- Selif Dev <[email protected]>  Wed, 30 Apr 2026 14:23:01 +0200

myapp (1.1.5-1) unstable; urgency=high

  * Security fix: CVE-2026-12345 (XSS nella pagina /search).

 -- Selif Dev <[email protected]>  Mon, 22 Apr 2026 09:00:00 +0200
🚫 Le regole sintattiche: prima riga nome (versione) suite; urgency=...; corpo con bullet che iniziano con * (due spazi + asterisco + spazio); riga firma -- Nome <email> Data RFC2822 (un solo spazio iniziale, due spazi tra email e data). Sgarrare significa errore. Usa dch o git-dch per generare entry — il formato lo fa lui.
generare changelog con dch
# Prima entry (crea il file)
$ dch --create --package myapp --newversion 1.0.0-1 \
       "Initial release."

# Nuova versione (incrementa Debian revision)
$ dch -i "Bugfix nel modulo X."

# Nuova versione upstream
$ dch -v 1.2.0-1 "New upstream release"

# Marca pronta per release (cambia "UNRELEASED" in "unstable")
$ dch -r ""

debian/copyright — formato DEP-5

debian/copyright
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: myapp
Upstream-Contact: Selif Dev <[email protected]>
Source: https://git.example.com/myapp

Files: *
Copyright: 2026 Selif Dev <[email protected]>
License: MIT

Files: debian/*
Copyright: 2026 Selif Dev <[email protected]>
License: MIT

License: MIT
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 "Software"), to deal in the Software without restriction, including
 without limitation the rights to use, copy, modify, merge, publish,
 ...

debian/source/format

Una sola riga, scegli tra:

  • 3.0 (native) — il pacchetto nasce per Debian, non c'è un upstream separato. Tipico per i tuoi tool interni.
  • 3.0 (quilt) — pacchetti che hanno un upstream esterno (es. nginx, redis) e si applicano patch su quello. Standard per archivio Debian.

debian/compat (legacy) o Build-Depends

Vecchio modo: file debian/compat con un numero (es. 13). Modo moderno: dichiara debhelper-compat (= 13) in Build-Depends dentro debian/control. Mai entrambi.

Compat 13 è il default ragionevole su Debian 12+ (bookworm) e Debian 13 (trixie).

09 / 12

Costruire il pacchetto

Da source con debian/ a .deb finito. Locale, in chroot, in CI.

Build locale veloce

build locale
# Prerequisiti: una volta sola
$ sudo apt install build-essential debhelper devscripts dh-make lintian

# Dentro la directory source (con debian/ presente)
$ cd myapp-1.0.0/

# Installa build-deps dichiarate in debian/control
$ sudo apt build-dep .
# equivalente a: leggi Build-Depends, apt install ognuno

# Build (binary only, senza firma)
$ dpkg-buildpackage -us -uc -b
#   -us: don't sign source
#   -uc: don't sign .changes
#   -b:  binary only (no source tarball)

# Output: tutto nella parent directory
$ ls ../
myapp-1.0.0/
myapp_1.0.0-1_amd64.deb         # <-- ecco il tuo .deb
myapp_1.0.0-1_amd64.buildinfo
myapp_1.0.0-1_amd64.changes
🔎 Sempre lintian dopo build. $ lintian ../myapp_1.0.0-1_amd64.deb ti elenca tutto ciò che viola la Debian Policy. Per release interne tollera i W:, mai gli E:.

Build pulita in chroot (pbuilder)

Il problema della build "locale" è che dipende dallo stato della tua macchina — pacchetti installati, env vars, versioni. pbuilder (o sbuild) crea un chroot Debian minimale, installa solo i build-deps dichiarati, fa la build dentro, restituisce il .deb. Così sai che il pacchetto si costruisce davvero da zero su una macchina pulita.

pbuilder workflow
$ sudo apt install pbuilder

# Setup: crea base chroot (una volta per distro target)
$ sudo pbuilder create --distribution trixie \
                       --basetgz /var/cache/pbuilder/trixie-base.tgz

# Build di un pacchetto (a partire dal .dsc generato da dpkg-buildpackage -S)
$ dpkg-buildpackage -S -us -uc          # source package → .dsc
$ sudo pbuilder build ../myapp_1.0.0-1.dsc \
                       --basetgz /var/cache/pbuilder/trixie-base.tgz

# Output in /var/cache/pbuilder/result/
$ ls /var/cache/pbuilder/result/
myapp_1.0.0-1_amd64.deb
myapp_1.0.0-1_amd64.changes

Build in CI (GitHub Actions / Gitea)

.gitea/workflows/build-deb.yml
name: build-deb

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        distro: [bookworm, trixie]
    container: debian:${{ matrix.distro }}
    steps:
      - uses: actions/checkout@v4
      - name: Install build prereqs
        run: |
          apt update
          apt install -y build-essential debhelper devscripts lintian
          apt build-dep -y .
      - name: Build .deb
        run: dpkg-buildpackage -us -uc -b
      - name: Lintian check
        run: lintian ../*.deb || true
      - uses: actions/upload-artifact@v3
        with:
          name: deb-${{ matrix.distro }}
          path: ../*.deb

Test di install in chroot (piuparts)

piuparts = "package installation, upgrading and removal testing suite". Esegue install → remove → install → upgrade → purge in un chroot fresco e cattura ogni anomalia (file lasciati in giro a purge, errori in script, file non posseduti).

piuparts
$ sudo apt install piuparts
$ sudo piuparts -d trixie myapp_1.0.0-1_amd64.deb
# Esegue install/remove/install/purge ciclo completo
# PASS = nessuna sorpresa
10 / 12

Repository APT firmato

Per distribuire i tuoi .deb con apt install <pkg>, non con curl | bash.

🔒 Cos'è un repository APT

Un sito statico con una struttura precisa di file e una firma GPG. Niente daemon, niente database. Può vivere su Cloudflare Pages, GitHub Pages, S3, MinIO, Nginx static, Apache — qualsiasi cosa serva file via HTTP/HTTPS.

La struttura standard

repo.example.com/ ├── dists/ │ └── trixie/ # nome della suite (= codename Debian) │ ├── Release # indice generale + checksum │ ├── Release.gpg # firma detached │ ├── InRelease # Release con firma inline (preferito) │ └── main/ # componente │ ├── binary-amd64/ │ │ ├── Packages │ │ ├── Packages.gz │ │ └── Release │ └── binary-arm64/ │ ├── Packages │ ├── Packages.gz │ └── Release └── pool/ └── main/ └── m/myapp/ # prima lettera + nome ├── myapp_1.0.0-1_amd64.deb └── myapp_1.0.0-1_arm64.deb

Costruirlo con reprepro

setup reprepro
$ sudo apt install reprepro

$ mkdir -p ~/myrepo/conf
$ cat > ~/myrepo/conf/distributions <<EOF
Origin: Selif
Label: Selif Repo
Codename: trixie
Suite: stable
Architectures: amd64 arm64 source
Components: main
Description: Selif personal package repository
SignWith: ABCDEF1234567890     # GPG key ID
EOF

# Aggiungi un .deb al repo
$ cd ~/myrepo
$ reprepro includedeb trixie /path/to/myapp_1.0.0-1_amd64.deb

# Lista pacchetti
$ reprepro list trixie

# Rimuovi
$ reprepro remove trixie myapp

A questo punto ~/myrepo/dists/ e ~/myrepo/pool/ sono pronti. Pubblicali con qualsiasi static hosting (rsync su VPS, push a Cloudflare Pages, sync a S3).

Generare la chiave GPG

chiave GPG per firma repo
$ gpg --batch --quick-gen-key "Selif Repo <[email protected]>" rsa4096

$ gpg --list-keys
# Cerca la riga "pub": ABCDEF1234567890... <-- key ID

# Esporta la chiave pubblica (questa la pubblichi)
$ gpg --armor --export ABCDEF1234567890 > selif-repo.gpg

# È un file di testo, lo metti su https://repo.example.com/gpg

Lato client: aggiungere il tuo repo

setup repo lato utente (Debian 12+)
# 1. Scarica la chiave pubblica
$ sudo install -d -m 0755 /etc/apt/keyrings
$ curl -fsSL https://repo.example.com/gpg \
    | sudo tee /etc/apt/keyrings/selif.asc > /dev/null

# 2. Aggiungi la sources list (sintassi nuova: signed-by inline)
$ echo "deb [signed-by=/etc/apt/keyrings/selif.asc] https://repo.example.com/debian trixie main" \
    | sudo tee /etc/apt/sources.list.d/selif.list

# 3. Aggiorna e installa
$ sudo apt update
$ sudo apt install myapp
⚠️ Mai più apt-key add. Il vecchio modo (apt-key add - < gpg.key) è deprecato dal 2022 e rimosso da Debian 12. La chiave deve essere in /etc/apt/keyrings/ o /etc/apt/trusted.gpg.d/, e referenziata con signed-by= nella sources list. Le guide vecchie su Stack Overflow ti faranno installare repo non firmati.

Alternative al self-hosted

☁️

Cloudsmith

Repo APT/RPM/PyPI/Docker. Free tier piccolo, paid scalabile.

🌐

Packagecloud

Storico, multi-format. Piano gratis per progetti open source.

📁

GitHub Releases

Hostare i .deb come release asset; non è un repo APT vero, l'utente fa wget && dpkg -i.

11 / 12

Versioning e upgrades

Il formato versione di Debian. Più ricco di SemVer, con regole sottili che è meglio conoscere.

📊 Il formato

Una versione Debian ha forma:

[epoch:]upstream_version[-debian_revision]

  • epoch — intero, raro. Si usa quando la nuova upstream va "indietro" nel formato (es. 2026.04 → 1.0.0 richiede 1:1.0.0 per essere considerata più recente)
  • upstream_version — la versione del software (es. 1.2.3)
  • debian_revision — quante volte il packaging è stato modificato per la stessa upstream (es. 1.2.3-1, 1.2.3-2, 1.2.3-3)

Esempi

VersioneCosa significa
1.0.0-1Prima release Debian del software 1.0.0
1.0.0-2Stesso codice, packaging cambiato (es. fix in postinst)
1.0.1-1Nuova upstream, primo packaging
2:1.0.0-1Epoch 2 (versione "logicamente posteriore" anche se il numero è minore)
1.0.0~rc1-1Pre-release: ~ significa "minore di"
1.0.0+dfsg1-1Upstream ripulito da file non-DFSG (es. binari proprietari rimossi)
1.0.0~git20260430.abcdef-1Snapshot git tra release ufficiali
1.0.0-1ubuntu1Convenzione Ubuntu: derivato da Debian 1.0.0-1, prima patch Ubuntu
🍌 La tilde ~ è magica. Comparando versioni, ~ è sempre minore di qualunque altro carattere, persino della stringa vuota. Quindi 1.0.0~rc1 < 1.0.0~rc2 < 1.0.0. Senza tilde, 1.0.0rc1 > 1.0.0 (il rc1 è > vuoto), che inverte la semantica: gli RC sembrerebbero "successivi" alla release. La tilde risolve esattamente questo.

Confrontare versioni

dpkg --compare-versions
$ dpkg --compare-versions 1.0.0~rc1 lt 1.0.0 && echo yes
yes

$ dpkg --compare-versions 1.0.0-1 lt 1.0.0-2 && echo yes
yes

$ dpkg --compare-versions 2:0.5 gt 1:99.9 && echo yes
yes
# Epoch 2 batte epoch 1 anche se i numeri parlano contrario

$ dpkg --compare-versions 1.0.0+dfsg1-1 gt 1.0.0-1 && echo yes
yes

Conffile durante upgrade

Quando aggiorni un pacchetto e un file in conffiles è cambiato sia upstream che dall'utente, dpkg presenta un prompt:

Configuration file '/etc/myapp/config.yaml'
 ==> Modified (by you or by a script) since installation.
 ==> Package distributor has shipped an updated version.
   What would you like to do about it ?  Your options are:
    Y or I  : install the package maintainer's version
    N or O  : keep your currently-installed version
      D     : show the differences between the versions
      Z     : start a shell to examine the situation
 The default action is to keep your current version.
*** config.yaml (Y/I/N/O/D/Z) [default=N] ?

Per non avere prompt in script automatici (Ansible, cloud-init):

upgrade non-interattivo
$ sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
       -o Dpkg::Options::="--force-confold"
#                              ^ tieni la versione locale
# Alternative:
#   --force-confnew  → sovrascrivi con la nuova maintainer
#   --force-confdef  → usa default (rispetta scelte già fatte)
12 / 12

Cheat sheet

I comandi che ti serviranno davvero, in una pagina da tenere aperta.

Ispezionare un .deb

inspect existing .deb
$ dpkg-deb -I pkg.deb              # control + info
$ dpkg-deb -c pkg.deb              # lista file con permessi
$ dpkg-deb -e pkg.deb /tmp/ctrl/   # estrai control + scripts
$ dpkg-deb -x pkg.deb /tmp/data/   # estrai i file (data)

$ ar t pkg.deb                     # i 3 membri ar
$ ar x pkg.deb                     # estrai i 3 membri

Pacchetti già installati

installed packages
$ dpkg -l                          # tutti i pacchetti
$ dpkg -l nginx                    # info su uno specifico
$ dpkg -L nginx                    # tutti i file installati da nginx
$ dpkg -s nginx                    # status + control
$ dpkg -S /etc/nginx/nginx.conf   # quale pkg possiede questo file?
$ dpkg --verify nginx              # confronta md5sums vs filesystem

$ apt-cache depends nginx          # di cosa nginx ha bisogno
$ apt-cache rdepends nginx         # chi dipende da nginx
$ apt-cache policy nginx           # versione installata + candidate + sorgenti

$ apt-mark hold nginx              # blocca upgrade automatici
$ apt-mark unhold nginx            # sblocca
$ apt-mark showhold                # lista pacchetti held

Build manuale rapido

quick manual build
$ mkdir -p mypkg/DEBIAN mypkg/usr/bin

$ cat > mypkg/DEBIAN/control <<EOF
Package: mypkg
Version: 1.0.0-1
Section: utils
Priority: optional
Architecture: all
Maintainer: Me <[email protected]>
Description: Test package
EOF

$ cp myapp mypkg/usr/bin/
$ chmod 0755 mypkg/usr/bin/myapp

$ dpkg-deb --build --root-owner-group mypkg
$ mv mypkg.deb mypkg_1.0.0-1_all.deb

$ sudo apt install ./mypkg_1.0.0-1_all.deb

Build "Debian way"

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

$ cd mypackage-1.0.0/

# Genera scheletro debian/ (interattivo)
$ dh_make --native --single --packagename mypackage_1.0.0

# Edita debian/{control,changelog,rules,copyright}

$ sudo apt build-dep .
$ dpkg-buildpackage -us -uc -b
$ lintian ../mypackage_1.0.0-1_amd64.deb

Test prima di pubblicare

pre-publish testing
$ lintian pkg.deb                   # style/policy check
$ sudo piuparts -d trixie pkg.deb   # install/remove/upgrade in chroot

# Install di test in container Docker
$ docker run --rm -v $PWD:/p debian:trixie \
    sh -c "apt update && apt install -y /p/pkg.deb && /p/test.sh"

Repo APT locale veloce

reprepro setup
$ sudo apt install reprepro
$ mkdir -p ~/myrepo/conf
$ cat > ~/myrepo/conf/distributions <<EOF
Origin: Me
Label: My Repo
Codename: trixie
Architectures: amd64 arm64 source
Components: main
SignWith: ABCDEF1234567890
EOF

$ cd ~/myrepo
$ reprepro includedeb trixie /path/to/pkg.deb
$ reprepro list trixie

# Ora ~/myrepo/dists e ~/myrepo/pool sono pronti
# Pubblica con: rsync -a ~/myrepo/ user@host:/var/www/repo/

Upgrade non-interattivo (per script)

unattended upgrade
$ sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y \
       -o Dpkg::Options::="--force-confold" \
       -o Dpkg::Options::="--force-confdef"
📚 Documentazione canonica (per quando vuoi davvero studiare): Debian Policy Manual, Debian New Maintainers' Guide, Guide for Debian Maintainers. Sono lunghe, ma sono la verità. Wikis e blog di terze parti sono spesso vecchi (vedi apt-key add).
🔗 Guide collegate: Linux Admin (le basi prima di pacchettizzare), Ansible (per distribuire i tuoi .deb a fleet), arx · nomina · missus (esempi reali di pacchetti .deb per panel web).