Freeze Your Linux Package State: Reproducible APT Mirrors with aptly Snapshots
If you manage more than one Linux box, you eventually hit the same problem: apt update && apt upgrade is not fully reproducible.
The package set behind a Debian or Ubuntu repository is a moving target. If you patch one machine in the morning and another in the evening, you might not get the exact same package versions. That is usually fine until you need one of these:
- a controlled rollout window
- a predictable staging-to-production promotion
- a quick rollback after a bad package update
- a stable package source for disconnected or bandwidth-limited environments
This is where aptly becomes genuinely useful.
Instead of treating upstream repositories as a live stream, aptly lets you:
- mirror them locally
- turn the current state into an immutable snapshot
- publish that snapshot as your own APT repository
- switch clients to a newer or older snapshot when you decide
That changes package management from “whatever upstream serves right now” to “the exact package set I approved.”
Why this is different from a caching proxy
A caching proxy like apt-cacher-ng is great when your goal is speed and bandwidth savings.
A snapshot-based mirror solves a different problem: repeatability and rollback.
That distinction matters:
- Cache: makes downloads faster
- Snapshot mirror: makes package state deterministic
If your goal is reproducible patch windows, auditability, or fast rollback, snapshots are the tool you want.
What aptly gives you
According to the aptly documentation, its goal is to provide repeatability and controlled changes in package environments, using immutable snapshots as the building block for deterministic installs and rollbacks.
In practice, that means you can:
- keep a local mirror of upstream packages
- snapshot a known-good state
- publish that state under your own URL
- republish clients to a newer snapshot later with
aptly publish switch - switch back to an older snapshot if needed
That is a very different operational model from pointing every machine directly at deb.debian.org.
The lab setup
I’ll use Debian Bookworm as the example, but the workflow applies to Ubuntu too.
Host roles:
-
mirror host: runs
aptly,gpg, andnginx - client hosts: consume the published repository over HTTP
Example mirror host URL:
https://repo.example.com/debian/
Step 1: Install aptly, nginx, and GnuPG
On the mirror host:
sudo apt update
sudo apt install -y aptly nginx gpg
Check the version so you know what you are operating:
aptly version
nginx -v
gpg --version | head -n 1
Step 2: Create a signing key for your repository
APT clients should trust your repository key, not blindly trust unsigned metadata.
Create a dedicated signing key:
gpg --quick-gen-key "Homelab Repo Signing Key <repo@example.com>" rsa4096 sign 1y
List it:
gpg --list-secret-keys --keyid-format long
Export the public key for clients:
gpg --armor --export "Homelab Repo Signing Key <repo@example.com>" > repo-signing-key.asc
Then install it in a place you can serve with nginx:
sudo install -d -m 0755 /var/www/repo
sudo install -m 0644 repo-signing-key.asc /var/www/repo/repo-signing-key.asc
Step 3: Create the upstream mirror
Create a mirror for Debian Bookworm main on amd64:
aptly -architectures="amd64" \
mirror create debian-bookworm-main \
https://deb.debian.org/debian/ \
bookworm \
main
Now download the current repository state:
aptly mirror update debian-bookworm-main
That first sync can take time and disk space. The payoff is that you now control when your downstream systems see change.
Step 4: Create an immutable snapshot
After the mirror is updated, create a timestamped snapshot:
SNAPSHOT="bookworm-main-$(date -u +%Y%m%d)"
aptly snapshot create "$SNAPSHOT" from mirror debian-bookworm-main
List snapshots:
aptly snapshot list
This is the key idea: the snapshot does not change, even after the mirror is updated later.
Step 5: Publish the snapshot as your repository
Publish it under a debian prefix and explicitly set distribution/component values so the result is obvious to clients:
aptly publish snapshot \
-distribution="bookworm" \
-component="main" \
"$SNAPSHOT" \
debian
By default, local publishes appear under aptly’s public directory. A common local path is:
~/.aptly/public
On many systems that resolves to:
/home/<user>/.aptly/public
Step 6: Serve the published repo with nginx
Create an nginx server block like this:
server {
listen 80;
server_name repo.example.com;
location /debian/ {
alias /home/repo/.aptly/public/;
autoindex on;
}
location = /repo-signing-key.asc {
root /var/www/repo;
}
}
Enable and validate it:
sudo ln -s /etc/nginx/sites-available/repo.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
A quick verification:
curl -I http://repo.example.com/debian/dists/bookworm/Release
curl -I http://repo.example.com/repo-signing-key.asc
autoindex on; is optional, but nginx documents that it enables directory listings when no index file is present, which can be handy for debugging repository paths.
Step 7: Configure a client safely with signed-by
In production, serve the repository over HTTPS. On a client, install your exported public key into a dedicated local keyring file:
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://repo.example.com/repo-signing-key.asc \
| sudo gpg --dearmor -o /etc/apt/keyrings/homelab-repo-archive-keyring.gpg
sudo chmod 0644 /etc/apt/keyrings/homelab-repo-archive-keyring.gpg
Then add the source:
echo 'deb [signed-by=/etc/apt/keyrings/homelab-repo-archive-keyring.gpg] https://repo.example.com/debian bookworm main' \
| sudo tee /etc/apt/sources.list.d/homelab-repo.list >/dev/null
Update package metadata and verify the source:
sudo apt update
apt-cache policy | sed -n '/repo.example.com/,+4p'
Why signed-by? Because APT source definitions support per-source options inside square brackets, which lets you bind trust for this repo to a specific keyring file instead of using a global trust model.
Step 8: Roll out updates on your schedule
When you are ready for a new patch window:
- update the upstream mirror
- create a new snapshot
- switch the published repo to that snapshot
Example:
aptly mirror update debian-bookworm-main
NEW_SNAPSHOT="bookworm-main-$(date -u +%Y%m%d)-2"
aptly snapshot create "$NEW_SNAPSHOT" from mirror debian-bookworm-main
aptly publish switch bookworm debian "$NEW_SNAPSHOT"
The important bit is aptly publish switch: it updates the published repository in place while preserving the repo’s publishing parameters.
That means clients keep using the same repo URL, but you decide which immutable snapshot sits behind it.
Step 9: Roll back fast if an update breaks something
Let’s say the new snapshot causes trouble.
Find the last known-good snapshot:
aptly snapshot list
Switch back:
aptly publish switch bookworm debian bookworm-main-20260404
Then on clients:
sudo apt update
sudo apt upgrade
If the newer package versions are already installed, you may also need explicit downgrades depending on what changed and how your pinning policy is set up. But the repository state itself is no longer the moving part. That alone makes incident response cleaner.
A practical systemd timer for snapshot refreshes
If you want a controlled daily ingest on the mirror host, use a oneshot service plus timer.
Service unit:
# /etc/systemd/system/aptly-snapshot-refresh.service
[Unit]
Description=Refresh aptly mirror and create a new snapshot
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=repo
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/bash -lc '
set -euo pipefail
aptly mirror update debian-bookworm-main
SNAPSHOT="bookworm-main-$(date -u +%%Y%%m%%d-%%H%%M%%S)"
aptly snapshot create "$SNAPSHOT" from mirror debian-bookworm-main
'
Timer unit:
# /etc/systemd/system/aptly-snapshot-refresh.timer
[Unit]
Description=Run aptly snapshot refresh daily
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
[Install]
WantedBy=timers.target
Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now aptly-snapshot-refresh.timer
sudo systemctl list-timers aptly-snapshot-refresh.timer
I prefer separating snapshot creation from publishing the switch. That gives you a buffer for validation:
- automation creates the candidate snapshot
- you test it on staging
- you run
aptly publish switchonly after approval
That is safer than auto-promoting every upstream change straight to production.
Step 10: Verify what clients will actually install
Before promoting a new snapshot broadly, verify package candidates from a test client:
apt-cache policy openssl
apt-cache madison openssl
That makes it obvious which version your published snapshot is offering before you upgrade production machines.
Storage and cleanup notes
Before you go all in, plan for disk usage.
A local mirror can consume significant space, especially if you keep multiple snapshots and more than one distribution/component/architecture.
Useful checks:
du -sh ~/.aptly
aptly mirror list
aptly snapshot list
aptly publish list
When old snapshots are no longer needed, remove them deliberately:
aptly snapshot drop old-snapshot-name
If you are unsure whether something is still referenced, inspect your published repos before deleting.
When this pattern is worth it
This setup is worth the operational cost when you care about:
- repeatable patching across many hosts
- staged promotions from test to production
- rollback speed after bad upstream updates
- auditable change windows
- offline or bandwidth-constrained environments
If you only want to reduce repeated package downloads, a caching proxy is simpler.
If you want deterministic package state, snapshots win.
Final thoughts
There is a big difference between “my servers update from Debian” and “my servers update from the exact package set I approved on Tuesday.”
aptly closes that gap.
It gives you a practical middle ground between direct upstream package consumption and a full-blown enterprise repository platform. For homelabs, small fleets, and cautious production environments, that can be exactly enough.
The nicest part is not the mirror itself. It is the confidence that comes from knowing you can move forward deliberately and go backward quickly.
References
- aptly overview: https://www.aptly.info/doc/overview/
- aptly mirror create: https://www.aptly.info/doc/aptly/mirror/create/
- aptly mirror update: https://www.aptly.info/doc/aptly/mirror/update/
- aptly snapshot create: https://www.aptly.info/doc/aptly/snapshot/create/
- aptly publish snapshot: https://www.aptly.info/doc/aptly/publish/snapshot/
- aptly publish switch: https://www.aptly.info/doc/aptly/publish/switch/
- Debian
sources.list(5)man page: https://manpages.debian.org/bookworm/apt/sources.list.5.en.html - nginx autoindex module: https://nginx.org/en/docs/http/ngx_http_autoindex_module.html
Top comments (0)