# How to Host Your Own Python Package Index


A self-hosted Python package index can save a team from PyPI outages, vendor lock-in, or restricted networks, but it also gives that team another service to run. Three tools cover almost every use case: [devpi](https://github.com/devpi/devpi) for production multi-team setups, [pypiserver](https://github.com/pypiserver/pypiserver) for a small team that needs somewhere to put `internal-greeter-0.1.0.whl`, and [bandersnatch](https://github.com/pypa/bandersnatch) for full PyPI mirrors on networks that cannot reach `pypi.org`.

Hosting an index is the server-side counterpart to [How to use private package indexes with uv](https://pydevtools.com/handbook/how-to/how-to-use-private-package-indexes-with-uv.md), which covers the client configuration for managed services like AWS CodeArtifact and JFrog Artifactory.

## Decide whether to self-host

Hosting an index has an operational tax: a server to keep alive, TLS to renew, storage to monitor, auth to wire to company SSO, backups to test. SaaS registries handle that. Pick self-hosted only when at least one of these applies.

* Airgapped or restricted network. No outbound HTTPS to `pypi.org` from the build environment. A bandersnatch mirror or devpi mirror index is the only way to install anything.
* Cost or vendor avoidance. Artifactory, CodeArtifact, and Cloudsmith bill per seat or per GB. A devpi container on existing infrastructure is a fixed cost.
* PyPI hardening. Surviving a PyPI outage, controlling exactly which package versions reach builds, or scanning every dependency before it lands in CI.
* Existing ops capacity. A team that already runs production infrastructure can host devpi cheaply. A team without on-call rotation should buy.

If none of those apply, use [a managed registry](https://pydevtools.com/handbook/how-to/how-to-use-private-package-indexes-with-uv.md) and skip the rest of this guide.

## Choose the right self-hosted package index

Each tool solves a different problem. The names are similar enough that they get confused in older blog posts.

| Tool | What it does | Pick it when |
|------|--------------|--------------|
| devpi | Per-user and per-team indexes that inherit from PyPI, plus a write API for uploads | Multiple teams need separate indexes, or staging-then-promote workflows, or PyPI caching plus internal hosting in one process |
| pypiserver | Serves wheels and sdists from a directory; accepts uploads | One team, internal packages only, "make this go away" energy |
| bandersnatch | Full PyPI clone synced over the [PEP 691](https://peps.python.org/pep-0691/) JSON simple-index API | Airgapped network, or an organization that needs the entire index available offline |
| proxpi | Read-through caching proxy | CI cache for a public PyPI mirror with no internal packages |

devpi covers most of pypiserver's use cases plus a lot more. bandersnatch and proxpi serve different audiences: bandersnatch ships every package on PyPI (~14TB and growing), proxpi caches only what someone has installed.

> [!NOTE]
> [packaging.python.org's hosting guide](https://packaging.python.org/en/latest/guides/hosting-your-own-index/) lists more options including simpleindex, dumb-pypi, and Pulp-python. Those exist but have small userbases. devpi, pypiserver, and bandersnatch cover the typical cases.

The rest of this guide walks through devpi end to end, then covers pypiserver and bandersnatch as alternatives.

## Run a devpi server

[devpi-server](https://pypi.org/project/devpi-server/) 6.19 needs Python 3.9 or newer. Install the three packages into a dedicated [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) using [uv](https://pydevtools.com/handbook/reference/uv.md):

```bash
uv venv --python 3.12 ~/devpi-env
uv pip install --python ~/devpi-env/bin/python \
  devpi-server devpi-client devpi-web
```
```powershell
uv venv --python 3.12 $HOME\devpi-env
uv pip install --python $HOME\devpi-env\Scripts\python.exe `
  devpi-server devpi-client devpi-web
```
`uv pip install --python <path>` writes packages into the target environment without requiring [pip](https://pydevtools.com/handbook/reference/pip.md) itself to be installed there. `uv venv` does not seed pip by default; this is the canonical way around it.

`devpi-server` is the index. `devpi-client` is the CLI for creating users and uploading packages. `devpi-web` adds a browsable web UI and search; it is optional but small.

Activate the environment so the rest of this guide can call `devpi`, `devpi-init`, and `devpi-server` without absolute paths:

```bash
source ~/devpi-env/bin/activate
```
```powershell
& $HOME\devpi-env\Scripts\Activate.ps1
```
Initialize a server data directory once:

```bash
devpi-init --serverdir ~/devpi-data
```

Start the server. By default it binds to `localhost:3141`:

```bash
devpi-server --serverdir ~/devpi-data \
  --host 127.0.0.1 --port 3141
```

The first request to the index triggers a one-time index of every PyPI project name; expect ~30 seconds of CPU work on first start.

> [!IMPORTANT]
> The `--host 127.0.0.1` flag keeps the server on localhost. To expose it on a LAN, change to `--host 0.0.0.0` only after putting it behind a reverse proxy with TLS. Devpi's HTTP server has no built-in TLS support and the upload endpoint accepts authenticated package uploads.

> [!NOTE]
> This guide runs devpi as a localhost foreground process for demonstration. A production setup adds process supervision (systemd, supervisord, or a container orchestrator), TLS termination via Nginx or Caddy, persistent and snapshotted storage for `--serverdir`, and external authentication via [devpi-ldap](https://github.com/devpi/devpi-ldap) or a reverse-proxy auth header. The [devpi quickstart](https://doc.devpi.net/latest/quickstart-server.html#permanent-server-installation) and the [oneuptime Docker walkthrough](https://oneuptime.com/blog/post/2026-02-08-how-to-run-devpi-in-docker-private-pypi-server/view) cover the production pieces.

## Create a user and an index on devpi

Open a second terminal (the server keeps running in the first) and activate the same environment. Then connect the client to the running server and create a user account:

```bash
devpi use http://127.0.0.1:3141
devpi user -c alice password=secret
devpi login alice --password=secret
```

`devpi user -c` creates the account; `devpi login` exchanges the password for a token cached at `~/.devpi/client/`. The token expires after 10 hours by default.

Create an index that inherits from `root/pypi`, the built-in PyPI mirror:

```bash
devpi index -c dev bases=root/pypi
devpi use alice/dev
```

`bases=root/pypi` is the part that turns a vanilla index into a PyPI-passthrough cache. A package not found on `alice/dev` falls through to `root/pypi`, which fetches it from `pypi.org` and caches it locally. Subsequent installs hit the cache.

## Upload an internal package to devpi

Build a wheel with [`uv build`](https://pydevtools.com/handbook/reference/uv.md):

```bash
cd internal-greeter
uv build
ls dist/
# internal_greeter-0.1.0-py3-none-any.whl
# internal_greeter-0.1.0.tar.gz
```

Upload to the active index:

```bash
devpi upload --from-dir dist
```

Devpi prints `file_upload of internal_greeter-0.1.0-...whl to http://127.0.0.1:3141/alice/dev/`. The package is now available on the simple-index endpoint:

```bash
curl http://127.0.0.1:3141/alice/dev/+simple/internal-greeter/
```

The response is a [PEP 503](https://pydevtools.com/handbook/explanation/what-is-pep-503.md) HTML page listing every uploaded version. Installers use the `+simple/` URL, not the human-facing project page.

## Install from devpi with uv

Point uv at the devpi simple index. The simplest form sets the index for a single command:

```bash
uv pip install \
  --index-url http://127.0.0.1:3141/alice/dev/+simple/ \
  internal-greeter
```

For a project that uses devpi as its primary registry, declare it in `pyproject.toml`:

```toml
[[tool.uv.index]]
name = "devpi"
url = "http://127.0.0.1:3141/alice/dev/+simple/"
default = true
```

Setting `default = true` replaces PyPI entirely. Because the `alice/dev` index inherits from `root/pypi`, public packages still resolve through devpi's cache. Public packages requested for the first time take a roundtrip to PyPI. Repeat installs are cached.

On a local M-series Mac, `uv pip install requests` through devpi took about 2.5s on the first hit and 0.14s from cache. On a fresh Debian x86_64 Modal container, the same test took about 2.0s cold and 0.45s warm. Treat those as ballpark numbers, not a guarantee.

For deeper coverage of `[[tool.uv.index]]`, authentication, and `tool.uv.sources` pinning, see [How to use private package indexes with uv](https://pydevtools.com/handbook/how-to/how-to-use-private-package-indexes-with-uv.md).

## Install from devpi with pip

Pip uses the same `simple/` URL via the `--index-url` flag or a `pip.conf` entry:

```bash
pip install \
  --index-url http://127.0.0.1:3141/alice/dev/+simple/ \
  internal-greeter
```

To make the setting persistent across all pip invocations, write it to the user-level config:

```ini
# ~/.config/pip/pip.conf
[global]
index-url = http://127.0.0.1:3141/alice/dev/+simple/
```
```ini
# %APPDATA%\pip\pip.ini
[global]
index-url = http://127.0.0.1:3141/alice/dev/+simple/
```
The devpi client can write this for you. From an environment with `devpi-client` installed and an active index:

```bash
devpi use --set-cfg alice/dev
```

`devpi use --set-cfg` edits `pip.conf` and `uv.toml` to point at the active devpi index. This helps on a developer machine but is risky in CI, where explicit `--index-url` flags or `UV_INDEX` environment variables make the configuration auditable.

## Run pypiserver as a lightweight alternative

If devpi feels heavy and the only requirement is "a place to host internal wheels," [pypiserver](https://pypi.org/project/pypiserver/) 2.4 needs a directory and a process. Install into a fresh environment, activate it, then run:

```bash
uv venv --python 3.12 ~/pypiserver-env
uv pip install --python ~/pypiserver-env/bin/python pypiserver
source ~/pypiserver-env/bin/activate   # PowerShell: & $HOME\pypiserver-env\Scripts\Activate.ps1
mkdir ~/packages
pypi-server run --port 8080 ~/packages
```

Drop wheels into `~/packages` and they appear at `http://127.0.0.1:8080/simple/`. The default mode allows anonymous downloads and disables uploads. To enable authenticated uploads:

```bash
htpasswd -sc ~/.htpasswd alice           # creates an htpasswd file
pypi-server run --port 8080 \
  -P ~/.htpasswd -a update,download \
  ~/packages
```

`-P` points to the credentials file; `-a update,download` requires authentication for both actions. Upload with [twine](https://pydevtools.com/handbook/reference/twine.md):

```bash
twine upload \
  --repository-url http://127.0.0.1:8080/ \
  --username alice --password secret \
  dist/*.whl
```

uv installs from a pypiserver index the same way it installs from devpi, by pointing `--index-url` at `/simple/`.

> [!TIP]
> Pypiserver does not cache PyPI. If a package is not in `~/packages`, the install fails. To layer pypiserver on top of PyPI, use `--extra-index-url https://pypi.org/simple/` on the client side or run pypiserver in `--fallback-url` mode.

## Mirror PyPI for offline use with bandersnatch

[bandersnatch](https://pypi.org/project/bandersnatch/) is the [PyPA](https://www.pypa.io)-maintained tool for syncing every package on PyPI to local disk. Used by airgapped enterprises and clusters with strict egress rules.

A full mirror is large: as of April 2026, PyPI hosts roughly 800,000 projects and 14TB of distribution files. Plan storage accordingly, or use bandersnatch's filtering to mirror only a subset.

Install and write a config:

```bash
uv venv --python 3.12 ~/bandersnatch-env
uv pip install --python ~/bandersnatch-env/bin/python bandersnatch
source ~/bandersnatch-env/bin/activate   # PowerShell: & $HOME\bandersnatch-env\Scripts\Activate.ps1
bandersnatch mirror --config /etc/bandersnatch.conf
```

A minimal `bandersnatch.conf`:

```ini
[mirror]
directory = /srv/pypi-mirror
master = https://pypi.org
workers = 3
hash-index = false
stop-on-error = false

[plugins]
enabled =
    allowlist_project

[allowlist]
packages =
    requests
    numpy
    pandas
```

The `allowlist_project` plugin restricts the mirror to listed packages and their dependencies. Without a plugin block, bandersnatch syncs everything on PyPI.

Run the sync regularly via cron or a systemd timer. After the first run, expose the mirror with any static-file web server:

```bash
python -m http.server 9000 --directory /srv/pypi-mirror/web
```

That is fine for a smoke test; in production, use Nginx or Caddy with TLS.

For airgapped deployment, sync to local disk on a connected machine, copy the mirror over a one-way diode or sneakernet, and serve the copy on the airgapped network. Bandersnatch's [diff file output](https://bandersnatch.readthedocs.io/en/latest/mirror_configuration.html) supports incremental transfers.

Point uv at the mirror just like any other simple index:

```toml
[[tool.uv.index]]
name = "internal-mirror"
url = "https://mirror.internal.example.com/simple/"
default = true
```

## Learn more

* [devpi documentation](https://doc.devpi.net/) covers replication, staging indexes, and tox integration
* [pypiserver README](https://github.com/pypiserver/pypiserver) covers htpasswd setup and reverse-proxy configuration
* [bandersnatch documentation](https://bandersnatch.readthedocs.io/) covers [PEP 381](https://peps.python.org/pep-0381/) (the original PyPI mirror protocol), diff-file workflows, and filter plugins
* [packaging.python.org: Hosting your own simple repository](https://packaging.python.org/en/latest/guides/hosting-your-own-index/) lists every known hosting tool
* [How to use private package indexes with uv](https://pydevtools.com/handbook/how-to/how-to-use-private-package-indexes-with-uv.md) covers client-side configuration for any PEP 503 index, including managed registries
* [What is PyPI?](https://pydevtools.com/handbook/explanation/what-is-pypi.md) explains the protocol that every self-hosted index implements
* [What is PEP 503?](https://pydevtools.com/handbook/explanation/what-is-pep-503.md) describes the simple repository API that uv and pip speak
