My name is Philipp C. Heckel and I write about nerdy things.
This site moved here recently from!

How-To: Create a Debian package and a Debian repository


How-To: Create a Debian package and a Debian repository

Debian packages and repositories are everywhere, yet many people don’t understand that creating them is actually pretty easy. While there are dozens of tutorials out there, none of them seemed to really show a good step-by-step. This is a quick tutorial on how to create a Debian package from scratch, and how to create a simple Debian repository.


1. Demo package ‘netutils’

For the sake of this tutorial, we’ll create a package called netutils with a command called ipaddr. The purpose of the command will be to get the external IP address, just like this:

2. Create a Debian package

Let’s start by creating the empty project directory netutils/. This folder will contain both the source code and the Debian build instructions.

2.1. Create the debian/ directory

For now, we’ll use the dh_make command to create the debian/ directory:

This created the debian/ folder. Explore them! Especially the example files (*.ex) as well as most importantly the files:

  • debian/control
  • debian/changelog
  • debian/rules

There are maaaany files, and Debian documented all of them in the Debian Maintainer’s Guide. For now, we don’t care about the rest of them, but here’s a list of all the created files:

2.2. Build the first (empty) package

As you can see there are a lot of files to tweak the package, but for now we’ll ignore all that and build an empty package. The dpkg-buildpackage command can be used to do that:

That’s it! That command built four files:

  • .tar.gz: Source package, contains the contents of the netutils/ folder
  • .deb: Debian package, contains the installable package
  • .dsc/.changes: Signature files, cryptographic signatures of all files

Obviously, the most interesting file (for now) is the .deb file.
Let’s examine the contents with the dpkg -c (aka dpkg --contents) command:

Nothing really in the package except the default changelog, copyright and README file. Instead of just listing the contents, we can also extract a Debian archive to a local location with dpkg -x (aka dpkg --extract):

2.3. Install the (empty) package

Enough playing around with the .deb file. Let’s install it with the dpkg -i (aka dpkg --install) command:

Done. That installed the package. Check out that it was actually installed by listing the installed packages with dpkg -l (aka dpkg --list). The list itself will contain all installed (or half-installed/configured) packages on the system, so we’ll use grep to limit the output:

The columns in the list are desired package state, actual package state, package name, package version, package architecture and a short package description. If all is well, the first column will contain ii, which means that the package is properly installed. The dpkg-query man page (man dpkg-query) contains all the possible desired/actual state values.

Now that the package is installed, you can list its contents with the dpkg -L (aka dpkg --listfiles) command. Unlike dpkg -c, it only works for installed packages:

2.4. Adding files and updating the changelog

Okay, enough with the empty package. Let’s now add actual files to our package. To do that, let’s create a folder files/ and use it to mirror the Linux filesystem structure. In that folder, let’s create our script files/usr/bin/ipaddr:

Obviously, you can add whatever you want to your script, but for the sake of this tutorial, we’ll go with a short script to grab the public IP address from a service called ipify. It provides an API to return the IP address in various formats. We’ll grab it with curl in JSON and then use jq to parse out the ‘ip’ field:

If we were to rebuild the package now, the dpkg-buildpackage command wouldn’t know which files to include in the package. So we’ll create the debian/install file to list directories to include (e.g. vi debian/install):

This basically means that everything in the files/usr/ folder will be installed at /usr/ on the target file system when the package is installed.

Once this is done, we could just rebuild and reinstall the package, but let’s go one step further and update the version of the package to 1.1.0. Versions are handled by the debian/changelog file. You can update it manually, or use the dch script (short for “Debian changelog”) to do so:

While the changelog looks just like a dumb text file, Debian uses it to manage the version number of the package, as well as to define the distribution(s) that the package is built for (here: “unstable”). Furthermore, it defines who has to sign the package (name of changelog editor).

After that, let’s rebuild and reinstall the package:

Looks like it installed correctly. Let’s check if the ipaddr script is where it’s supposed to be. And then let’s try to run it:

