1. Introduction

APT (Advanced Package Tool) is a package manager for Debian-based Linux distributions like Ubuntu. It utilizes software repositories, which are online libraries of pre-compiled packages. APT automates the download, resolution of dependencies, and configuration of these packages, making software management on these systems efficient.

In contrast to the standard online repositories used by package managers like APT in Debian-based systems, local APT repositories are locally hosted collections of software packages. They offer several advantages, particularly for organizations managing multiple systems. Local repositories are useful controlled environments, testing cases, offline installations, and reduced bandwidth usage.

In this tutorial, we’re going to explore several ways to set up a Local APT repository:

  • with dpkg-scanpackages
  • over LAN with Apache
  • with reprepro and Apache
  • with apt-mirror

We’ll discuss each of the above methods in detail. Notably, some of these methods require a client machine for testing purposes. All commands in this tutorial have been tested on Ubuntu 20.04.

2. Setting Up a Local APT Repository With dpkg-scanpackages

One of the simplest yet effective methods to create a local repository is the dpkg-scanpackages tool. This utility, part of the dpkg suite, enables us to generate the necessary metadata for a collection of .deb packages, effectively turning any directory into a functional Debian repository.

By following a few fairly straightforward steps, we can create a repository that’s easily accessible to APT. This enables installing and managing local packages alongside those from official sources.

2.1. Install dpkg-dev and Create Package Directory

The dpkg-scanpackages utility is a command-line tool used in Debian-based systems to create package indexes for a local repository.

Although this utility is typically installed by default, we might have to install it through the dpkg-dev package:

$ sudo apt-get install dpkg-dev

Once installed, we can now create a directory at /usr/local/repos to store the .deb packages:

$ sudo mkdir -p /usr/local/repos

For this tutorial, we use the nginx .deb package. Let’s navigate to the repos directory and download it from source:

$ cd /usr/local/repos
$ sudo wget http://archive.ubuntu.com/ubuntu/pool/main/n/nginx/nginx_1.22.0-1ubuntu3_amd64.deb
...

nginx_1.22.0-1ubuntu3_amd64.deb   100%[===========================================================>]   3.87K  --.-KB/s    in 0s      

2024-07-22 07:08:35 (104 MB/s) - ‘nginx_1.22.0-1ubuntu3_amd64.deb’ saved [3966/3966]

Alternatively, we can copy previously downloaded packages .deb packages available in the /var/cache/apt/archives/ directory to the repos directory:

$ sudo cp /var/cache/apt/archives/<package> /usr/local/repos/

Hence, we’ve successfully added a .deb package to the new package directory.

2.2. Configuring Local Repository

Now, let’s generate the Packages.gz zip file from the package(s) in the repos directory and compress it:

$ dpkg-scanpackages . /dev/null | gzip -9c | sudo tee Packages.gz > /dev/null
dpkg-scanpackages: warning: Packages in archive but missing from override file:
dpkg-scanpackages: warning:   nginx
dpkg-scanpackages: info: Wrote 1 entries to output Packages file.

Now, let’s add the repos directory path to sources.list:

$ echo "deb [trusted=yes] file:/usr/local/repos ./" | sudo tee -a /etc/apt/sources.list
deb [trusted=yes] file:/usr/local/repos ./

It’s good practice to verify that the appended directory path is in the sources.list file:

$ tail -5 /etc/apt/sources.list
deb http://security.ubuntu.com/ubuntu focal-security universe
# deb-src http://security.ubuntu.com/ubuntu focal-security universe
deb http://security.ubuntu.com/ubuntu focal-security multiverse
# deb-src http://security.ubuntu.com/ubuntu focal-security multiverse
deb [trusted=yes] file:/usr/local/repos ./

Finally, we update the package cache:

$ sudo apt-get update
Get:1 file:/usr/local/repos ./ InRelease
Ign:1 file:/usr/local/repos ./ InRelease
Get:2 file:/usr/local/repos ./ Release
Ign:2 file:/usr/local/repos ./ Release
Get:3 file:/usr/local/repos ./ Packages
Ign:3 file:/usr/local/repos ./ Packages
...

The output above shows APT fetches packages from the repos directory we created.

Additionally, we can confirm APT has recognized the local repository by checking the availability of the nginx package:

$ apt-cache policy nginx
nginx:
  Installed: (none)
  Candidate: 1.22.0-1ubuntu3
  Version table:
     1.22.0-1ubuntu3 500
        500 file:/usr/local/repos ./ Packages
     1.18.0-0ubuntu1.5 500

As seen above, the local repository is now visible to APT.

3. Setting Up a Local Repository Over LAN With Apache

