Setting up an APT cache

Speeding up upgrades

I’ve been advocating in several past posts for the use of LXD as a virtual development environment - see for instance “Setting up ROS 2 Jazzy with LXD”. But when setting up such environment for ROS 2, we find ourselves downloading hundreds of packages which may take some times.

There are several ways we can address this and we are going to explore one here that has the advantages of being rather simple and generalise beyond LXD and ROS 2. That solution is to cache our packages in an apt proxy.

In this post we’re going to make use of LXD, cloud-init and addresse ROS 2 specifically but none of those are absolutely necessary and a similar setup can be replicated in a different scenario.

Let’s get started.

Creating a proxy

First thing first, we shall set up a container that will act as the apt proxy and locally cache the debs.

So let’s start by creating said container:

lxc launch ubuntu:24.04 apt-proxy

Once instantiated, we can then shell into the container and update/upgrade it for good measure:

lxc shell apt-proxy
root@apt-proxy:~$ apt update && apt upgrade -y

Hereafter, the commands are run inside the ‘apt-proxy’ container.

In our proxy, we will use the apt-cacher-ng project. To best describe apt-cacher-ng, let’s quote it’s documentation:

[apt-cacher-ng is a] caching proxy. Specialized for package files from Linux distributors, primarily for Debian (and Debian based) distributions

And at this point, that’s pretty much all we need to know as it is pretty much ‘install & forget’. So let’s do just that.

To install it enter:

apt install apt-cacher-ng

After that, make sure that the service is running:

$ systemctl status apt-cacher-ng
● apt-cacher-ng.service - Apt-Cacher NG software download proxy
     Loaded: loaded (/usr/lib/systemd/system/apt-cacher-ng.service; enabled; preset: enabled)
     Active: active (running)

And that’s it, the caching mechanism it set up and listening by default on port 3142.

Before going any further, wouldn’t it be nice if this very container used the caching mechanism? To do so, create the file:

vi /etc/apt/apt.conf.d/02proxy

with the following content:

Acquire::http { Proxy ""; };

With this, the apt-proxy LXD container will use the caching as well.

I’ve created a cloud-init config available on GitHub to easily launch this proxy container with the following one-liner:

lxc launch ubuntu:24.04 apt-proxy --config=user.user-data="$(curl -L"

Keeping an eye

Before moving on, let’s point out that we can keep an eye on the logs of the cacher as they are produced. This will be helpfull to make sure that everything goes as planned. To do so, enter the following in a terminal:

tail -f /var/log/apt-cacher-ng/apt-cacher.log

Creating a client

Typically, a cloud-init configuration for ROS 2 is something along the lines of the following:


      source: "deb [arch=amd64] {{ v1.distro_release }} main"
      keyid: C1CF 6E31 E6BA DE88 68B1 72B4 F42E D6FB AB17 C654

package_upgrade: true

  - build-essential
  - python3-colcon-common-extensions
  - ros-jazzy-ros-base

Installing ROS 2, whether a minimal install or a full desktop one, will usually install a lot of packages. That’s the whole reason we’re setting up a cache in the first place!

So how can we point our new ROS 2 container to the proxy? The simplest solution is to install the auto-apt-proxy package. This project is pretty much a bash script that “autodetect common [local] APT proxy setups”.

To install it, we add the following to our cloud-init config:


+ bootcmd:
+   - [ cloud-init-per, once, apt-proxy-aptupdate, apt-get, update ]
+   - [ cloud-init-per, once, apt-proxy-aptinstall, apt-get, install, auto-apt-proxy ]

This will make sure that the auto-apt-proxy package is be installed sufficiently early in the cloud-init process that subsequent calls to apt (such as the one operated by the ‘packages’ keyword) will hit our proxy and thus our apt cache. All of that happens rather automagically.

Note that all in all, we only install the auto-apt-proxy package.

Alright, let us make sure that everything work. First we shall create a client container:

lxc launch ubuntu:24.04 jazzy-lxc --config=user.user-data="$(curl -L"

Note that I’m using in the command above a cloud-init configuration file that is available on GitHub.

While the container is instantiating, let’s have a look on our cacher logs:

tail -f /var/log/apt-cacher-ng/apt-cacher.log


Oh wow. There is a lot that got printed. Upon closer inspection, we can essentially notice that the first 12 lines correspond to an (apt) update, while the following ones are packages being downloaded. It looks like it works!

Another way to verify that is to shell inside the client container:

lxc shell jazzy-lxc

and call apt with some debug logs as follows:

