aptly
Overview
aptly is a self-hosted tool for managing Debian and Ubuntu package repositories. It mirrors upstream APT repositories to a fast local copy, snapshots them so a fleet always installs an identical, reproducible set of packages, and publishes your own GPG-signed repositories that any apt client can verify and consume.
This guide covers deploying and configuring aptly on Ubuntu 24.04 on Azure, using the SecureAnyCloud image from the Azure Marketplace. The image installs aptly 1.6.3 from the official upstream release (a single self-contained Go binary) and runs it under systemd behind nginx. The aptly REST API is bound to the loopback interface only (127.0.0.1:8080), and nginx is the sole network-facing surface.
Nothing secret is baked into the image. On the first boot of every instance, aptly generates a unique GPG signing key, a unique HTTP basic authentication password for the management API and a unique self-signed TLS certificate. It then publishes a demonstration repository signed with that per-instance key.
What you get:
- aptly 1.6.3 from the official upstream release binary, run under systemd (
aptly.service) as an unprivilegedaptlyuser with the REST API bound to127.0.0.1:8080. aptly has no first-party web interface, so you drive it with the command line and the REST API. - nginx (
nginx.service) terminating TLS on port 443 as the only network-facing service. It fronts the management API with HTTP basic authentication and serves your published repositories read-only to APT clients. - A per-instance GPG signing key, management API password and TLS certificate, all generated fresh on first boot, with the per-instance credentials written to
/root/aptly-credentials.txt - A signed demonstration repository published on first boot, and the exported public key at
/aptly-repo-key.ascso clients can trust it - The full aptly command line and REST API for mirroring, snapshotting and publishing your own repositories
- Repository trust that comes from GPG signatures, which is the standard APT security model. Serving the repositories read-only over HTTPS is therefore normal and safe, and the management API cannot be reached without the per-instance password.
Prerequisites
Before you deploy this image, make sure you have:
- An active Azure subscription, an SSH public key, and a VNet plus subnet in the target region
- A subscription to the aptly listing on the Azure Marketplace
- Network Security Group rules that allow TCP 22 (admin) and TCP 443 (HTTPS, for both the published repositories and the management API) from the clients and administrators that need them
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for a departmental repository server. Mirroring large upstream repositories is disk and bandwidth intensive, so increase the size and attach a larger data disk for that workload.
Deploy from the Azure Portal
Search the Marketplace for aptly, select the SecureAnyCloud publisher, and select Create.
Configure the Network Security Group to allow TCP 443 from the client machines and build agents that will consume the repositories, and TCP 22 for administration. Keep port 443 restricted to the networks you serve. A package repository rarely needs to be reachable from the whole internet.
Deploy from the Azure CLI
RG="aptly-prod"; LOCATION="eastus"; VM_NAME="aptly1" IMAGE_URN="sac1647283583153:aptly-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:aptly-ubuntu-24-04:default:latest
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$IMAGE_URN" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 443 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1002First boot and per-instance credentials
On first boot, the image resolves this VM's IP and generates a per-instance GPG signing key, a per-instance management API password and a per-instance TLS certificate. It publishes the signed demonstration repository and writes everything to /root/aptly-credentials.txt. This completes within seconds. SSH in as azureuser and read the credentials file:
sudo cat /root/aptly-credentials.txtThe file is 0600 root:root. It records the API user and password, the repository and API URLs, the exported public key URL and the GPG key ID. These values are unique to this instance.
Confirm the services are running
aptly.service and nginx.service are both active, aptly version reports 1.6.3, and ss confirms that nginx owns port 443 while the aptly REST API is bound to loopback only on port 8080.
systemctl is-active aptly.service nginx.service /opt/aptly/aptly version
sudo ss -tlnp | grep -E ':443 |:8080 'Use the management API
The management REST API is reachable only through nginx on port 443, and only with the per-instance HTTP basic authentication. An unauthenticated request returns 401, and a request with the per-instance credentials returns 200. The block below reads the credentials from the instance credentials file, so it works unchanged on any instance.
API_USER=$(sudo grep -E '^APTLY_API_USER=' /root/aptly-credentials.txt | cut -d= -f2-)
API_PASS=$(sudo grep -E '^APTLY_API_PASSWORD=' /root/aptly-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'no auth -> HTTP %{http_code}\n' https://127.0.0.1/api/version
curl -sk -u "$API_USER:$API_PASS" https://127.0.0.1/api/version; echo
curl -sk -u "$API_USER:$API_PASS" https://127.0.0.1/api/repos; echoThe -k flag tells curl to accept the self-signed per-instance certificate. The /api/repos call lists the repositories aptly is managing, starting with the demo repository published on first boot.
The published, signed repository
The demonstration repository is published and served read-only over HTTPS. Fetch its Release file and its Packages index, both of which are plain files that any apt client downloads.
curl -sk https://127.0.0.1/dists/stable/Release
curl -sk https://127.0.0.1/dists/stable/main/binary-amd64/PackagesThe Release file describes the distribution (stable), its components (main) and architectures (amd64), and the Packages index lists the demonstration package sac-aptly-demo that was added on first boot.
Verify the GPG signature
The repository is signed by the per-instance GPG key, and the public key is exported so clients can verify it. Download the signed InRelease file and the public key, then verify the signature with gpgv.
curl -sk https://127.0.0.1/dists/stable/InRelease -o /tmp/InRelease
curl -sk https://127.0.0.1/aptly-repo-key.asc -o /tmp/aptly-key.asc gpg --dearmor < /tmp/aptly-key.asc > /tmp/aptly-key.gpg gpgv --keyring /tmp/aptly-key.gpg /tmp/InReleasegpgv reports a good signature from the per-instance signing key. This is exactly the check apt performs on every repository update, and it is the integrity guarantee that lets the repositories be served read-only.
Add the repository on a client
On any Ubuntu or Debian client, trust the exported public key and add the repository as a signed apt source. Replace <vm-ip> with this instance's address, or with a DNS name that you point at it.
curl -fsSL https://<vm-ip>/aptly-repo-key.asc | sudo gpg --dearmor -o /etc/apt/keyrings/aptly-repo.gpg echo "deb [signed-by=/etc/apt/keyrings/aptly-repo.gpg] https://<vm-ip>/ stable main" | sudo tee /etc/apt/sources.list.d/sac-aptly.list
sudo apt-get updateFor production, put a real DNS name and a CA-signed certificate in front of the repository, replacing the per-instance self-signed certificate in /etc/nginx/tls. Clients then do not need to trust a self-signed certificate.
Create and publish your own repository
You manage aptly through its REST API. The examples below create a repository. You upload packages and publish through the API as well. Replace <vm-ip> and the credentials with the values for your instance.
# create a new local repository via the authenticated API curl -sk -u "$API_USER:$API_PASS" -X POST -H 'Content-Type: application/json' \
-d '{"Name":"myrepo","DefaultDistribution":"noble","DefaultComponent":"main"}' \
https://<vm-ip>/api/repos
# upload a package file, add it to the repo, then publish it signed with the per instance key
# (see the aptly REST API docs: https://www.aptly.info/doc/api/ )The aptly command line is also available for interactive use. The API server holds the aptly database while it runs, so stop it with sudo systemctl stop aptly before you run aptly commands directly, then start it again afterwards. For automation, prefer the REST API.
Mirror and snapshot upstream repositories
Mirroring and snapshotting are aptly's core strengths. Create a mirror of an upstream repository, update it to pull the latest packages, snapshot it for a reproducible point in time, and publish the snapshot. Your fleet then installs from the published snapshot, so every machine gets an identical set of packages until you deliberately roll forward to a newer snapshot.
See the aptly documentation at https://www.aptly.info/doc/ for the mirror, snapshot and publish workflow. All of it is available through the REST API on this instance.
Security recommendations
Restrict the NSG. Allow TCP 443 only from the client subnets and build agents that consume the repositories, and TCP 22 only from your administration network. A package repository rarely needs to be public.
Protect the management API password. It lives in /root/aptly-credentials.txt (0600 root:root). The management API is reachable only through nginx and only with this password. Rotate it by updating /etc/nginx/aptly.htpasswd.
Use a real certificate for production. The per-instance certificate is self-signed. Put a CA-signed certificate and a DNS name in front of the repository so clients trust the transport without extra configuration.
Guard the signing key. The per-instance GPG signing key lives in the aptly user's keyring under /var/lib/aptly/.gnupg. Back it up securely, because anyone who holds it can sign packages that your clients will trust.
Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Support and Licensing
aptly is open-source software distributed under the MIT License. This SecureAnyCloud image bundles the unmodified official aptly release binary. SecureAnyCloud provides the packaging, the systemd hardening, the nginx TLS front, the loopback-only API with per-instance basic authentication, the per-instance signing key and credential automation, the paired deployment guide, and 24/7 support with a guaranteed 24-hour response SLA.
SecureAnyCloud is not affiliated with or endorsed by the aptly project. aptly is a mark of its respective owner and is used here only to identify the software.
Deploy on Azure
Find aptly on Ubuntu 24.04 LTS on the Azure Marketplace, published by SecureAnyCloud. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email sales@secureanycloud.com for deployment help, configuration questions or licensing enquiries.
Deploy on Azure
Launch aptly 1.6.3 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
aptly is a self-hosted tool for managing APT package repositories. It mirrors upstream repositories to a fast local copy, snapshots them so a fleet always installs an identical, reproducible set of packages, and publishes your own GPG-signed repositories that any apt client can verify and consume.
This guide covers deploying and operating aptly on AWS, using the SecureAnyCloud listing on the AWS Marketplace. The image installs aptly 1.6.3 from the official upstream release (a single self-contained Go binary, MIT licensed) and runs it under systemd behind nginx. The aptly REST API is bound to the loopback interface only (127.0.0.1:8080), and nginx is the sole network-facing surface.
Nothing secret is baked into the image. On the first boot of every instance, aptly generates a unique GPG signing key, a unique HTTP basic authentication password for the management API and a unique self-signed TLS certificate. It then publishes a demonstration repository signed with that per-instance key.
What you get:
- aptly 1.6.3 from the official upstream release binary, run under systemd (
aptly.service) as an unprivilegedaptlyuser with the REST API bound to127.0.0.1:8080. aptly has no first-party web interface, so you drive it with the command line and the REST API. - nginx (
nginx.service) terminating TLS on port 443 as the only network-facing service. It fronts the management API with HTTP basic authentication and serves your published repositories read-only to apt clients. Port 80 redirects to HTTPS. - A per-instance GPG signing key, management API password and TLS certificate, all generated fresh on first boot, with the per-instance values written to
/root/aptly-credentials.txt(mode 0600 root:root) - A signed demonstration repository published on first boot, and the exported public key at
/aptly-repo-key.ascso clients can trust it - The full aptly command line and REST API for mirroring, snapshotting and publishing your own repositories
- Repository trust that comes from GPG signatures, which is the standard apt security model. Serving the repositories read-only over HTTPS is therefore normal and safe, and the management API cannot be reached without the per-instance password.
- 24/7 SecureAnyCloud support
Prerequisites
You need an AWS account, an EC2 key pair and a VPC subnet. The security group should allow inbound TCP 22 (SSH) from your administration network. It should also allow TCP 443 (published repositories and the management API) and TCP 80 (redirect to HTTPS) from the client machines and build agents that consume your repositories. A package repository rarely needs to be reachable from the whole internet.
m5.large is a comfortable default. aptly itself is a small Go binary, so disk and bandwidth drive sizing, and mirroring large upstream repositories is storage intensive. Grow the root volume, or attach and mount your own EBS volume at /var/lib/aptly, before you start mirroring.
Connecting to your instance
SSH in as the default login user for the operating system variant you launched. This listing currently ships one variant:
- Ubuntu 24.04 LTS: the SSH login user is ubuntu
ssh ubuntu@<public-ip>Launch from AWS Marketplace
Subscribe to the SecureAnyCloud aptly listing and launch an instance. Pick m5.large and choose your VPC and subnet. Attach a security group that allows inbound TCP 22 from your administration network and TCP 443 and 80 from the hosts that will consume the repositories.
First boot and per-instance credentials
On first boot, the instance resolves its own public address from EC2 instance metadata. It generates a per-instance GPG signing key, a per-instance management API password and a per-instance TLS certificate, publishes the signed demonstration repository, and writes everything to /root/aptly-credentials.txt. This completes within seconds of the instance becoming reachable.
The file is 0600 root:root. The block below lists it and prints the non-secret entries. Read the API password itself with sudo cat /root/aptly-credentials.txt when you need it, and keep it out of your terminal history and out of screenshots.
sudo ls -l /root/aptly-credentials.txt
sudo grep -E '^APTLY_(API_USER|API_URL|REPO_URL|REPO_DISTRIBUTION|REPO_COMPONENT|REPO_PUBKEY|GPG_KEYID)=' /root/aptly-credentials.txt-rw------- 1 root root 1494 Jul 25 20:58 /root/aptly-credentials.txt APTLY_API_USER=aptly APTLY_API_URL=https://54.163.186.39/api/version APTLY_REPO_URL=https://54.163.186.39/ APTLY_REPO_DISTRIBUTION=stable APTLY_REPO_COMPONENT=main APTLY_REPO_PUBKEY=https://54.163.186.39/aptly-repo-key.asc APTLY_GPG_KEYID=BB9D786F6E7D0B13Every value above is unique to the instance, including the GPG key ID. Two instances launched from this listing never share a signing key or an API password.
Confirm the services are running
aptly.service and nginx.service are both active, aptly version reports 1.6.3, and ss confirms that nginx owns port 443 while the aptly REST API is bound to loopback only on port 8080.
sudo systemctl is-active aptly nginx /opt/aptly/aptly version
sudo ss -tlnp | grep -E ':443 |:8080 'active active aptly version: 1.6.3 LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("aptly",pid=2841,fd=3)) LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=2854,fd=7),("nginx",pid=2853,fd=7),("nginx",pid=2852,fd=7)) LISTEN 0 511 [::]:443 [::]:* users:(("nginx",pid=2854,fd=8),("nginx",pid=2853,fd=8),("nginx",pid=2852,fd=8))Use the management API
The management REST API is reachable only through nginx on port 443, and only with the per-instance HTTP basic authentication. An unauthenticated request returns 401, and a request with the per-instance credentials returns 200. The block below reads the credentials into shell variables straight from the instance credentials file, so it works unchanged on any instance and never prints the password.
API_USER=$(sudo grep -E '^APTLY_API_USER=' /root/aptly-credentials.txt | cut -d= -f2-)
API_PASS=$(sudo grep -E '^APTLY_API_PASSWORD=' /root/aptly-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'no auth -> HTTP %{http_code}\n' https://127.0.0.1/api/version
curl -sk -u "$API_USER:$API_PASS" -w '\n' https://127.0.0.1/api/version
curl -sk -u "$API_USER:$API_PASS" -w '\n' https://127.0.0.1/api/reposno auth -> HTTP 401 {"Version":"1.6.3"} [{"Name":"demo","Comment":"","DefaultDistribution":"stable","DefaultComponent":"main","NumPackages":1}]The -k flag tells curl to accept the self-signed per-instance certificate. The /api/repos call lists the repositories aptly is managing, starting with the demo repository that was published on first boot with one package in it.
Confirm the API refuses unauthenticated callers
This is the security property that matters most on a repository server, so test it rather than assume it. The block below expects the call to be refused and exits 0 only when it is.
if curl -skf -o /dev/null https://127.0.0.1/api/repos; then echo "UNEXPECTED: unauthenticated management API answered" exit 1 else echo "unauthenticated management API refused (exit $?) - correct" fiunauthenticated management API refused (exit 22) - correctThe aptly API process itself never accepts a connection from the network. The block below lists every listening socket on the instance. Port 8080 appears only on 127.0.0.1, so the only route to the API is through the authenticated nginx location.
sudo ss -tlnH | awk '{print $1, $4}' | sort -uLISTEN 0.0.0.0:22 LISTEN 0.0.0.0:443 LISTEN 0.0.0.0:80 LISTEN 127.0.0.1:8080 LISTEN 127.0.0.53%lo:53 LISTEN 127.0.0.54:53 LISTEN [::]:22 LISTEN [::]:443 LISTEN [::]:80The published, signed repository
The demonstration repository is published and served read-only over HTTPS. Fetch its Release file and its Packages index, both of which are plain files that any apt client downloads.
curl -sk https://127.0.0.1/dists/stable/Release
curl -sk https://127.0.0.1/dists/stable/main/binary-amd64/PackagesOrigin: . stable Label: . stable Suite: stable Codename: stable Date: Sat, 25 Jul 2026 20:58:04 UTC Architectures: amd64 Components: main Description: Created by aptly MD5Sum: 42cee938057afe0a991fee5a87411f45 80 Contents-amd64 84645f00a5c752853d346b2a420ae1dd 844 main/binary-amd64/Packages f69f4af4b8eaad2b01c382828976c4d1 116 main/binary-amd64/Release SHA256: 1d2b4c0e43d010b934556a98cefca50bb96f4c269bc335ee35b0d2c0cd7818fd 80 Contents-amd64 5b21ecafd3853536764c447c879133502063e4405c5024fff8bee574ea022e8b 844 main/binary-amd64/Packages c0cb6579869bb1ecd9caf087b2bc55a4d7a5697c189a7d6d18bd16dac868ed3c 116 main/binary-amd64/Release Package: sac-aptly-demo Priority: optional Section: misc Maintainer: sac <sales@secureanycloud.com> Architecture: all Version: 1.0 Filename: pool/main/c/sac-aptly-demo/sac-aptly-demo_1.0_all.deb Size: 880 SHA256: 094431ca5ff466e4cd287c36084f251bc99053c31a30c88837a92a84e5495189 Description: sac aptly demonstration packageThe Release file describes the distribution (stable), its component (main) and architecture (amd64) with a checksum for every index file, and the Packages index lists the demonstration package sac-aptly-demo that was added on first boot. The listing above is abridged. The real files also carry SHA1 and SHA512 sections and the compressed index variants.
Verify the GPG signature
The repository is signed by the per-instance GPG key, and the public key is exported so clients can verify it. Download the signed InRelease file and the public key, then verify the signature with gpgv.
WORK=$(mktemp -d)
curl -sk https://127.0.0.1/dists/stable/InRelease -o "$WORK/InRelease"
curl -sk https://127.0.0.1/aptly-repo-key.asc -o "$WORK/aptly-key.asc" gpg --dearmor -o "$WORK/aptly-key.gpg" "$WORK/aptly-key.asc" gpgv --keyring "$WORK/aptly-key.gpg" "$WORK/InRelease"gpgv: Signature made Sat Jul 25 20:58:04 2026 UTC gpgv: using RSA key 8A52E9C4AFEA266767802B00BB9D786F6E7D0B13 gpgv: Good signature from "sac aptly repository signing key <aptly-repo@ip-172-31-82-48>"gpgv reports a good signature from the per-instance signing key. This is exactly the check apt performs on every repository update, and it is the integrity guarantee that lets the repositories be served read-only. The key is an RSA 3072-bit signing key generated on this instance and on no other.
Add the repository on a client
On any apt-based client, trust the exported public key and add the repository as a signed apt source. Replace <public-ip> with this instance's address, or with a DNS name that you point at it.
curl -fsSL https://<public-ip>/aptly-repo-key.asc | sudo gpg --dearmor -o /etc/apt/keyrings/aptly-repo.gpg echo "deb [signed-by=/etc/apt/keyrings/aptly-repo.gpg] https://<public-ip>/ stable main" | sudo tee /etc/apt/sources.list.d/sac-aptly.list
sudo apt-get update
sudo apt-get install sac-aptly-demoFor production, put a real DNS name and a CA-signed certificate in front of the repository, replacing the per-instance self-signed certificate in /etc/nginx/tls. Clients then do not need --insecure-style exceptions to trust the transport.
Create and publish your own repository
You manage aptly through its REST API. The example below creates a repository through the authenticated API. You upload packages and publish through the API as well. Replace <public-ip> and use the credentials for your instance.
curl -sk -u "$API_USER:$API_PASS" -X POST -H 'Content-Type: application/json' \
-d '{"Name":"myrepo","DefaultDistribution":"noble","DefaultComponent":"main"}' \
https://<public-ip>/api/reposThe aptly command line is also available for interactive use at /opt/aptly/aptly. The API server holds aptly's embedded database while it runs, so stop it with sudo systemctl stop aptly before you run aptly commands directly, then start it again afterwards. For automation, prefer the REST API.
Mirror and snapshot upstream repositories
Mirroring and snapshotting are aptly's core strengths. Create a mirror of an upstream repository, update it to pull the latest packages, snapshot it for a reproducible point in time, and publish the snapshot. Your fleet then installs from the published snapshot, so every machine gets an identical set of packages until you deliberately roll forward to a newer snapshot.
The full mirror, snapshot and publish workflow is documented at https://www.aptly.info/doc/. Every step of it is available through the REST API on this instance.
curl -sk -u "$API_USER:$API_PASS" -X POST -H 'Content-Type: application/json' \
-d '{"Name":"upstream-noble","ArchiveURL":"http://archive.ubuntu.com/ubuntu/","Distribution":"noble","Components":["main"],"Architectures":["amd64"]}' \
https://<public-ip>/api/mirrorsSizing the repository storage
aptly stores its package pool, its embedded database and the published repository trees under /var/lib/aptly, which is on the root volume by default. Mirroring a full upstream distribution component runs to tens of gigabytes. Check the free space before you start, and grow the volume to suit.
df -h /var/lib/aptlyFilesystem Size Used Avail Use% Mounted on /dev/root 29G 3.1G 25G 12% /To scale beyond the root volume, attach an EBS volume, format it, and mount it at /var/lib/aptly. Use an /etc/fstab entry that references the filesystem UUID so the mount survives a reboot. Stop aptly.service before you move the directory, and start it again afterwards.
Security recommendations
Restrict the security group. Allow TCP 443 and 80 only from the client subnets and build agents that consume the repositories, and TCP 22 only from your administration network.
Protect the management API password. It lives in /root/aptly-credentials.txt (0600 root:root). The management API is reachable only through nginx and only with this password. Rotate it by rewriting /etc/nginx/aptly.htpasswd with htpasswd and reloading nginx.
Use a real certificate for production. The per-instance certificate is self-signed. Put a CA-signed certificate and a DNS name in front of the repository so clients trust the transport without extra configuration.
Guard the signing key. The per-instance GPG signing key lives in the aptly user's keyring under /var/lib/aptly/.gnupg. Back it up securely, because anyone who holds it can sign packages that your clients will trust.
Keep the OS patched. Unattended security upgrades remain enabled on the running instance.
Support and Licensing
aptly is open-source software distributed under the MIT License. This SecureAnyCloud image bundles the unmodified official aptly release binary, verified by SHA256 checksum at build time. SecureAnyCloud provides the packaging, the systemd hardening, the nginx TLS front, the loopback-only API with per-instance basic authentication, the per-instance signing key and credential automation, this paired deployment guide, and 24/7 support.
SecureAnyCloud is not affiliated with or endorsed by the aptly project. aptly is a mark of its respective owner and is used here only to identify the software.
Deploy on AWS
Find aptly on AWS Marketplace, published by SecureAnyCloud. Subscribe and launch as shown above.
Need Help?
Email sales@secureanycloud.com for deployment help, configuration questions, or licensing enquiries.
Deploy on AWS
Launch aptly with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com