Software Development Simplified

The Ultimate Guide to Creating Debian Packages

By Dario on Apr 30, 2025
Debian Packaging Process

The Ultimate Guide to Creating Debian Packages

If you’re a developer who works with Linux systems, particularly Debian-based distributions, creating packages for your software is a powerful skill. Debian packages provide a standardized way to distribute applications, making installation, upgrading, and removal seamless for users. However, the packaging process can seem intimidating with its specific file structure and conventions.

In this comprehensive guide, I’ll walk you through creating production-ready Debian packages, including how to target multiple distributions efficiently using Docker. Rather than covering theoretical examples, I’ll share real automation techniques from my own projects like uv-debian and lazygit-debian.

Why Create Debian Packages?

Before diving into the technical details, let’s understand why Debian packages are worth the effort:

  1. Simplified installation - Users can install your software with a simple apt install command
  2. Dependency management - Automatically handle prerequisite packages
  3. Clean upgrades and removals - Provide a consistent experience for package lifecycle
  4. System integration - Properly register services, documentation, and configuration files
  5. Distribution channel - Enable distribution through official or custom repositories

Understanding the Debian Package Structure

A Debian package (.deb file) is essentially an archive containing your application files and metadata. The basic structure includes:

package-name-version/
├── DEBIAN/
│   ├── control       # Package metadata
│   ├── preinst       # Pre-installation script (optional)
│   ├── postinst      # Post-installation script (optional)
│   ├── prerm         # Pre-removal script (optional)
│   └── postrm        # Post-removal script (optional)
└── usr/
    ├── bin/          # Executable files
    ├── lib/          # Library files
    ├── share/
    │   ├── doc/      # Documentation
    │   └── man/      # Man pages
    └── ...           # Other directories following FHS

Essential Files Explained

The most critical file is DEBIAN/control, which contains package metadata. Here’s an example from my uv package:

Source: uv
Section: utils
Priority: optional
Maintainer: Dario Griffo <dariogriffo@gmail.com>
Homepage: https://github.com/astral-sh/uv
Package: uv
Version: 0.7.0-1+bookworm
Architecture: amd64
Depends: 
Description: An extremely fast Python package and project manager, written in Rust.

Let’s break down the key fields:

  • Source: The name of the source package
  • Section: The package category (common values include ‘utils’, ‘net’, ‘devel’)
  • Priority: Usually ‘optional’ for most third-party packages
  • Maintainer: Your name and email
  • Package: The package name (often the same as Source)
  • Version: Following the format [upstream-version]-[debian-revision]+[distribution]
  • Architecture: Target architecture (‘amd64’, ‘arm64’, ‘all’, etc.)
  • Depends: Required packages for your software to run
  • Description: A brief description followed by a longer multi-line description

Creating a Basic Debian Package Manually

Let’s create a package for lazygit, a terminal UI for Git commands:

  1. Create the package directory structure:
mkdir -p lazygit-0.40.0/DEBIAN
mkdir -p lazygit-0.40.0/usr/bin
mkdir -p lazygit-0.40.0/usr/share/doc/lazygit
  1. Create the control file:
cat > lazygit-0.40.0/DEBIAN/control << EOF
Source: lazygit
Section: utils
Priority: optional
Maintainer: Dario Griffo <dariogriffo@gmail.com>
Homepage: https://github.com/jesseduffield/lazygit
Package: lazygit
Version: 0.40.0-1
Architecture: amd64
Depends: git
Description: Simple terminal UI for git commands
 A simple terminal UI for git commands, written in Go with the gocui library.
 Main features: Easily manage your git workflow from a terminal-based UI.
EOF
  1. Add executable file: Download the binary from the official release and place it in the correct location:
# Download the lazygit binary
wget -O lazygit-0.40.0/usr/bin/lazygit \
  https://github.com/jesseduffield/lazygit/releases/download/v0.40.0/lazygit_0.40.0_Linux_x86_64.tar.gz
tar -xzf lazygit-0.40.0/usr/bin/lazygit
cp lazygit lazygit-0.40.0/usr/bin/
chmod +x lazygit-0.40.0/usr/bin/lazygit
  1. Add documentation:
cat > lazygit-0.40.0/usr/share/doc/lazygit/changelog.Debian << EOF
lazygit (0.40.0-1) unstable; urgency=low

  * Packaging lazygit version 0.40.0

 -- Dario Griffo <dariogriffo@gmail.com>  $(date -R)
EOF
gzip -9 -n lazygit-0.40.0/usr/share/doc/lazygit/changelog.Debian