In this method, we set up a local repository similar to the previous section but with the Apache Web server.

Setting up a local repository with a Web server enables us to host the Debian packages over HTTP, making them accessible to multiple machines on the network.

3.1. Install Apache and Setup Package Repository

To begin, let’s install Apache:

$ sudo apt install apache2

By default, Apache serves the /var/www/html directory. Hence, let’s create the package directory named repos in this path:

$ sudo mkdir -p /var/www/html/repos

However, we have to create another directory based on the server architecture (amd64 or i386):

$ sudo mkdir -p /var/www/html/repos/amd64

Hence, we’ve successfully installed Apache and set up the package directory.

3.2. Add Local Repository to APT

Now, we copy a .deb package to the amd64 directory.

For this example, let’s use the amd64 version of the postfix package:

$ cd /var/www/html/repos/amd64
$ sudo wget http://archive.ubuntu.com/ubuntu/pool/main/p/postfix/postfix_2.11.0-1_amd64.deb
--2024-07-23 03:41:29--  http://archive.ubuntu.com/ubuntu/pool/main/p/postfix/postfix_2.11.0-1_amd64.deb
Resolving archive.ubuntu.com (archive.ubuntu.com)... 185.125.190.81, 185.125.190.83, 91.189.91.81, ...
Connecting to archive.ubuntu.com (archive.ubuntu.com)|185.125.190.81|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1085128 (1.0M) [application/vnd.debian.binary-package]
Saving to: ‘postfix_2.11.0-1_amd64.deb’

postfix_2.11.0-1_amd64.deb        100%[===========================================================>]   1.03M   756KB/s    in 1.4s    

2024-07-23 03:41:36 (756 KB/s) - ‘postfix_2.11.0-1_amd64.deb’ saved [1085128/1085128]

Next, we create the Packages.gz file via dpkg-scanpackages:

$ dpkg-scanpackages . /dev/null | gzip -9c | sudo tee Packages.gz > /dev/null
dpkg-scanpackages: warning: Packages in archive but missing from override file:
dpkg-scanpackages: warning:   postfix
dpkg-scanpackages: info: Wrote 1 entries to output Packages file.

Optionally, we can create a Release file to contain the repository metadata:

$ echo "Origin: Baeldung Repository" | sudo tee Release
$ echo "Label: Baeldung Label" | sudo tee -a Release
$ echo "Suite: stable" | sudo tee -a Release
$ echo "Codename: local" | sudo tee -a Release
$ echo "Architectures: amd64 i386" | sudo tee -a Release
$ echo "Components: main" | sudo tee -a Release
$ echo "Description: This is a local Repository" | sudo tee -a Release

Since we’ve set up the local repository, let’s add the path to sources.list:

$ echo "deb [trusted=yes] http://localhost/repos/ amd64/" | sudo tee -a /etc/apt/sources.list
deb [trusted=yes] http://localhost/repos/ amd64/

Then, we update the package cache:

$ sudo apt-get update
Ign:1 http://localhost/repos amd64/ InRelease
Get:2 http://localhost/repos amd64/ Release [106 B]
Ign:3 http://localhost/repos amd64/ Release.gpg
Ign:4 http://localhost/repos amd64/ Packages
...

The command output shows that APT now recognizes the local repository hosted with Apache.

Let’s confirm the availability of the postfix package using the apt-cache policy command:

$ apt-cache policy postfix
postfix:
  Installed: (none)
  Candidate: 3.4.13-0ubuntu1.4
  Version table:
...
     3.4.10-1ubuntu1 500
        500 http://archive.ubuntu.com/ubuntu focal/main amd64 Packages
     2.11.0-1 500
        500 http://localhost/repos amd64/ Packages

Alternatively, we can download and view the local repository by entering the server’s IP address with the repos/amd64 path in a Web browser:

Local repository using Apache via web browser

Hence, we’ve successfully set up a local repository over LAN with Apache.

4. Setting Up a Local APT Repository with reprepro and Apache

reprepro is a tool used in Debian-based Linux distributions for managing local repositories of software packages. It helps system administrators and developers create, maintain, and distribute custom repositories. Additionally, reprepro can sign packages and generate repository metadata.

In this method, we set up a local repository with repropro and make it available on the Internet to client machines using Apache.

4.1. Install reprepro and Generate GPG key

To begin, let’s log in to the server as the root user and install the reprepro package:

$ apt-get install reprepro

Next, we install gnupg and dpkg-sig packages for key generation and signing of the Debian packages:

$ apt-get install gnupg dpkg-sig 

Once installed, we can now generate the key with  –gen-key option:

