Tools: Freeze Your Linux Package State: Reproducible APT Mirrors with aptly Snapshots - Analysis

Tools: Freeze Your Linux Package State: Reproducible APT Mirrors with aptly Snapshots - Analysis

Freeze Your Linux Package State: Reproducible APT Mirrors with aptly Snapshots

Why this is different from a caching proxy

What aptly gives you

The lab setup

Step 1: Install aptly, nginx, and GnuPG

Step 2: Create a signing key for your repository

Step 3: Create the upstream mirror

Step 4: Create an immutable snapshot

Step 5: Publish the snapshot as your repository

Step 6: Serve the published repo with nginx

Step 7: Configure a client safely with signed-by

Step 8: Roll out updates on your schedule

Step 9: Roll back fast if an update breaks something

A practical systemd timer for snapshot refreshes

Step 10: Verify what clients will actually install

Storage and cleanup notes

When this pattern is worth it

Final thoughts

References 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: This is where aptly becomes genuinely useful. Instead of treating upstream repositories as a live stream, aptly lets you: That changes package management from “whatever upstream serves right now” to “the exact package set I approved.” 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: If your goal is reproducible patch windows, auditability, or fast rollback, snapshots are the tool you want. 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: That is a very different operational model from pointing every machine directly at deb.debian.org. I’ll use Debian Bookworm as the example, but the workflow applies to Ubuntu too. Example mirror host URL: Check the version so you know what you are operating: APT clients should trust your repository key, not blindly trust unsigned metadata. Create a dedicated signing key: Export the public key for clients: Then install it in a place you can serve with nginx: Create a mirror for Debian Bookworm main on amd64: Now download the current repository state: That first sync can take time and disk space. The payoff is that you now control when your downstream systems see change. After the mirror is updated, create a timestamped snapshot: This is the key idea: the snapshot does not change, even after the mirror is updated later. Publish it under a debian prefix and explicitly set distribution/component values so the result is obvious to clients: By default, local publishes appear under aptly’s public directory. A common local path is: On many systems that resolves to: Create an nginx server block like this: Enable and validate it: A quick verification: 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. In production, serve the repository over HTTPS. On a client, install your exported public key into a dedicated local keyring file: 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. When you are ready for a new patch window: 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. Let’s say the new snapshot causes trouble. Find the last known-good snapshot: 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. If you want a controlled daily ingest on the mirror host, use a oneshot service plus timer. I prefer separating snapshot creation from publishing the switch. That gives you a buffer for validation: That is safer than auto-promoting every upstream change straight to production. Before promoting a new snapshot broadly, verify package candidates from a test client: That makes it obvious which version your published snapshot is offering before you upgrade production machines. 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. When old snapshots are no longer needed, remove them deliberately: If you are unsure whether something is still referenced, inspect your published repos before deleting. This setup is worth the operational cost when you care about: If you only want to reduce repeated package downloads, a caching proxy is simpler. If you want deterministic package state, snapshots win. 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