cat > lazygit-0.40.0/usr/share/doc/lazygit/copyright << EOF
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: lazygit
Source: https://github.com/jesseduffield/lazygit

Files: *
Copyright: $(date +%Y) Jesse Duffield <jessedduffield@gmail.com>
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...
EOF
  1. Build the package:
dpkg-deb --build lazygit-0.40.0

This command produces a .deb file that can be installed using dpkg -i lazygit-0.40.0.deb.

Targeting Multiple Debian Distributions with Docker

One of the challenges in Debian packaging is supporting multiple distributions (Bookworm, Trixie, Sid, etc.). Each distribution might have different dependencies or packaging requirements.

I’ve solved this problem in my uv-debian and lazygit-debian repositories by using Docker to build packages for different distributions. Let’s see how I’ve set this up:

Creating a Dockerfile for Package Building

Here’s the Dockerfile I use for building the uv package:

ARG DEBIAN_DIST=bookworm
FROM debian:$DEBIAN_DIST

ARG DEBIAN_DIST
ARG UV_VERSION
ARG BUILD_VERSION
ARG FULL_VERSION

RUN apt update && apt install -y wget
RUN mkdir -p /output/usr/bin
RUN mkdir -p /output/usr/share/doc/uv
RUN cd /output/usr/bin && wget https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-x86_64-unknown-linux-musl.tar.gz && tar -xf uv-x86_64-unknown-linux-musl.tar.gz && rm -f uv-x86_64-unknown-linux-musl.tar.gz
RUN mkdir -p /output/DEBIAN

COPY output/DEBIAN/control /output/DEBIAN/
COPY output/copyright /output/usr/share/doc/uv/
COPY output/changelog.Debian /output/usr/share/doc/uv/
COPY output/README.md /output/usr/share/doc/uv/

RUN sed -i "s/DIST/$DEBIAN_DIST/" /output/usr/share/doc/uv/changelog.Debian
RUN sed -i "s/FULL_VERSION/$FULL_VERSION/" /output/usr/share/doc/uv/changelog.Debian
RUN sed -i "s/DIST/$DEBIAN_DIST/" /output/DEBIAN/control
RUN sed -i "s/UV_VERSION/$UV_VERSION/" /output/DEBIAN/control
RUN sed -i "s/BUILD_VERSION/$BUILD_VERSION/" /output/DEBIAN/control

RUN dpkg-deb --build /output /uv_${FULL_VERSION}.deb

Creating a Build Script

Here’s the build script I use to automate building packages for multiple distributions:

#!/bin/bash
UV_VERSION=$1
BUILD_VERSION=$2
declare -a arr=("bookworm" "trixie" "sid")
for i in "${arr[@]}"
do
  DEBIAN_DIST=$i
  FULL_VERSION=$UV_VERSION-${BUILD_VERSION}+${DEBIAN_DIST}_amd64
  docker build . -t uv-$DEBIAN_DIST  --build-arg DEBIAN_DIST=$DEBIAN_DIST --build-arg UV_VERSION=$UV_VERSION --build-arg BUILD_VERSION=$BUILD_VERSION --build-arg FULL_VERSION=$FULL_VERSION
  id="$(docker create uv-$DEBIAN_DIST)"
  docker cp $id:/uv_$FULL_VERSION.deb - > ./uv_$FULL_VERSION.deb
  tar -xf ./uv_$FULL_VERSION.deb
done

Running the script with version parameters is simple:

./build.sh 0.7.0 1

This will produce three packages:

  • uv_0.7.0-1+bookworm_amd64.deb
  • uv_0.7.0-1+trixie_amd64.deb
  • uv_0.7.0-1+sid_amd64.deb

Preparing Template Files

For this to work, I prepare template files with placeholders. Here’s what the control file looks like for the uv package:

Source: uv
Section: utils
Priority: optional
Maintainer: Dario Griffo <dariogriffo@gmail.com>
Homepage: https://github.com/astral-sh/uv
Package: uv
Version: UV_VERSION-BUILD_VERSION+DIST
Architecture: amd64
Depends: 
Description: An extremely fast Python package and project manager, written in Rust.

And the changelog.Debian template:

uv (FULL_VERSION) DIST; urgency=low

 
  

The build process automatically replaces placeholders like UV_VERSION, BUILD_VERSION, DIST, and FULL_VERSION with the appropriate values for each distribution.

Real-world Example: Packaging Multiple Applications