$ gpg --gen-key
gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: baeldung
Email address: [email protected]
You selected this USER-ID:
    "baeldung <[email protected]>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
...
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key 5CCAC30D1DBB40EC marked as ultimately trusted
gpg: directory '/root/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/83CB92A14380717980A148445CCAC30D1DBB40EC.rev'
public and secret key created and signed.

pub   rsa3072 2024-07-24 [SC] [expires: 2026-07-24]
      83CB92A14380717980A148445CCAC30D1DBB40EC
uid                      baeldung <[email protected]>
sub   rsa3072 2024-07-24 [E] [expires: 2026-07-24]

For this example, let’s download and use the amd64 version of the powertop package:

$ wget http://archive.ubuntu.com/ubuntu/pool/main/p/powertop/powertop-dbg_2.9-0ubuntu1_amd64.deb

Once the download is complete, we sign the package with the dpkg-sig utility:

$ export GPG_TTY=$(tty)
$ dpkg-sig --sign builder powertop-dbg_2.9-0ubuntu1_amd64.deb
Processing powertop-dbg_2.9-0ubuntu1_amd64.deb...
Signed deb powertop-dbg_2.9-0ubuntu1_amd64.deb

Hence, we’ve successfully signed the powertop package.

4.2. Configuring Local Repository

Let’s create file system layouts that support the distribution (like Debian or Ubuntu) we’re using:

$ mkdir -p /var/packages/ubuntu
$ mkdir /var/packages/ubuntu/conf
$ mkdir -p /var/packages/debian
$ mkdir /var/packages/debian/conf

To create a local repository, reprepo searches for four configuration files in the directory provided with the –confdir option or within the conf/ subdirectory of the base directory:

  • distributions: defines managed distributions
  • options: sets global configuration options for all distributions
  • updates: syncs with external repositories
  • pulls: syncs within the same reprepro database

Let’s create the distributions file in the conf directory:

$ cat /var/packages/ubuntu/conf/distributions
Origin: baeldung.com
Label: baeldung.com
Codename: precise
Architectures: i386 amd64 source
Components: main
Description: Baeldung Local APT Repository
SignWith: yes
DebOverride: override.precise
DscOverride: override.precise

Save and exit the file. Next, we create the options file:

$ cat /var/packages/ubuntu/conf/options
verbose
ask-passphrase
basedir .

We can skip the updates and pulls files, as they are not necessary in this case.

Finally, let’s create an empty override.precise file:

$ touch /var/packages/ubuntu/conf/override.precise

Thus, we’ve successfully configured the local repository to be ready for use by reprepro.

4.3. Build Local Repository and Add Packages

Now, let’s the downloaded powertop package to the repository via reprepro:

$ cd /var/packages/ubuntu
$ reprepro includedeb precise /root/powertop-dbg_2.9-0ubuntu1_amd64.deb
/root/powertop-dbg_2.9-0ubuntu1_amd64.deb: component guessed as 'main'
Exporting indices...

We can verify the result by listing the packages in the repository:

$ reprepro list precise
precise|main|amd64: powertop-dbg 2.9-0ubuntu1

Additionally, we delete packages from the repository using the remove command:

$ reprepro remove precise <package>

Hence, we’ve successfully built and added a package to the local repository with reprepro.

4.4. Configuring Repository Access Using Apache VirtualHost

One of the best ways to configure access to the repository is using Apache.

So, let’s configure a virtual host in the Apache configuration repo.conf file:

$ cat /etc/apache2/sites-available/repo.conf
<VirtualHost *:80>
    DocumentRoot /var/packages
    ServerName <server_ip>

    ErrorLog /var/log/apache2/error.log
    CustomLog /var/log/apache2/access.log combined
    LogLevel warn

    ServerSignature On

    <Directory "/var/packages">
        Options Indexes FollowSymLinks MultiViews
        DirectoryIndex index.html
        Require all granted
    </Directory>

    <Directory "/var/packages/*/conf">
        Require all denied
    </Directory>

    <Directory "/var/packages/*/db">
        Require all denied
    </Directory>
</VirtualHost>

Once ready, let’s enable the site:

$ a2ensite repo.conf
Enabling site repo.
..

Then, we activate the configuration with a reload of the apache2 service:

$ systemctl reload apache2

However, since we signed the packages with a gpg key, client machines have to download that same key. So, let’s place it in the root of the Apache Web server:

$ cd /var/packages/
$ gpg --armor --output local-packages.key --export [email protected]

As seen above, we store the key in the local-packages.key file.

4.5. Connecting Client Machine

To connect a client, we first ensure the correct local-packages.key file is available locally:

