Python aptly offline debian mirrors
posted on 05 Oct 2025 under category devops
Date | Language | Author | Description |
---|---|---|---|
05.10.2025 | English | Claus Prüfer (Chief Prüfer) | Managing Offline Debian Mirrors with Python Aptly Module |
In high-security environments and mission-critical enterprise projects, dependency management presents a fundamental challenge. Online Debian repositories are continuously updated—packages are added, removed, or modified without notice. For projects requiring absolute reproducibility and long-term stability, this volatility is unacceptable.
The solution: snapshotted Debian HTTP mirrors managed through the Python aptly module.
The aptly module provides programmatic control over Debian repository snapshots, ensuring your project dependencies remain frozen throughout the entire project lifecycle.
Consider a typical scenario:
Common Issues:
A single missing package can cascade into hours or days of debugging and dependency resolution.
Aptly is a powerful Debian repository management tool that enables:
The Python aptly module wraps the aptly API, providing Pythonic access to these capabilities.
For High-Security Environments:
* Immutable dependency sets prevent unauthorized package changes
* Complete audit trail of all packages in use
* Air-gapped deployments with offline mirror data
* Reproducible builds across years, not just weeks
For Enterprise Projects:
* Guaranteed consistency across development, staging, and production
* Protection against upstream repository changes
* Simplified compliance and certification processes
* Rollback capability to previous package states
In regulated industries (finance, healthcare, defense), immutable package repositories are often a compliance requirement.
The aptly module interfaces with an aptly API server. First, set up the aptly server:
# install aptly
sudo apt-get update
sudo apt-get install aptly
# configure aptly
mkdir -p ~/.aptly
cat > ~/.aptly.conf <<EOF
{
"rootDir": "/var/aptly",
"downloadConcurrency": 4,
"downloadSpeedLimit": 0,
"architectures": ["amd64", "i386"],
"dependencyFollowSuggests": false,
"dependencyFollowRecommends": false,
"dependencyFollowAllVariants": false,
"dependencyFollowSource": false,
"dependencyVerboseResolve": false,
"gpgDisableSign": false,
"gpgDisableVerify": false,
"gpgProvider": "gpg",
"downloadSourcePackages": false,
"skipLegacyPool": true,
"ppaDistributorID": "ubuntu",
"ppaCodename": "",
"skipContentsPublishing": false,
"FileSystemPublishEndpoints": {},
"S3PublishEndpoints": {},
"SwiftPublishEndpoints": {}
}
EOF
Install the Python module:
pip install python-aptly
For production environments, run aptly API as a systemd service with proper authentication.
# start aptly API (development)
aptly api serve -listen=:8080
# production with authentication
aptly api serve -listen=127.0.0.1:8080 -no-lock
from aptly import Aptly
# connect to local aptly API
aptly = Aptly('http://localhost:8080')
# with authentication
aptly = Aptly('http://localhost:8080', auth=('username', 'password'))
# create a mirror of Ubuntu focal main repository
mirror_name = 'ubuntu-focal-main'
aptly.mirrors.create(
name=mirror_name,
archive_url='http://archive.ubuntu.com/ubuntu',
distribution='focal',
components=['main']
)
# update the mirror (downloads packages)
aptly.mirrors.update(mirror_name)
The key feature for immutability:
from datetime import datetime
# create a snapshot from the mirror
snapshot_name = f'ubuntu-focal-{datetime.now().strftime("%Y%m%d")}'
aptly.snapshots.create_from_mirror(
mirror_name=mirror_name,
snapshot_name=snapshot_name,
description='Production snapshot 2025-03-15'
)
# list all snapshots
snapshots = aptly.snapshots.list()
for snapshot in snapshots:
print(f"Snapshot: {snapshot['Name']} - Created: {snapshot['CreatedAt']}")
# publish snapshot as HTTP repository
aptly.publish.publish(
source_kind='snapshot',
sources=[{'Name': snapshot_name}],
prefix='ubuntu',
distribution='focal'
)
# update existing publication with new snapshot
aptly.publish.update(
prefix='ubuntu',
distribution='focal',
snapshots=[{'Component': 'main', 'Name': snapshot_name}]
)
Now your immutable repository is available at:
http://your-server:8080/ubuntu/dists/focal/
On client systems, configure /etc/apt/sources.list
:
# replace official repositories with your aptly mirror
deb http://aptly-server.yourdomain.com/production focal main
# deb http://archive.ubuntu.com/ubuntu focal main # disabled
For even greater control, different environments can use different snapshots:
Development (latest):
deb http://aptly-server.yourdomain.com/development focal main
Staging (weekly snapshot):
deb http://aptly-server.yourdomain.com/staging focal main
Production (monthly snapshot, thoroughly tested):
deb http://aptly-server.yourdomain.com/production focal main
# create snapshots from different mirrors
aptly.snapshots.create_from_mirror('ubuntu-focal-main', 'snap-main-20250315')
aptly.snapshots.create_from_mirror('ubuntu-focal-universe', 'snap-universe-20250315')
# merge snapshots
aptly.snapshots.merge(
destination='ubuntu-focal-combined-20250315',
sources=['snap-main-20250315', 'snap-universe-20250315'],
description='Combined main and universe repositories'
)
# create filtered snapshot with only specific packages
aptly.snapshots.filter(
source='ubuntu-focal-combined-20250315',
destination='ubuntu-focal-minimal-20250315',
package_refs=['nginx', 'postgresql-14', 'python3.8'],
with_deps=True # Include dependencies
)
Always sign your published repositories:
# generate GPG key for repository signing
gpg --gen-key
# configure aptly to use the key
aptly publish snapshot \
-gpg-key="YOUR_KEY_ID" \
snap-20250315 \
focal
Production Setup:
# use HTTPS with authentication
aptly = Aptly(
'https://aptly.yourdomain.com:8443',
auth=('admin', 'secure_password'),
verify_ssl=True
)
Nginx Reverse Proxy:
server {
listen 443 ssl;
server_name aptly.yourdomain.com;
ssl_certificate /etc/ssl/certs/aptly.crt;
ssl_certificate_key /etc/ssl/private/aptly.key;
location / {
auth_basic "Aptly Repository";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:8080;
}
}
In high-security environments, run aptly on an isolated network segment accessible only via VPN.
FROM ubuntu:22.04
# install aptly
RUN apt-get update && \
apt-get install -y aptly python3-pip gnupg && \
pip3 install python-aptly
# copy configuration
COPY aptly.conf /etc/aptly.conf
COPY scripts/ /opt/aptly-scripts/
# expose API port
EXPOSE 8080
CMD ["aptly", "api", "serve", "-listen=:8080"]
Traditional Docker workflows often include package installations directly in Dockerfiles:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
postgresql-client \
nginx \
redis-tools \
git \
curl \
vim \
htop \
# ... potentially dozens more packages
Problems with this approach:
Instead of managing individual packages in Dockerfiles, use the Debian package management system (debuild
) to create meta-packages that declare all dependencies.
Key Benefits:
* Single meta-package manages all container dependencies
* Version control for entire dependency sets
* Automated dependency resolution
* Consistent across all environments
* Trivial updates via single package version bump
* Perfect integration with aptly snapshots
Directory structure:
myapp-deps/
├── debian/
│ ├── control # Package metadata and dependencies
│ ├── changelog # Version history
│ ├── compat # Debhelper compatibility level
│ └── rules # Build instructions
debian/control example:
Source: myapp-deps
Section: metapackages
Priority: optional
Maintainer: DevOps Team <devops@example.com>
Build-Depends: debhelper (>= 10)
Standards-Version: 4.1.2
Package: myapp-deps
Architecture: all
Depends:
python3 (>= 3.8),
python3-pip,
postgresql-client-14,
nginx (>= 1.18),
redis-tools,
git,
curl,
vim,
htop,
libpq-dev,
build-essential
Description: Meta-package for MyApp container dependencies
This meta-package installs all required dependencies for the
MyApp containerized application environment.
debian/changelog:
myapp-deps (1.2.0) focal; urgency=medium
* Added libpq-dev for PostgreSQL development
* Upgraded nginx minimum version to 1.18
* Added build-essential for compilation tools
-- DevOps Team <devops@example.com> Mon, 15 Mar 2025 10:00:00 +0000
debian/rules:
#!/usr/bin/make -f
%:
dh $@
debian/compat:
10
# navigate to package directory
cd myapp-deps
# build the package
debuild -us -uc -b
# output: myapp-deps_1.2.0_all.deb
from aptly import Aptly
aptly = Aptly('http://localhost:8080')
# add package to local repository
aptly.files.upload('myapp-deps', '/path/to/myapp-deps_1.2.0_all.deb')
aptly.repos.add_packages_from_dir('myapp-repo', 'myapp-deps')
# create snapshot
aptly.snapshots.create_from_repo(
repo_name='myapp-repo',
snapshot_name='myapp-deps-1.2.0',
description='MyApp dependencies v1.2.0'
)
# publish snapshot
aptly.publish.publish(
source_kind='snapshot',
sources=[{'Name': 'myapp-deps-1.2.0'}],
prefix='myapp',
distribution='focal'
)
Before (traditional approach):
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
postgresql-client \
nginx \
redis-tools \
git \
curl \
vim \
htop \
# ... many more packages
# application setup
COPY app/ /app/
WORKDIR /app
After (meta-package approach):
FROM ubuntu:22.04
# configure custom aptly mirror
RUN echo "deb http://aptly.example.com/myapp focal main" > /etc/apt/sources.list.d/myapp.list && \
apt-get update && \
apt-get install -y myapp-deps
# application setup
COPY app/ /app/
WORKDIR /app
The Dockerfile is now dramatically simpler and declares dependencies via a single meta-package.
Updating dependencies:
debian/control
to add/update packagesdebian/changelog
with new version (e.g., 1.3.0)debuild -us -uc -b
Rollback capability:
# install specific version
RUN apt-get install -y myapp-deps=1.2.0
# or use previous snapshot
RUN echo "deb http://aptly.example.com/myapp-archive/1.2.0 focal main" > /etc/apt/sources.list.d/myapp.list
Time savings example:
Without debuild meta-packages:
* 50 containers × 20 packages each = 1000 package references
* Update 1 package = manually update 50 Dockerfiles
* Verify consistency = manual inspection of 50 files
* Time per update: 2-4 hours
With debuild meta-packages:
* 50 containers × 1 meta-package each = 50 package references
* Update 1 package = modify 1 control file, rebuild, publish
* Verify consistency = automatic via dependency resolution
* Time per update: 15-30 minutes
Result: 80-90% time reduction on dependency management tasks across the DevOps team.
For smaller environments it is also advisable to copy the .deb
meta-package file directly into the Docker image and install it locally without requiring an Aptly mirror.
FROM ubuntu:22.04
# copy debian package to /
ARG APP_DEB_FILE=myapp-deps_1.2.0_all.deb
COPY ./$APP_DEB_FILE ./
# install debian package by running apt
RUN apt-get -qq install -y ./$APP_DEB_FILE
Our x0 framework is using this approach: WEBcodeX1/x0.
The true power emerges when combining debuild meta-packages with aptly snapshots:
Workflow:
myapp-deps
latest versionmyapp-deps-1.2.0-staging
)myapp-deps-1.2.0-production
)Example multi-environment setup:
# development snapshot (updated weekly)
aptly.publish.publish(
source_kind='snapshot',
sources=[{'Name': 'myapp-deps-latest'}],
prefix='myapp-dev',
distribution='focal'
)
# staging snapshot (updated monthly after testing)
aptly.publish.publish(
source_kind='snapshot',
sources=[{'Name': 'myapp-deps-1.2.0-staging'}],
prefix='myapp-staging',
distribution='focal'
)
# production snapshot (updated quarterly with full certification)
aptly.publish.publish(
source_kind='snapshot',
sources=[{'Name': 'myapp-deps-1.1.0-production'}],
prefix='myapp-prod',
distribution='focal'
)
Environment-specific Dockerfiles:
# dockerfile.dev
FROM ubuntu:22.04
RUN echo "deb http://aptly.example.com/myapp-dev focal main" > /etc/apt/sources.list.d/myapp.list && \
apt-get update && apt-get install -y myapp-deps
# dockerfile.staging
FROM ubuntu:22.04
RUN echo "deb http://aptly.example.com/myapp-staging focal main" > /etc/apt/sources.list.d/myapp.list && \
apt-get update && apt-get install -y myapp-deps=1.2.0
# dockerfile.prod
FROM ubuntu:22.04
RUN echo "deb http://aptly.example.com/myapp-prod focal main" > /etc/apt/sources.list.d/myapp.list && \
apt-get update && apt-get install -y myapp-deps=1.1.0
Meta-package naming:
* Use descriptive names: myapp-deps, myapp-build-deps, myapp-runtime
* Separate build and runtime dependencies when possible
* Version semantically: major.minor.patch
Dependency specification:
* Pin critical package versions: postgresql-client-14
* Use minimum versions where flexibility needed: python3 (>= 3.8)
* Avoid unnecessary constraints for stability
Testing:
* Test meta-package installation in clean container
* Verify all dependencies resolve correctly
* Run application test suite with new dependencies
* Validate across all target environments
Case Study: 50-Container Microservices Architecture
Before debuild + aptly:
After debuild + aptly:
Annual savings: ~430 hours of DevOps time = $50,000+ in productivity
More time for coffee, less time managing package lists in Dockerfiles.
The Python aptly module transforms repository management from a liability into an asset. For high-security environments, long-term projects, or any scenario requiring reproducible builds, immutable package snapshots are not optional—they’re essential.
Key Takeaways:
Start your next project with aptly snapshots. Your future self (and your team) will thank you.
Coming soon: “AI Enhanced Web-Development - The Model Problem”