Client Setup¶
Configure machines to install packages from your Repod repository.
Prerequisites¶
- The Repod repository server is running and accessible from the client machine
- At least one package has been successfully uploaded and published
- The repository's GPG public key is available (find it in Settings → GPG)
APT clients (Debian / Ubuntu)¶
Step 1 — Import the GPG signing key¶
The APT client verifies the repository's InRelease file signature. Import the
signing key once per machine:
Step 2 — Add the APT source¶
# Replace 'jammy' with your target distribution codename
echo "deb http://YOUR_HOST:80/repos jammy main" \
| sudo tee /etc/apt/sources.list.d/repod.list
Step 3 — Update and install¶
Verify the setup¶
# Check that the repo is recognised
apt-cache policy mypackage
# Should show "http://YOUR_HOST:80/repos jammy/main" as a candidate
Multiple distributions¶
# Ubuntu 22.04 packages
deb http://YOUR_HOST:80/repos jammy main
# Ubuntu 24.04 packages (if you maintain separate packages per release)
deb http://YOUR_HOST:80/repos noble main
Ansible role¶
---
- name: Configure Repod APT repository
hosts: all
become: true
tasks:
- name: Import Repod GPG key
ansible.builtin.get_url:
url: "http://YOUR_HOST:80/repos/gpg.key"
dest: /tmp/repod.asc
- name: Convert and install key
ansible.builtin.shell: |
gpg --dearmor < /tmp/repod.asc > /etc/apt/trusted.gpg.d/repod.gpg
- name: Add APT source
ansible.builtin.apt_repository:
repo: "deb http://YOUR_HOST:80/repos {{ ansible_distribution_release }} main"
filename: repod
state: present
update_cache: yes
RPM clients — DNF (AlmaLinux / RHEL / Rocky / Fedora)¶
Step 1 — Import the GPG signing key¶
Verify the key was imported:
Step 2 — Create the repo file¶
[repod]
name=Repod Private Repository
baseurl=http://YOUR_HOST:80/repos/almalinux9/x86_64/
enabled=1
gpgcheck=1
gpgkey=http://YOUR_HOST:80/repos/gpg.key
repo_gpgcheck=0
metadata_expire=300
repo_gpgcheck=0
Set repo_gpgcheck=0 unless you have signed repomd.xml with a key that
DNF can verify separately. The package-level gpgcheck=1 is sufficient for
most deployments.
Step 3 — Install packages¶
# Refresh metadata
sudo dnf makecache --repo=repod
# Install a package
sudo dnf install mypackage
# List available packages from Repod only
sudo dnf list available --repo=repod
Distribution-specific baseurl¶
| Distribution | baseurl |
|---|---|
| AlmaLinux 8 | http://YOUR_HOST:80/repos/almalinux8/x86_64/ |
| AlmaLinux 9 | http://YOUR_HOST:80/repos/almalinux9/x86_64/ |
| Rocky Linux 8 | http://YOUR_HOST:80/repos/rocky8/x86_64/ |
| Rocky Linux 9 | http://YOUR_HOST:80/repos/rocky9/x86_64/ |
| CentOS Stream 9 | http://YOUR_HOST:80/repos/centos-stream9/x86_64/ |
| Fedora 42 | http://YOUR_HOST:80/repos/fedora/x86_64/ |
| openSUSE Leap 15.6 | http://YOUR_HOST:80/repos/opensuse-leap-15.6/x86_64/ |
Ansible role (DNF)¶
---
- name: Configure Repod RPM repository
hosts: all
become: true
tasks:
- name: Import Repod GPG key
ansible.builtin.rpm_key:
key: "http://YOUR_HOST:80/repos/gpg.key"
state: present
- name: Add Repod repository
ansible.builtin.yum_repository:
name: repod
description: Repod Private Repository
baseurl: "http://YOUR_HOST:80/repos/{{ ansible_distribution | lower }}{{ ansible_distribution_major_version }}/x86_64/"
enabled: yes
gpgcheck: yes
gpgkey: "http://YOUR_HOST:80/repos/gpg.key"
state: present
RPM clients — Zypper (openSUSE Leap)¶
Step 1 — Add the repository¶
sudo zypper addrepo \
--name "Repod Private Repository" \
--gpgcheck \
http://YOUR_HOST:80/repos/opensuse-leap-15.6/x86_64/ \
repod
Step 2 — Import the GPG key¶
Step 3 — Install packages¶
Maven clients (mvn / Gradle)¶
Repod's Maven support (V1) hosts private release artifacts — it does not proxy
Maven Central. Every deploy (PUT) requires HTTP Basic authentication; reads
(GET) are open, the same known limitation already documented for
APT/RPM/APK package downloads. SNAPSHOT versions are not supported yet —
deploy release versions only.
Step 1 — Configure credentials in settings.xml¶
The <server><id> must match the <id> used in your POM's
<distributionManagement>/<repository> block below. The password can be
your Repod account password or, for CI/CD, an API token (repod_...).
<settings>
<servers>
<server>
<id>repod-releases</id>
<username>YOUR_USERNAME</username>
<password>YOUR_PASSWORD_OR_API_TOKEN</password>
</server>
</servers>
</settings>
Plain HTTP repositories
Maven blocks external http:// repositories by default since 3.8.1. If
your Repod instance isn't behind TLS yet (see docker-compose.tls.yml),
add a mirror override to unblock your specific repository id:
Step 2 — Publish (mvn deploy)¶
<project>
...
<distributionManagement>
<repository>
<id>repod-releases</id>
<url>http://YOUR_HOST:8000/api/v1/maven/releases</url>
</repository>
</distributionManagement>
</project>
releases is the target Maven repository name — it's created automatically
on first successful deploy (no pre-registration needed), matching the OCI
registry's dynamic-repository model. Then:
Checksums (.sha1/.md5) and maven-metadata.xml are always generated
server-side from the actually-stored artifact — anything the Maven client
uploads for those is accepted but discarded, never trusted as-is.
Step 3 — Resolve (mvn dependency:get / regular dependencies)¶
<repositories>
<repository>
<id>repod-releases</id>
<url>http://YOUR_HOST:8000/api/v1/maven/releases</url>
</repository>
</repositories>
Gradle¶
repositories {
maven {
url "http://YOUR_HOST:8000/api/v1/maven/releases"
credentials {
username = project.findProperty("repodUser") ?: ""
password = project.findProperty("repodToken") ?: ""
}
}
}
publishing {
repositories {
maven {
url "http://YOUR_HOST:8000/api/v1/maven/releases"
credentials {
username = project.findProperty("repodUser") ?: ""
password = project.findProperty("repodToken") ?: ""
}
}
}
}
Access control¶
By default any repository name is open to mvn deploy for uploader/maintainer/
admin accounts. To restrict a specific repository to certain roles or groups,
use POST /api/v1/maven/repositories/{name}/access (admin only) — the same
RBAC model as distribution_access/oci_repository_access: no rows means
open, admin always bypasses, a denial on read returns 404 rather than 403
to avoid leaking the existence of a restricted repository.
PyPI clients (pip / twine)¶
Repod's PyPI support (V1) hosts private wheels/sdists — it does not proxy
PyPI. It implements the Simple Repository API (PEP 503) in HTML for
pip install/pip download, and the "legacy" upload protocol
(POST .../legacy/) for twine upload — the same protocol real PyPI's
upload.pypi.org uses, so standard tooling works without modification.
Unlike Maven, reads (the Simple API pages and file downloads) are also
gated by RBAC when a repository index is restricted — see "Access control"
below.
Step 1 — Install (pip install)¶
pip install --index-url http://YOUR_USER:YOUR_PASSWORD_OR_TOKEN@YOUR_HOST:8000/api/v1/pypi/private/simple/ mypkg
Or persist it in pip.conf/pip.ini:
[global]
index-url = http://YOUR_USER:YOUR_PASSWORD_OR_TOKEN@YOUR_HOST:8000/api/v1/pypi/private/simple/
private is the target PyPI index name — like Maven repositories and OCI
repositories, it's created automatically on first successful upload (no
pre-registration needed). If the index has no RBAC restriction, credentials
in the URL are optional — plain pip install --index-url
http://YOUR_HOST:8000/api/v1/pypi/private/simple/ mypkg works too.
Combine with the public PyPI
Use --extra-index-url instead of --index-url to fall back to
pypi.org for packages not hosted privately:
Step 2 — Publish (twine upload)¶
pip install twine
twine upload --repository-url http://YOUR_HOST:8000/api/v1/pypi/private/legacy/ \
-u YOUR_USERNAME -p YOUR_PASSWORD_OR_API_TOKEN \
dist/*
Or via .pypirc:
[distutils]
index-servers = repod-private
[repod-private]
repository = http://YOUR_HOST:8000/api/v1/pypi/private/legacy/
username = YOUR_USERNAME
password = YOUR_PASSWORD_OR_API_TOKEN
Checksums are always computed server-side from the uploaded file — the
sha256_digest twine sends is accepted but not trusted as-is.
Access control¶
By default any index name is open to publish for uploader/maintainer/admin
accounts, and open to read for anyone. To restrict a specific index (reads
and writes) to certain roles or groups, use POST
/api/v1/pypi/repositories/{name}/access (admin only) — same RBAC model as
distribution_access/maven_repository_access: no rows means open, admin
always bypasses, a denial returns 404 rather than 403 to avoid leaking
the existence of a restricted index. A restricted index rejects
unauthenticated pip install/pip download — supply credentials in the
--index-url as shown above.
npm clients (npm / yarn)¶
Repod's npm support (V1) hosts private packages — it does not proxy
npmjs.org. It implements the real npm registry protocol (packument via
GET /{package}, publish via a single PUT /{package} with the tarball
base64-encoded in the request body — the same shape npm publish sends to
any registry), so standard tooling works without modification. As with
PyPI, reads (packument + tarball download) are RBAC-gated too when a
namespace is restricted.
Step 1 — Get a token¶
Generate a Repod API token (Settings → API Tokens, or ask an admin) —
npm login interactive flow isn't supported in V1; paste a pre-generated
token directly into .npmrc instead (the standard approach for private
registries — GitHub Packages, Artifactory, and Verdaccio-with-token-auth
all work the same way).
Step 2 — Configure .npmrc¶
registry=http://YOUR_HOST:8000/api/v1/npm/private/
//YOUR_HOST:8000/api/v1/npm/private/:_authToken=YOUR_API_TOKEN
private is the target npm namespace — like Maven/PyPI, it's created
automatically on first successful publish (no pre-registration needed). If
the namespace has no RBAC restriction, the _authToken line is only
needed for npm publish (reads work without it).
To scope a specific organization to a private namespace while keeping everything else on the public npm registry:
@myorg:registry=http://YOUR_HOST:8000/api/v1/npm/private/
//YOUR_HOST:8000/api/v1/npm/private/:_authToken=YOUR_API_TOKEN
Step 3 — Install / publish¶
npm install mypkg # resolves via the configured registry
npm publish # from inside the package directory
Access control¶
By default any namespace is open to publish for uploader/maintainer/admin
accounts, and open to read for anyone. To restrict a namespace (reads
and writes) to certain roles or groups, use POST
/api/v1/npm/repositories/{name}/access (admin only) — same RBAC model as
maven_repository_access/pypi_repository_access: no rows means open,
admin always bypasses, a denial returns 404 rather than 403.
Known V1 limitations¶
- Only the
latestdist-tag is supported — it resolves to whichever version was published most recently (the real npm default behavior fornpm publishwithout--tag), not necessarily the highest semver. dependencies/scripts/enginesand otherpackage.jsonfields aren't preserved in the packument — sufficient fornpm install <pkg>/npm publishof a standalone package, not for resolving a package that itself depends on other npm packages.npm unpublishandnpm deprecatearen't implemented.
Authenticated access (API tokens)¶
If your Repod instance requires authentication to download packages (not the default for the repository Nginx, but possible via reverse proxy rules), use HTTP Basic Auth or a token in the request header.
CI/CD configuration¶
- name: Configure Repod repository (APT)
run: |
curl -fsSL http://${{ secrets.REPOD_HOST }}/repos/dists/jammy/InRelease \
| gpg --dearmor \
| sudo tee /etc/apt/trusted.gpg.d/repod.gpg > /dev/null
echo "deb http://${{ secrets.REPOD_HOST }}/repos jammy main" \
| sudo tee /etc/apt/sources.list.d/repod.list
sudo apt update
sudo apt install mypackage
install:
before_script:
- |
curl -fsSL http://${REPOD_HOST}/repos/dists/jammy/InRelease \
| gpg --dearmor \
| tee /etc/apt/trusted.gpg.d/repod.gpg > /dev/null
echo "deb http://${REPOD_HOST}/repos jammy main" \
> /etc/apt/sources.list.d/repod.list
apt-get update -qq
script:
- apt-get install -y mypackage
Troubleshooting¶
| Problem | Likely cause | Fix |
|---|---|---|
NO_PUBKEY on apt update |
GPG key not imported or expired | Re-run the key import command |
404 Not Found on apt update |
Wrong distribution codename | Check the codename in sources.list |
GPG key retrieval failed on dnf install |
Key not imported | Run rpm --import ... |
| Package version is stale | Metadata cache too long | dnf makecache or apt update |
HTTPS required by policy |
Internal policy | Add reverse proxy with TLS; see reverse proxy guide |