https://repo.example.com/debian/ https://repo.example.com/debian/ https://repo.example.com/debian/ sudo apt update sudo apt install -y aptly nginx gpg sudo apt update sudo apt install -y aptly nginx gpg sudo apt update sudo apt install -y aptly nginx gpg aptly version nginx -v gpg --version | head -n 1 aptly version nginx -v gpg --version | head -n 1 aptly version nginx -v gpg --version | head -n 1 gpg --quick-gen-key "Homelab Repo Signing Key <[email protected]>" rsa4096 sign 1y gpg --quick-gen-key "Homelab Repo Signing Key <[email protected]>" rsa4096 sign 1y gpg --quick-gen-key "Homelab Repo Signing Key <[email protected]>" rsa4096 sign 1y gpg --list-secret-keys --keyid-format long gpg --list-secret-keys --keyid-format long gpg --list-secret-keys --keyid-format long gpg --armor --export "Homelab Repo Signing Key <[email protected]>" > repo-signing-key.asc gpg --armor --export "Homelab Repo Signing Key <[email protected]>" > repo-signing-key.asc gpg --armor --export "Homelab Repo Signing Key <[email protected]>" > repo-signing-key.asc sudo install -d -m 0755 /var/www/repo sudo install -m 0644 repo-signing-key.asc /var/www/repo/repo-signing-key.asc sudo install -d -m 0755 /var/www/repo sudo install -m 0644 repo-signing-key.asc /var/www/repo/repo-signing-key.asc sudo install -d -m 0755 /var/www/repo sudo install -m 0644 repo-signing-key.asc /var/www/repo/repo-signing-key.asc aptly -architectures="amd64" \ mirror create debian-bookworm-main \ https://deb.debian.org/debian/ \ bookworm \ main aptly -architectures="amd64" \ mirror create debian-bookworm-main \ https://deb.debian.org/debian/ \ bookworm \ main aptly -architectures="amd64" \ mirror create debian-bookworm-main \ https://deb.debian.org/debian/ \ bookworm \ main aptly mirror update debian-bookworm-main aptly mirror update debian-bookworm-main aptly mirror update debian-bookworm-main SNAPSHOT="bookworm-main-$(date -u +%Y%m%d)" aptly snapshot create "$SNAPSHOT" from mirror debian-bookworm-main SNAPSHOT="bookworm-main-$(date -u +%Y%m%d)" aptly snapshot create "$SNAPSHOT" from mirror debian-bookworm-main SNAPSHOT="bookworm-main-$(date -u +%Y%m%d)" aptly snapshot create "$SNAPSHOT" from mirror debian-bookworm-main aptly snapshot list aptly snapshot list aptly snapshot list aptly publish snapshot \ -distribution="bookworm" \ -component="main" \ "$SNAPSHOT" \ debian aptly publish snapshot \ -distribution="bookworm" \ -component="main" \ "$SNAPSHOT" \ debian aptly publish snapshot \ -distribution="bookworm" \ -component="main" \ "$SNAPSHOT" \ debian ~/.aptly/public ~/.aptly/public ~/.aptly/public /home/<user>/.aptly/public /home/<user>/.aptly/public /home/<user>/.aptly/public 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; } } 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; } } 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; } } sudo ln -s /etc/nginx/sites-available/repo.example.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx sudo ln -s /etc/nginx/sites-available/repo.example.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx sudo ln -s /etc/nginx/sites-available/repo.example.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx curl -I http://repo.example.com/debian/dists/bookworm/Release curl -I http://repo.example.com/repo-signing-key.asc curl -I http://repo.example.com/debian/dists/bookworm/Release curl -I http://repo.example.com/repo-signing-key.asc curl -I http://repo.example.com/debian/dists/bookworm/Release curl -I http://repo.example.com/repo-signing-key.asc 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 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 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 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 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 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 sudo apt update apt-cache policy | sed -n '/repo.example.com/,+4p' sudo apt update apt-cache policy | sed -n '/repo.example.com/,+4p' sudo apt update apt-cache policy | sed -n '/repo.example.com/,+4p' 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" 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" 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" aptly snapshot list aptly snapshot list aptly snapshot list aptly publish switch bookworm debian bookworm-main-20260404 aptly publish switch bookworm debian bookworm-main-20260404 aptly publish switch bookworm debian bookworm-main-20260404 sudo apt update sudo apt upgrade sudo apt update sudo apt upgrade sudo apt update sudo apt upgrade # /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 ' # /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 ' # /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 ' # /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 # /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 # /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 sudo systemctl daemon-reload sudo systemctl enable --now aptly-snapshot-refresh.timer sudo systemctl list-timers aptly-snapshot-refresh.timer sudo systemctl daemon-reload sudo systemctl enable --now aptly-snapshot-refresh.timer sudo systemctl list-timers aptly-snapshot-refresh.timer sudo systemctl daemon-reload sudo systemctl enable --now aptly-snapshot-refresh.timer sudo systemctl list-timers aptly-snapshot-refresh.timer apt-cache policy openssl apt-cache madison openssl apt-cache policy openssl apt-cache madison openssl apt-cache policy openssl apt-cache madison openssl du -sh ~/.aptly aptly mirror list aptly snapshot list aptly publish list du -sh ~/.aptly aptly mirror list aptly snapshot list aptly publish list du -sh ~/.aptly aptly mirror list aptly snapshot list aptly publish list aptly snapshot drop old-snapshot-name aptly snapshot drop old-snapshot-name aptly snapshot drop old-snapshot-name - 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 - 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 - Cache: makes downloads faster - Snapshot mirror: makes package state deterministic - 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 - mirror host: runs aptly, gpg, and nginx - client hosts: consume the published repository over HTTP - update the upstream mirror - create a new snapshot - switch the published repo to that snapshot - automation creates the candidate snapshot - you test it on staging - you run aptly publish switch only after approval - 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 - 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