Oops! We forgot that not every system might have jq installed. Let’s add it as a dependency!

2.5. Updating the description and adding dependencies

Each Debian package can depend on other packages. In the case of our ipaddr script, we use the curl and jq commands, so the ‘netutils’ package depends on these commands.

Since typically the command name is different from the package name, it might be necessary to find the actual package name. This can be done with the dpkg -S (aka dpkg --search) command:

To add the dependencies to the ‘netutils’ package, edit the Depends: section in the debian/control file (e.g. via vi debian/control):

That’s it. All that’s left (again) is to update the version to 1.2.0, rebuild the package and reinstall it:

Wow. What happened here?

Well, unlike apt-get install, the dpkg -i command does not automatically resolve and install missing dependencies. It just complains about them. That is perfectly normal and expected. In fact, it gives us the perfect opportunity to check the package state (like we did above):

As you can see by the output, the desired state for the package is ‘i’ (= installed), but the actual state is ‘U’ (= Unpacked). That’s not good. Luckily though, dependencies can be automatically resolved by apt-get install -f (aka apt-get install --fix-broken):

Finally, it’s installed correctly. Now let’s test it:

3. Create and use a Debian repository

3.1. dpkg -i vs. apt-get install

So why did we use dpkg -i and not apt-get install? Because apt-get install looks in all the configured Debian repositories; it does not look for files. And as of right now, there is no Debian repository serving the netutils package:

3.2. Locally configured Debian/APT repositories (/etc/apt/sources.list.d/*.list)

So where does ‘apt-get install’ look? How does it know where to retrieve/download the packages and its dependencies from?

APT repositories are configured in the files /etc/apt/sources.list and /etc/apt/sources.list.d/*.list. On a typical Debian/Ubuntu system, there are quite a few of them:

Each of these files contains a list of Debian repositories:

3.3. The APT cache and Packages/Packages.gz files

Whenever apt-get update is called, all of these repos are checked for new versions. The APT cache (see apt-cache command) is refreshed:

The ‘Hit’ actually means that it downloaded/checked the Packages.gz file (in the example above from the URL The file looks like this:

Looks familiar? Yes! A Packages.gz file looks very, very similar to the debian/control file in our package. And that’s no coincidence.

3.4. Creating a local Debian repository

So in a nutshell, all we need to create a Debian repository is a Packages.gz file and a way to expose this file — either via HTTP or locally. Easy, easy, easy!

For the sake of this tutorial, let’s create a local repository at /tmp/repo and copy all the netutils_*.deb files to it. Once that is done, we’ll create a Packages.gz file using the dpkg-scanpackages command:

That’s it. Let’s check what the Packages.gz file looks like:

3.5. Adding the local Debian repository and installing via apt-get install

All that’s left is to add that repository to the local APT config by adding a *.list file, e.g. at /etc/apt/sources.list.d/local.list:

Now we can install the netutils package via apt-get install:

3.6. Listing and installing older versions

Maybe you noticed that the Packages.gz file contained multiple versions of the package. That means that we can actually list and install specific/older versions of the package if we wanted to.

Let’s explore what versions we could install. This can be done with apt-cache policy or apt-cache madison:

By default apt-get install always picks the newest one, but by doing apt-get install netutils=1.0.0, we could tell it to install version 1.0.0:

3.7. Normal/typical repository layout

Our local repository is very basic. It does not have the same sophisticated structure of a normal repository such as the main upstream Ubuntu repository. Normal repositories looks more like this:

If you really wanted to, you could create that structure yourself. For the fun of it, let’s do that with our repository at /tmp/repo. Remember, this is what it looks like:

Now to “transform” it:

After that, the directory structure of our almost-proper repository looks like this:

Then, we obviously need to update our sources .list file at /etc/apt/sources.list.d/local.list:

Leave a comment

I'd very much like to hear what you think of this post. Feel free to leave a comment. I usually respond within a day or two, sometimes even faster. I will not share or publish your e-mail address anywhere.