Robert Važan

Caching apt-get in containers

I build a lot of containers, so I decided to cache downloads performed by Ubuntu's apt. It took some effort to get it right. I will describe the final setup here for future reference.

Why cache apt-get in containers?

If you build a standard container or just a few private containers, layer cache will handle all the caching needs you might have. But if you are like me and build dozens to hundreds of private containers, caching distribution's packages becomes highly desirable for several reasons. In case of images based on the popular Ubuntu distribution, that means caching apt.

So about the reasons... Firstly, it's obviously rude to abuse Ubuntu's servers to repeatedly download the same packages several times per day. Secondly, downloads can be slow and unreliable. Thirdly, local cache provides resilience to (possibly hostile) disruptions in Internet connectivity or Ubuntu's servers.

Prepare your Containerfile

We want to cache apt downloads both during image build and later during container execution. We want this caching to be largely transparent and optional, so that the Containerfile can be included in a public repository without forcing other people to implement our caching system. Cache service configured via build argument is ideal for this purpose.

Let's create an example container that can be optionally configured to use apt cache:

FROM docker.io/library/ubuntu:24.04

# Optional APT proxy. This affects both build and subsequent container execution.
ARG APT_PROXY=""

# Set up non-interactive frontend for package installations.
ENV DEBIAN_FRONTEND=noninteractive

# Configure APT proxy if provided.
RUN if [ -n "$APT_PROXY" ]; then \
        echo "Acquire::http::Proxy \"$APT_PROXY\";" > /etc/apt/apt.conf.d/99proxy; \
    fi

# Install something, say curl, as a demo.
RUN apt-get -y update && \
    apt-get -y install curl && \
    apt-get -y clean && \
    rm -rf /var/lib/apt/lists/*

# ...

Notice that the APT_PROXY argument has a default value, even if it is just an empty string. This prevents warnings during build about unset arguments. Also note that adding the proxy setting under /etc/apt/apt.conf.d/ makes it permanent. The proxy will be used also during subsequent container execution. You can remove the file if you want caching only during build.

Let's start with the baseline scenario when no caching is enabled. You can build and run the image as usual:

podman build -t localhost/apt-cache-test
podman run -it --rm localhost/apt-cache-test

Containerfile for the cache service

We will use apt-cacher-ng, which is specifically designed for caching apt downloads.

FROM docker.io/library/ubuntu:24.04

# Don't forget ca-certificates to access HTTPS mirrors.
RUN export DEBIAN_FRONTEND=noninteractive && \
    apt-get -y update && \
    apt-get -y install --no-install-recommends ca-certificates apt-cacher-ng && \
    rm -rf /var/lib/apt/lists/*

# Container version of apt-cacher-ng needs a little help with the /run directory and configuration.
RUN mkdir -p /run/apt-cacher-ng && \
    chown apt-cacher-ng:apt-cacher-ng /run/apt-cacher-ng && \
    chmod 0755 /run/apt-cacher-ng && \
    sed -i 's/^# *ForeGround: 0/ForeGround: 1/' /etc/apt-cacher-ng/acng.conf && \
    sed -i 's/^# *PassThroughPattern:.*this would allow.*/PassThroughPattern: .* #/' /etc/apt-cacher-ng/acng.conf

EXPOSE 3142

USER apt-cacher-ng

CMD ["/usr/sbin/apt-cacher-ng", "-c", "/etc/apt-cacher-ng"]

We have to build the image before we can use it:

podman build -t localhost/apt-cache

Configure systemd

We can now use podman's systemd integration to run the image as a systemd service:

[Unit]
Description=APT caching proxy (apt-cacher-ng)
After=network-online.target
Wants=network-online.target

[Container]
Image=localhost/apt-cache
ContainerName=apt-cache
RunInit=true
Volume=apt-cache-data:/var/cache/apt-cacher-ng:z
PublishPort=127.0.0.1:3142:3142

[Service]
Restart=always

[Install]
WantedBy=default.target

Save this file as ~/.config/containers/systemd/apt-cache.container, let systemd know about it, and restart it in case there's previous version running:

systemctl --user daemon-reload
systemctl --user restart apt-cache

Data will be saved in named volume apt-cache-data. Notice that we are using rootless podman container to run the service under unprivileged account. I try to avoid rootful containers as much as possible. It makes things easier when you want to introduce bind mounts for whatever reason. To make the container start even without user login, enable lingering for the user:

sudo loginctl enable-linger $USER

If everything works, you should see a help page for apt-cacher-ng when you open http://127.0.0.1:3142/.

Configure the container to use the cache

To run the example image we introduced at the beginning of this article, you just have to pass in one argument and forward one port. Port must be forwarded during both build and and execution:

podman build \
    -t localhost/apt-cache-test \
    --build-arg APT_PROXY=http://127.0.0.1:3142 \
    --network=pasta:-T,3142
podman run -it --rm \
    --network=pasta:-T,3142 \
    localhost/apt-cache-test

You should see the cache being used in a report at http://127.0.0.1:3142/acng-report.html.