Skip to content

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:

curl -fsSL http://YOUR_HOST:80/repos/dists/jammy/InRelease \
  | gpg --dearmor \
  | sudo tee /etc/apt/trusted.gpg.d/repod.gpg > /dev/null
  1. Open Settings → GPG in the Repod web interface
  2. Click Copy public key
  3. On the client machine:
    echo "PASTE_KEY_HERE" | gpg --dearmor \
      | sudo tee /etc/apt/trusted.gpg.d/repod.gpg > /dev/null
    

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

sudo apt update
sudo apt install mypackage

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

/etc/apt/sources.list.d/repod.list
# 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

configure-repod.yml
---
- 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

sudo rpm --import http://YOUR_HOST:80/repos/gpg.key

Verify the key was imported:

rpm -q gpg-pubkey --qf '%{name}-%{version}-%{release} --> %{summary}\n'

Step 2 — Create the repo file

/etc/yum.repos.d/repod.repo
[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)

configure-repod-rpm.yml
---
- 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

sudo rpm --import http://YOUR_HOST:80/repos/gpg.key

Step 3 — Install packages

sudo zypper refresh repod
sudo zypper install mypackage

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_...).

~/.m2/settings.xml
<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:

~/.m2/settings.xml
<mirrors>
  <mirror>
    <id>maven-default-http-blocker</id>
    <mirrorOf>external:http:*,!repod-releases</mirrorOf>
    <name>Allow repod-releases over plain HTTP (dev only)</name>
    <url>http://0.0.0.0/</url>
    <blocked>true</blocked>
  </mirror>
</mirrors>

Step 2 — Publish (mvn deploy)

pom.xml
<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:

mvn deploy

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)

pom.xml or settings.xml <profile>
<repositories>
  <repository>
    <id>repod-releases</id>
    <url>http://YOUR_HOST:8000/api/v1/maven/releases</url>
  </repository>
</repositories>
mvn dependency:get -Dartifact=com.acme:widget:1.0.0

Gradle

build.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:

~/.pip/pip.conf (Linux) or %APPDATA%\pip\pip.ini (Windows)
[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:

pip install --extra-index-url http://YOUR_HOST:8000/api/v1/pypi/private/simple/ mypkg

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:

~/.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
twine upload --repository repod-private dist/*

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

~/.npmrc or ./.npmrc (project-local)
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:

.npmrc
@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 latest dist-tag is supported — it resolves to whichever version was published most recently (the real npm default behavior for npm publish without --tag), not necessarily the highest semver.
  • dependencies/scripts/engines and other package.json fields aren't preserved in the packument — sufficient for npm install <pkg>/npm publish of a standalone package, not for resolving a package that itself depends on other npm packages.
  • npm unpublish and npm deprecate aren'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.

/etc/apt/auth.conf.d/repod.conf
machine YOUR_HOST:80
login token
password YOUR_API_TOKEN

Add credentials to the .repo file:

/etc/yum.repos.d/repod.repo
[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
username=token
password=YOUR_API_TOKEN


CI/CD configuration

.github/workflows/install.yml
- 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
.gitlab-ci.yml
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

Full troubleshooting guide →