Last Updated on April 16, 2026 by Thiago Crepaldi
For developers managing internal libraries or AI infrastructure, a private PyPI server is essential for hosting nightly builds without polluting public registries. devpi is the gold standard for this, specifically for its “volatile” index feature which allows for rapid iteration.
In this guide, we’ll deploy it on RHEL 8.10 using Docker Compose, rooting everything in /opt/local for clean service isolation.
Prerequisites
- RHEL 8.10 with rootful Docker installed.
- Docker Compose V2.
- Sudo access for filesystem and firewall management.
Prepare the Host Filesystem
We will store both our configuration (docker-compose.yml) and the actual package data under /opt/local/devpi.
# Create the project structure
sudo mkdir -p /opt/local/devpi/data
sudo chmod 700 /opt/local/devpi/data
cd /opt/local/devpiConfigure the RHEL Firewall
By default, RHEL will block external access. Open 3141 persistently:
sudo firewall-cmd --permanent --add-port=3141/tcp
sudo firewall-cmd --reloadCreate the Docker Compose Configuration
Create a docker-compose.yml file in /opt/local/devpi.
Critical RHEL Note: We use the :Z flag on the volume mount. This tells Docker to automatically relabel the host directory for SELinux compliance, preventing “Permission Denied” errors.
services:
devpi:
image: jonasal/devpi-server:latest
container_name: devpi-server
restart: always
ports:
- "3141:3141"
volumes:
- ./data:/data:Z
environment:
- DEVPI_PASSWORD=SetYourAdminPasswordDeploy the Service
sudo docker compose up -dConfiguring the “Nightly” Index
Standard PyPI indexes don’t allow version overwrites. For nightlies, we want a volatile index.
From your workstation (with devpi-client installed):
# 1. Connect and login
devpi use http://<your-server-ip>:3141
devpi login root --password=SetYourAdminPassword
# 2. Create a CI/CD user
devpi user -c build_bot password=bot-secret-pass
# 3. Create the volatile index
devpi index -c build_bot/nightly volatile=TrueEnabling/Disabling PyPI.org Caching
One of devpi’s best features is its ability to act as a caching proxy for the official PyPI.
To Enable Caching (Inherit from PyPI):
If you want your index to find and cache public packages when they aren’t found locally:
devpi index build_bot/nightly bases=root/pypiTo Disable Caching (Private Only):
If you want to isolate your index so it only contains your internal packages (improving security by preventing dependency confusion attacks):
devpi index build_bot/nightly bases=""Create a Dummy Package for Testing
Create a small internal utility library to verify the setup.
pyproject.toml:
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "geek-utils"
version = "0.1.0.dev1"
description = "Internal test library for GeekIsTheWay"
requires-python = ">=3.9"
dependencies = ["requests>=2.28"]src/geek_utils/core.py:
import secrets
def get_geek_token():
return f"GEEK-{secrets.token_hex(16)}"Build and Publish Dummy Package for testing
Build the Package
pip install build
python -m buildUploading via Twine
Option A: Explicit Command
twine upload --repository-url http://<your-server-ip>:3141/build_bot/nightly/ \
-u build_bot -p bot-secret-pass dist/*Option B: Using .pypirc (Recommended) Add this to your ~/.pypirc:
[nightly]
repository = http://<your-server-ip>:3141/build_bot/nightly/
username = build_bot
password = bot-secret-passThen run: twine upload -r nightly dist/*
Client-Side Configuration (pip & requirements.txt)
Set Global Configuration
pip config set global.index-url http://<your-server-ip>:3141/build_bot/nightly/+simple/
pip config set global.trusted-host <your-server-ip>Using Requirements Files
requirements.txt (Mixed Mode)
--extra-index-url http://<your-server-ip>:3141/build_bot/nightly/+simple/
--trusted-host <your-server-ip>
geek-utils==0.1.0.dev1
requests>=2.28.0Install Command Examples
Mixed Mode (devpi + PyPI.org): pip install --extra-index-url http://<your-server-ip>:3141/build_bot/nightly/+simple/ --trusted-host <your-server-ip> geek-utils
Strict Mode (Only use devpi): pip install --index-url http://<your-server-ip>:3141/build_bot/nightly/+simple/ --trusted-host <your-server-ip> geek-utils
Deleting Packages from the Registry
If a nightly build is corrupted or no longer needed, use the following commands from your workstation.
Delete a Specific Version
This is the most common task for cleaning up specific failed builds:
# Connect and login if not already
devpi use http://<your-server-ip>:3141/build_bot/nightly
devpi login build_bot --password bot-secret-pass
# Delete version 0.1.0.dev1 of geek-utils
devpi remove geek-utils==0.1.0.dev1Delete an Entire Project
To remove every version of a project from the index:
devpi remove geek-utilsBulk Cleanup (Pro-Tip)
While devpi doesn’t have a built-in “TTL” (Time To Live) for packages, you can use a simple bash loop with the client:
# Example: List versions and remove them (logic can be expanded for date filtering)
devpi list geek-utilsTroubleshooting & Maintenance
- Docker Pull Limits: If you hit rate limits, run
sudo docker loginon the RHEL host. - SELinux: Verify labels with
ls -Zd /opt/local/devpi/data. - Backup:
sudo tar -czvf devpi-backup.tar.gz /opt/local/devpi. - Preventing Dependency Confusion
- If a package exists in both your private index and
root/pypi, devpi will prefer the local version if the versions are identical. However,pipwill always try to download the highest version number it can find across all available indexes. - To fix this for your nightly builds:
- Always use a higher version/suffix: Tag your nightlies with a suffix that PyPI.org wouldn’t have (e.g.,
1.2.0.dev20260416). - Explicit Bases: If you are worried about a specific project, you can create an index with no bases specifically for that project, ensuring 0% chance of leaking to or from the public internet.
- Always use a higher version/suffix: Tag your nightlies with a suffix that PyPI.org wouldn’t have (e.g.,
- If a package exists in both your private index and