I’m maintaining packages for several tools, including:

  1. uv - A fast Python package manager written in Rust (uv-debian repository)
  2. lazygit - A terminal UI for Git (lazygit-debian repository)

Both projects follow a similar structure, with a Dockerfile that:

  1. Takes the distribution name as an argument
  2. Downloads the binary from the original project releases
  3. Sets up the package structure
  4. Uses template files with placeholders
  5. Builds the package for the specific Debian distribution

This approach allows me to:

  1. Quickly update packages when a new upstream version is released
  2. Support multiple Debian distributions from a single codebase
  3. Automate the build process completely
  4. Maintain high-quality packages with minimal effort

Best Practices for Debian Packaging

Based on my experience maintaining packages like uv and lazygit, here are some best practices:

  1. Follow Debian Policy: Familiarize yourself with the Debian Policy Manual for guidelines.

  2. Version Carefully: Use a clear versioning scheme that allows for future updates. I use the format upstream-version-debian-revision+distribution (e.g., 0.7.0-1+bookworm).

  3. Automate Building: Use scripts to automate the build process, making it reproducible.

  4. Test Your Packages: Install and test packages in clean environments to verify they work correctly.

  5. Include Proper Documentation: Always provide copyright, changelog, and README files.

  6. Use Lintian: Run lintian on your packages to check for common issues:

    lintian uv_0.7.0-1+bookworm_amd64.deb
    
  7. Sign Your Packages: Use GPG to sign packages for improved security:

    dpkg-sig --sign builder uv_0.7.0-1+bookworm_amd64.deb
    
  8. Consider Continuous Integration: Set up CI/CD pipelines to automatically build packages when your source code changes. My repositories use GitHub Actions for this purpose.

Advanced Topics

Handling Dependencies

Dependencies are specified in the control file’s Depends field. You can specify version requirements using operators:

Depends: libc6 (>= 2.14), python3 (>= 3.9), libgcc1 (>= 1:4.2)

For lazygit, the only dependency is git:

Depends: git

Maintainer Scripts

For more complex packages, you might need maintainer scripts:

  • preinst: Runs before unpacking the package
  • postinst: Runs after unpacking
  • prerm: Runs before removing
  • postrm: Runs after removing

Here’s a simple example of a postinst script:

#!/bin/bash
set -e

# Create a configuration file if it doesn't exist
if [ ! -f /etc/my-package/config.yaml ]; then
    mkdir -p /etc/my-package
    echo "# Default configuration" > /etc/my-package/config.yaml
    echo "enabled: true" >> /etc/my-package/config.yaml
fi

# Reload systemd if present
if command -v systemctl >/dev/null; then
    systemctl daemon-reload
fi

exit 0

Creating a Local Repository

While a comprehensive guide to hosting your own repository will come in a future post, here’s a quick overview of the process I use for my debian.griffo.io repository:

  1. Create a directory structure:

    mkdir -p repo/dists/{bookworm,trixie,sid}/{main,contrib,non-free}/binary-amd64
    
  2. Copy packages to the appropriate directories:

    cp uv_0.7.0-1+bookworm_amd64.deb repo/dists/bookworm/main/binary-amd64/
    
  3. Generate package indexes:

    cd repo
    apt-ftparchive packages dists/bookworm/main/binary-amd64 > dists/bookworm/main/binary-amd64/Packages
    gzip -9c dists/bookworm/main/binary-amd64/Packages > dists/bookworm/main/binary-amd64/Packages.gz
    
  4. Make the repository accessible:

    # Add to sources.list.d
    echo "deb https://debian.griffo.io/apt $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.list
    

Conclusion

Creating Debian packages is a valuable skill that allows you to distribute your software in a standardized way. By using Docker for package building, you can easily target multiple Debian distributions with minimal effort, as demonstrated in my uv-debian and lazygit-debian repositories.

In this guide, we’ve covered:

  1. The structure and essential files of a Debian package
  2. How to create a basic package manually
  3. Using Docker to target multiple distributions
  4. Real-world examples with automation scripts
  5. Best practices for package maintenance

If you’re interested in learning how to host these packages in your own Debian repository, check out my follow-up guide: The Ultimate Guide to Self-Hosting a Debian Repository, which shows you how to set up a repository that allows users to install your packages using apt and receive automatic updates.

I hope this guide helps you streamline your Debian packaging process. If you have any questions or suggestions, feel free to reach out!

Twitter iconLinkedIn iconGitHub iconYouTube icon
© 2025 Dario Griffo. All rights reserved.