$ apt update -o Debug::Acquire::http=true
Using auto proxy detect command: /usr/bin/auto-apt-proxy
auto detect command returned: ''
Using auto proxy detect command: /usr/bin/auto-apt-proxy
auto detect command returned: ''
Using auto proxy detect command: /usr/bin/auto-apt-proxy
auto detect command returned: ''
0% [Working]GET HTTP/1.1
Cache-Control: max-age=0
Accept: text/*
If-Modified-Since: Wed, 03 Jul 2024 14:01:45 GMT
User-Agent: Debian APT-HTTP/1.3 (2.7.14) non-interactive

Cache-Control: max-age=0
Accept: text/*
If-Modified-Since: Tue, 25 Jun 2024 14:39:05 GMT
User-Agent: Debian APT-HTTP/1.3 (2.7.14) non-interactive

where ‘’ should be the IP address of the apt-proxy container.

As before, we should see some new logs printed by the cacher, logs that correspond to the update call.

At last, for extra good measure, we can verify that our ROS 2 packages are indeed cached on our proxy server. To do so, we can enter the following command in the apt-proxy container:

$ ls /var/cache/apt-cacher-ng/*/
python3-catkin-pkg-modules  python3-colcon-cmake              python3-colcon-installed-package-information  python3-colcon-package-information  python3-colcon-recursive-crawl  python3-rosdistro-modules
python3-colcon-alias        python3-colcon-common-extensions  python3-colcon-mixin                          python3-colcon-package-selection    python3-colcon-ros              python3-rospkg-modules
python3-colcon-bash         python3-colcon-core               python3-colcon-notification                   python3-colcon-parallel-executor    python3-colcon-zsh              python3-vcstool
python3-colcon-cd           python3-colcon-defaults           python3-colcon-output                         python3-colcon-powershell           python3-rosdep
python3-colcon-clean        python3-colcon-devtools           python3-colcon-override-check                 python3-colcon-python-setup-py      python3-rosdep-modules

ros-jazzy-action-msgs                             ros-jazzy-ament-lint-common           ros-jazzy-pybind11-vendor            ros-jazzy-ros2run                               ros-jazzy-rpyutils
ros-jazzy-actionlib-msgs                          ros-jazzy-ament-package               ros-jazzy-python-cmake-module        ros-jazzy-ros2service                           ros-jazzy-sensor-msgs
ros-jazzy-ament-cmake                             ros-jazzy-ament-pep257                ros-jazzy-rcl                        ros-jazzy-ros2topic                             ros-jazzy-sensor-msgs-py
ros-jazzy-ament-cmake-auto                        ros-jazzy-ament-uncrustify            ros-jazzy-rcl-action                 ros-jazzy-rosbag2                               ros-jazzy-service-msgs
ros-jazzy-ament-cmake-copyright                   ros-jazzy-ament-xmllint               ros-jazzy-rcl-interfaces             ros-jazzy-rosbag2-compression                   ros-jazzy-shape-msgs
ros-jazzy-ament-cmake-core                        ros-jazzy-builtin-interfaces          ros-jazzy-rcl-lifecycle              ros-jazzy-rosbag2-compression-zstd              ros-jazzy-shared-queues-vendor

Yep, there are plenty packages there.

Some metrics

As a very scientific benchmark, we are going to spawn the very same container, with the same ROS 2 ready cloud-init configuration we used previously, with and without the apt-proxy container running.

We recall the command to launch the container:

lxc launch ubuntu:24.04 jazzy-lxc --config=user.user-data="$(curl -L"

In both cases, we’ll wait for cloud-init to finish:

lxc exec jazzy-lxc -- cloud-init status --wait

And as a measure, we will rely on cloud-init with:

lxc exec jazzy-lxc -- cloud-init analyze show

And the results are:

exec time (s)apt_configurepackage_update_upgrade_installtotal
w/o proxy18.44179.95240.47
with proxy03.5739.5990.66

The results are clear, we observe a ~2.5x speed increase in apt operations.

Note that for good measure I ran this test a couple times. The results between runs varies a bit of course but not significantly given the difference.


We have seen in this post how to set up an apt proxy to locally cache debs allowing us to speed up the instantiation of ROS 2 ready LXD containers. And as we’ve seen, it is fairly simple. Gaining a ~2.5x speed increase in apt operations at the cost of ‘fire & forget’ing a server container and installing a single package in our clients is a pretty good deal.

Remember that while we examplified the setup here using LXD and a ROS 2 dev environment, this can be replicated in other scenario as well.

At last, the apt-cacher-ng comes loaded with features and options that you can define in the file /etc/apt-cacher-ng/acng.conf. I leave it to you to explore that and tweak the cacher to your needs.

Edit this page