Migrate from Sonatype Nexus¶
This guide walks you through moving an existing APT repository from Sonatype Nexus Repository Manager to Repod. The process exports every .deb asset from Nexus via its REST API, imports them into Repod with a bulk upload script, updates client machines, and then cuts over the URL — all without breaking a single apt install during the transition.
1. When to migrate¶
This guide is the right approach when:
- Nexus is used exclusively (or primarily) for APT hosting, and the rest of its features — Maven, npm, Docker — are unused or managed elsewhere.
- Your team wants built-in security scanning (CVE reports, SBOM manifests) without maintaining a separate pipeline.
- You want a lighter operational footprint: Repod runs as a single Docker Compose stack with no Java heap to tune.
If you are still using Nexus for other artifact types, you can run both systems in parallel and migrate APT repositories one at a time.
2. Before you start — inventory checklist¶
Gather this information from Nexus before touching any configuration:
- Number of APT repositories (each Nexus "hosted" APT repo becomes one Repod distribution).
- Distributions and components in each repository (e.g.
jammy main,focal restricted). - Approximate package count and total disk size (
du -shon the Nexus blob store, or the Nexus UI's repository health check). - Number of client machines consuming the repository, and how their
sources.listentries are managed (Ansible, Chef, cloud-init, manual). - Whether Nexus is currently enforcing authentication on the read path — if so, clients already have a token or username/password in their apt configuration.
- Your Nexus admin credentials and the base URL (e.g.
https://nexus.example.com).
Tip
Export the Nexus repository list from Administration → Repositories as a CSV before you start. It becomes your tracking sheet for the migration.
3. Step 1 — Export .deb files from Nexus¶
Nexus exposes a paginated components API. The script below iterates through every page and downloads each .deb asset to a local staging directory.
#!/usr/bin/env bash
# nexus-export.sh — download all .deb assets from a Nexus APT repository
set -euo pipefail
NEXUS_URL="https://nexus.example.com"
REPO_NAME="apt-repo" # the Nexus repository name
NEXUS_USER="admin"
NEXUS_PASS="changeme"
OUT_DIR="./nexus-export"
mkdir -p "$OUT_DIR"
continuation_token=""
while true; do
url="${NEXUS_URL}/service/rest/v1/components?repository=${REPO_NAME}"
if [[ -n "$continuation_token" ]]; then
url="${url}&continuationToken=${continuation_token}"
fi
response=$(curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" "$url")
continuation_token=$(echo "$response" | jq -r '.continuationToken // empty')
# Extract asset download URLs for .deb files
mapfile -t asset_urls < <(echo "$response" | \
jq -r '.items[].assets[] | select(.contentType == "application/vnd.debian.binary-package") | .downloadUrl')
for asset_url in "${asset_urls[@]}"; do
filename=$(basename "$asset_url")
echo "Downloading: $filename"
curl -s -L -u "${NEXUS_USER}:${NEXUS_PASS}" \
-o "${OUT_DIR}/${filename}" \
"$asset_url"
done
[[ -z "$continuation_token" ]] && break
done
echo "Export complete. Files saved to ${OUT_DIR}/"
Note
This script requires jq (apt install jq). If your Nexus instance uses HTTPS with a self-signed certificate, add -k to the curl flags or install the CA bundle.
Run it and verify the file count matches what you recorded in the inventory:
4. Step 2 — Set up Repod¶
If you do not already have Repod running, follow the Getting Started guide first. Come back here once:
- The Docker Compose stack is up (
frontend :3003,backend :8000,nginx :80). - A GPG signing key has been generated in Settings → GPG.
- You have at least one API token (prefix
repod_) from Settings → API Tokens.
5. Step 3 — Bulk import script¶
With the .deb files staged locally, upload them to Repod in a loop. The backend enforces a rate limit of 20 uploads per minute, so the script sleeps briefly between calls to stay within that limit.
#!/usr/bin/env bash
# repod-import.sh — upload all .deb files in a directory to Repod
set -euo pipefail
REPOD_URL="http://repod.example.com" # or http://localhost:8000 during testing
API_TOKEN="repod_xxxxxxxxxxxxxxxx"
DEB_DIR="./nexus-export"
DISTRIBUTION="jammy" # target distribution in Repod
COMPONENT="main" # target component
UPLOAD_DELAY=3 # seconds between uploads; 20/min limit = 3s minimum
success=0
failed=0
for deb in "${DEB_DIR}"/*.deb; do
filename=$(basename "$deb")
echo -n "Uploading ${filename} ... "
http_code=$(curl -s -o /tmp/repod_response.json -w "%{http_code}" \
-X POST "${REPOD_URL}/upload/" \
-H "Authorization: Bearer ${API_TOKEN}" \
-F "file=@${deb}" \
-F "distribution=${DISTRIBUTION}" \
-F "component=${COMPONENT}")
if [[ "$http_code" == "200" || "$http_code" == "201" ]]; then
echo "OK"
((success++))
else
echo "FAILED (HTTP ${http_code})"
cat /tmp/repod_response.json
((failed++))
fi
sleep "$UPLOAD_DELAY"
done
echo ""
echo "Done. Success: ${success} Failed: ${failed}"
Warning
If you are uploading thousands of packages, run this script inside a tmux or screen session so a disconnected SSH session does not interrupt it. Large .deb files (> 500 MB) may hit the default Nginx client body size limit — increase client_max_body_size in the Nginx configuration before starting.
Run the import:
6. Step 4 — Verify¶
Compare package counts between Nexus and Repod before touching any client machines.
Nexus package count (from your export):
Repod package count (via API):
curl -s -H "Authorization: Bearer ${API_TOKEN}" \
"${REPOD_URL}/packages/?distribution=jammy" | jq '.total'
Then test a full apt install cycle on a canary machine — an isolated VM or container — by temporarily pointing it at the new Repod URL:
# On the canary machine
echo "deb [signed-by=/etc/apt/trusted.gpg.d/repod.gpg] \
http://repod.example.com jammy main" \
| sudo tee /etc/apt/sources.list.d/repod-test.list
curl -fsSL http://repod.example.com/gpg.key \
| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/repod.gpg
sudo apt update
sudo apt install your-internal-package
Tip
Test the packages that are most critical to your infrastructure first. A broken apt update on the canary machine is far better than a broken rollout fleet-wide.
7. Step 5 — Update client machines¶
Once canary testing passes, roll out the new APT source to all clients. The exact method depends on your configuration management tooling.
# Remove the old Nexus source
sudo rm /etc/apt/sources.list.d/nexus.list
# Add the Repod source
echo "deb [signed-by=/etc/apt/trusted.gpg.d/repod.gpg] \
http://repod.example.com jammy main" \
| sudo tee /etc/apt/sources.list.d/repod.list
# Import the Repod GPG key
curl -fsSL http://repod.example.com/gpg.key \
| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/repod.gpg
sudo apt update
- name: Remove Nexus APT source
ansible.builtin.file:
path: /etc/apt/sources.list.d/nexus.list
state: absent
- name: Download Repod GPG key
ansible.builtin.get_url:
url: http://repod.example.com/gpg.key
dest: /tmp/repod.gpg.asc
- name: Dearmor and install GPG key
ansible.builtin.command:
cmd: gpg --dearmor -o /etc/apt/trusted.gpg.d/repod.gpg /tmp/repod.gpg.asc
creates: /etc/apt/trusted.gpg.d/repod.gpg
- name: Add Repod APT source
ansible.builtin.apt_repository:
repo: "deb [signed-by=/etc/apt/trusted.gpg.d/repod.gpg] http://repod.example.com jammy main"
state: present
filename: repod
8. Step 6 — Cut over¶
With all clients reconfigured, the final step is to make the new URL canonical:
- DNS change: Update the CNAME or A record for your internal APT hostname to point to the Repod host. Clients using the canonical hostname need no further changes.
- Reverse proxy change: If you front both Nexus and Repod behind Nginx or HAProxy, update the upstream block to route APT traffic to Repod.
- Direct URL change: If clients already have the new Repod URL in their
sources.list(from Step 5), no further action is needed.
Verify by running sudo apt update on several machines from different network segments.
9. Rollback plan¶
Keep Nexus running and reachable at its original internal URL for at least two weeks after cut-over. If a critical issue surfaces:
- Revert the DNS record or reverse proxy upstream to point back to Nexus.
- No client changes are needed — they will automatically pick up packages from Nexus again on the next
apt update. - Investigate and resolve the issue in Repod before attempting cut-over again.
Warning
Do not decommission Nexus until you have run apt install successfully from Repod across all production environments and the two-week rollback window has passed.
10. Common issues¶
Authentication errors from the Nexus API
If you see 401 Unauthorized when running nexus-export.sh, confirm the credentials are correct and that the Nexus user has the nx-repository-view-*-*-read privilege. Anonymous read access must be enabled on the repository if you omit credentials.
Pagination stops early
Nexus's continuationToken is only returned when more pages exist. If the script exits before downloading all packages, check that the jq expression matches your Nexus version's response schema — the field name has not changed since Nexus 3.20, but the surrounding structure may differ in older releases.
Large .deb files timing out
The Repod Nginx frontend has a default client_max_body_size of 100 MB. Edit docker-compose.yaml (or the Nginx config volume) to increase this before uploading large packages. The upload endpoint also has a 20-requests-per-minute rate limit; the import script's UPLOAD_DELAY variable handles this, but you may need to increase the delay if you share the API token with other processes.
Package already exists
If a package with the same name, version, and architecture already exists in Repod (e.g. from a previous partial import), the upload returns 409 Conflict. This is safe to ignore — the package is already present. Filter these out of your failed count by checking the response body.