$ curl -H GET <server_ip>/local-packages.key > local-packages.key
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2448  100  2448    0     0  1195k      0 --:--:-- --:--:-- --:--:-- 1195k

Then, we add it to the apt repository key ring:

$ apt-key add local-packages.key
OK

Next, we update the sources.list file with the repository link:

$ echo "deb http://<server_ip>/ubuntu precise main" | sudo tee -a /etc/apt/sources.list
$ echo "deb http://<server_ip>/ubuntu precise main" | sudo tee -a /etc/apt/sources.list

Let’s attempt an update:

$ apt-get update
Hit:1 http://<server_ip>/ubuntu precise InRelease
Get:2 http://security.ubuntu.com/ubuntu focal-security InRelease [128 kB]
...

Thus, we verify that apt recognizes the local repository, let’s update the package cache.

5. Setting Up a Local APT Repository With apt-mirror

apt-mirror is a tool that creates local copies of remote Ubuntu or Debian repositories. It retrieves all packages, including binaries and source files.

It’s flexible and enables us to choose which repositories to mirror. Creating a local copy reduces Internet bandwidth usage for multiple machines on a network.

5.1. Install Apache and Configure Package directory

To begin with, we acquire a Web server like Apache or Nginx to serve the local repository.

For this example, let’s install the apache2 Web server:

$ sudo apt-get install apache2

apache2 service starts automatically after installation by default.

Next, we create a package directory named ubuntu for the local repository under /var/www/html/:

$ sudo mkdir -p /var/www/html/ubuntu

Hence, we’ve installed Apache and configured the package directory.

5.2. Install and Configure apt-mirror

Now, let’s install the apt-mirror utility:

$ sudo apt-get install apt-mirror

Once installed, apt-mirror stores its configuration in the /etc/apt/mirror.list file. We edit this file and set the base_path variable to the newly created /var/www/html/ubuntu directory.

In this case, we use vim to edit /etc/apt/mirror.list:

$ sudo vim /etc/apt/mirror.list

Then, we update the base_path variable and comment out the deb-src repositories to minimize download size:

############# config ##################
#
set base_path    /var/www/html/ubuntu
...
set nthreads     20
set _tilde 0
#
############# end config ##############

deb http://archive.ubuntu.com/ubuntu focal main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu focal-security main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu focal-updates main restricted universe multiverse
...
#deb-src http://archive.ubuntu.com/ubuntu focal main restricted universe multiverse
clean http://archive.ubuntu.com/ubuntu

At this point, we should be good to go.

5.3. Verify Setup

After saving the file, we can now mirror the remote packages:

$ sudo apt-mirror
Downloading 114 index files using 20 threads...
...
Processing translation indexes: [TTT]

Downloading 555 translation files using 20 threads...
...
Processing DEP-11 indexes: [DDD]

Downloading 88 dep11 files using 20 threads...
...
Processing cnf indexes: [CCC]

Downloading 24 cnf files using 20 threads...
...
Processing indexes: [PPP]

466.2 GiB will be downloaded into archive.
Downloading 103454 archive files using 20 threads...
Begin time: Thu Jul 25 01:39:33 2024

Notably, the mirroring process usually takes a lot of time, so we can execute the mirror process in the background using the nohup command:

$ nohup sudo apt-mirror &

Then, we monitor with the tail command:

$ tail nohup.out

Once the mirror is complete, the mirrored packages should be in the ubuntu directory:

$ ls -l /var/www/html/ubuntu/
total 20
drwxr-xr-x 3 root root  4096 Jul 25 01:39 mirror
drwxr-xr-x 3 root root  4096 Jul 25 01:16 skel
drwxr-xr-x 2 root root 12288 Jul 25 01:39 var

These packages are also accessible via the Web browser on the http://<server_ip>/ubuntu/ path:

Local repository using apt-mirror via web browser

Hence, we’ve successfully created a local APT repository using apt-mirror.

5.4. Scheduling Mirroring With a Cron Job

To keep the local repository up-to-date, it’s recommended we set up a cron job and run apt-mirror periodically, optimally when the bandwidth usage is low.

For this example, let’s update the mirror daily at 1:00 AM using crontab:

$ sudo crontab -e
00  01  *  *  *  /usr/bin/apt-mirror

Hence, we’ve successfully scheduled mirroring to ensure updates in the local repository.

6. Conclusion

In this article, we’ve learned how to create local APT repositories to enhance package management in Debian-based environments like Ubuntu.

In particular, we explored several ways to create local APT repositories using tools such as dpkg-scanpackages, apache2, reprepro, and apt-mirror. Although each approach works, which one we choose mainly depends on the environment and requirements.