Private Python package repositories let you share proprietary code securely, cache public packages, and maintain control over your dependency supply chain. This guide covers self-hosted and cloud options for enterprise Python packaging.
When You Need Private Repositories
┌─────────────────────────────────────────────────────────────┐
│ Private Repository Use Cases │
├─────────────────────────────────────────────────────────────┤
│ │
│ PROPRIETARY CODE │
│ ├── Internal libraries shared across projects │
│ ├── Company-specific tools and utilities │
│ └── Code you can't publish to public PyPI │
│ │
│ CONTROL & COMPLIANCE │
│ ├── Freeze specific versions for reproducibility │
│ ├── Audit trail for package usage │
│ └── Air-gapped or regulated environments │
│ │
│ RELIABILITY & PERFORMANCE │
│ ├── Cache PyPI packages (survive outages) │
│ ├── Faster installs from local/regional servers │
│ └── Reduce external network dependencies │
│ │
│ SECURITY │
│ ├── Scan packages before allowing use │
│ ├── Prevent dependency confusion attacks │
│ └── Control what packages developers can install │
│ │
└─────────────────────────────────────────────────────────────┘
Solution Comparison
| Feature | devpi | pypiserver | CodeArtifact | Artifact Registry | Artifactory |
|---|---|---|---|---|---|
| Self-hosted | Yes | Yes | No | No | Yes/Cloud |
| PyPI caching | Yes | No | Yes | Yes | Yes |
| Multiple indexes | Yes | No | Yes | Yes | Yes |
| User management | Yes | Basic | IAM | IAM | Yes |
| Cost | Free | Free | Pay-per-use | Pay-per-use | Licensed |
| Setup complexity | Medium | Low | Low | Low | High |
Self-Hosted: devpi
devpi is the most capable self-hosted solution, offering caching, replication, and multiple indexes.
Installation
# Install devpi server and client
pip install devpi-server devpi-client devpi-web
# Initialize server (creates ~/.devpi/server directory)
devpi-init
# Start server
devpi-server --host 0.0.0.0 --port 3141
Basic Configuration
# Connect client to server
devpi use http://localhost:3141
# Login as root (default password is empty)
devpi login root --password=""
# Change root password
devpi user -m root password=secretpassword
# Create a user
devpi user -c mycompany password=userpass
# Create an index that inherits from PyPI
devpi login mycompany --password=userpass
devpi index -c dev bases=root/pypi
Index Hierarchy
┌─────────────────────────────────────────────────────────────┐
│ devpi Index Structure │
├─────────────────────────────────────────────────────────────┤
│ │
│ root/pypi (built-in) │
│ └── Mirrors/caches packages from pypi.org │
│ │
│ mycompany/dev │
│ ├── bases=root/pypi (inherits PyPI packages) │
│ └── Upload private packages here │
│ │
│ mycompany/staging │
│ ├── bases=mycompany/dev │
│ └── Promoted packages for testing │
│ │
│ mycompany/prod │
│ ├── bases=mycompany/staging │
│ └── Approved packages for production │
│ │
│ Package lookup: prod → staging → dev → pypi.org │
│ │
└─────────────────────────────────────────────────────────────┘
Uploading Packages
# Select your index
devpi use mycompany/dev
# Upload a package
devpi upload dist/*
# Or use twine
twine upload --repository-url http://localhost:3141/mycompany/dev \
-u mycompany -p userpass dist/*
Configuring pip to Use devpi
# ~/.pip/pip.conf (Linux/macOS) or %APPDATA%\pip\pip.ini (Windows)
[global]
index-url = http://localhost:3141/mycompany/dev/+simple/
trusted-host = localhost
Production Deployment
# Run with nginx reverse proxy
# /etc/nginx/sites-available/devpi
server {
listen 443 ssl;
server_name devpi.company.com;
ssl_certificate /etc/ssl/certs/devpi.crt;
ssl_certificate_key /etc/ssl/private/devpi.key;
location / {
proxy_pass http://localhost:3141;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Run as systemd service
# /etc/systemd/system/devpi.service
[Unit]
Description=devpi PyPI server
After=network.target
[Service]
User=devpi
ExecStart=/usr/local/bin/devpi-server --host 127.0.0.1 --port 3141
Restart=always
[Install]
WantedBy=multi-user.target
Self-Hosted: pypiserver (Simple)
For basic needs, pypiserver is simpler than devpi.
Quick Setup
# Install
pip install pypiserver
# Create packages directory
mkdir -p /var/pypi/packages
# Run server
pypi-server run -p 8080 /var/pypi/packages
# With authentication
pip install passlib
htpasswd -sc /var/pypi/.htpasswd admin
pypi-server run -p 8080 -P /var/pypi/.htpasswd /var/pypi/packages
Upload Packages
# Configure .pypirc
# ~/.pypirc
[distutils]
index-servers = local
[local]
repository = http://localhost:8080
username = admin
password = yourpassword
# Upload
twine upload --repository local dist/*
AWS CodeArtifact
CodeArtifact is AWS's managed artifact repository.
Setup
# Create domain and repository
aws codeartifact create-domain --domain mycompany
aws codeartifact create-repository \
--domain mycompany \
--repository python-packages
# Connect to PyPI upstream
aws codeartifact associate-external-connection \
--domain mycompany \
--repository python-packages \
--external-connection public:pypi
Authentication
# Get authorization token (expires in 12 hours)
aws codeartifact login --tool pip \
--domain mycompany \
--repository python-packages
# Or get token manually
export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
--domain mycompany \
--query authorizationToken \
--output text)
Configure pip
# Automatic configuration
aws codeartifact login --tool pip --domain mycompany --repository python-packages
# This creates/updates pip.conf with:
# [global]
# index-url = https://aws:[email protected]/pypi/python-packages/simple/
Configure Poetry
# Get repository URL
REPO_URL=$(aws codeartifact get-repository-endpoint \
--domain mycompany \
--repository python-packages \
--format pypi \
--query repositoryEndpoint \
--output text)
# Configure Poetry
poetry config repositories.codeartifact $REPO_URL
poetry config http-basic.codeartifact aws $CODEARTIFACT_AUTH_TOKEN
CI/CD Integration (GitHub Actions)
# .github/workflows/publish.yml
name: Publish to CodeArtifact
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # For OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1
- name: Login to CodeArtifact
run: |
aws codeartifact login --tool pip \
--domain mycompany \
--repository python-packages
aws codeartifact login --tool twine \
--domain mycompany \
--repository python-packages
- name: Build and publish
run: |
pip install build twine
python -m build
twine upload --repository codeartifact dist/*
Google Cloud Artifact Registry
GCP's Artifact Registry supports Python packages.
Setup
# Enable API
gcloud services enable artifactregistry.googleapis.com
# Create repository
gcloud artifacts repositories create python-packages \
--repository-format=python \
--location=us-central1 \
--description="Private Python packages"
Authentication
# Configure pip authentication
gcloud artifacts print-settings python \
--project=myproject \
--repository=python-packages \
--location=us-central1
# Output shows pip.conf settings
Configure pip
# pip.conf
[global]
extra-index-url = https://us-central1-python.pkg.dev/myproject/python-packages/simple/
# Use keyring for authentication
pip install keyring keyrings.google-artifactregistry-auth
# Keyring handles authentication automatically
pip install my-private-package
Publishing
# Configure twine
# ~/.pypirc
[distutils]
index-servers = artifact-registry
[artifact-registry]
repository = https://us-central1-python.pkg.dev/myproject/python-packages/
# Upload (keyring handles auth)
twine upload --repository artifact-registry dist/*
JFrog Artifactory
Artifactory is an enterprise solution with extensive features.
Creating a Python Repository
# Via REST API
curl -X PUT "https://artifactory.company.com/artifactory/api/repositories/python-local" \
-H "Content-Type: application/json" \
-u admin:password \
-d '{
"key": "python-local",
"rclass": "local",
"packageType": "pypi"
}'
# Create remote repository (PyPI proxy)
curl -X PUT "https://artifactory.company.com/artifactory/api/repositories/python-remote" \
-H "Content-Type: application/json" \
-u admin:password \
-d '{
"key": "python-remote",
"rclass": "remote",
"packageType": "pypi",
"url": "https://pypi.org"
}'
# Create virtual repository (combines local and remote)
curl -X PUT "https://artifactory.company.com/artifactory/api/repositories/python-virtual" \
-H "Content-Type: application/json" \
-u admin:password \
-d '{
"key": "python-virtual",
"rclass": "virtual",
"packageType": "pypi",
"repositories": ["python-local", "python-remote"]
}'
Configure pip
# pip.conf
[global]
index-url = https://artifactory.company.com/artifactory/api/pypi/python-virtual/simple
Authentication Options
# Basic auth in URL
pip install --index-url https://user:[email protected]/artifactory/api/pypi/python-virtual/simple mypackage
# API key
pip install --index-url https://user:[email protected]/artifactory/api/pypi/python-virtual/simple mypackage
# Access token (recommended)
pip install --index-url https://artifactory.company.com/artifactory/api/pypi/python-virtual/simple \
--extra-index-url https://pypi.org/simple \
--trusted-host artifactory.company.com \
mypackage
Publishing to Artifactory
# ~/.pypirc
[distutils]
index-servers = artifactory
[artifactory]
repository = https://artifactory.company.com/artifactory/api/pypi/python-local
username = deployer
password = API_KEY
twine upload --repository artifactory dist/*
GitHub Packages
GitHub Packages supports Python via container registry.
Publishing
# Build and tag for GitHub Packages
# Note: GitHub Packages Python support uses container registry format
# Alternative: Use GitHub Actions with trusted publishing
# .github/workflows/publish.yml
name: Publish Python Package
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build package
run: |
pip install build
python -m build
- name: Publish to GitHub Packages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install twine
twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
Configuring Tools
pip Configuration
# ~/.pip/pip.conf (global)
# or project/.pip.conf (per-project)
[global]
index-url = https://private.company.com/simple/
extra-index-url = https://pypi.org/simple/
trusted-host = private.company.com
[install]
# Require hashes for security
require-hashes = true
Poetry Configuration
# pyproject.toml
[[tool.poetry.source]]
name = "private"
url = "https://private.company.com/simple/"
priority = "primary"
[[tool.poetry.source]]
name = "PyPI"
priority = "supplemental"
# Configure authentication
poetry config http-basic.private username password
# Or use environment variables
export POETRY_HTTP_BASIC_PRIVATE_USERNAME=user
export POETRY_HTTP_BASIC_PRIVATE_PASSWORD=pass
pip-tools Configuration
# Use custom index in requirements.in
--index-url https://private.company.com/simple/
--extra-index-url https://pypi.org/simple/
django>=4.0
my-private-package>=1.0
# Compile with custom index
pip-compile --index-url https://private.company.com/simple/ requirements.in
Security Best Practices
1. Always Use HTTPS
# Never use HTTP for package repositories
# BAD
index-url = http://internal-pypi.company.com/simple/
# GOOD
index-url = https://internal-pypi.company.com/simple/
2. Credential Management
# Use environment variables (CI/CD)
export PIP_INDEX_URL=https://${PYPI_USER}:${PYPI_PASS}@private.company.com/simple/
# Use .netrc for local development
# ~/.netrc
machine private.company.com
login username
password token
# Never commit credentials
# .gitignore
.netrc
.pypirc
pip.conf
3. Namespace Private Packages
# Prevent dependency confusion attacks
# Use company prefix for all private packages
# setup.py / pyproject.toml
name = "acme-internal-utils" # Not just "utils"
name = "acme-auth-client" # Not just "auth-client"
4. Package Signing
# Sign packages with GPG
gpg --detach-sign -a dist/mypackage-1.0.0.tar.gz
# Upload signature with package
twine upload dist/mypackage-1.0.0.tar.gz dist/mypackage-1.0.0.tar.gz.asc
CI/CD Integration Patterns
GitLab CI
# .gitlab-ci.yml
variables:
PIP_INDEX_URL: https://${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}@registry.gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple
publish:
stage: deploy
script:
- pip install build twine
- python -m build
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token
twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
only:
- tags
Jenkins
// Jenkinsfile
pipeline {
agent any
environment {
PYPI_CREDS = credentials('private-pypi')
}
stages {
stage('Build') {
steps {
sh '''
pip install build
python -m build
'''
}
}
stage('Publish') {
when {
tag pattern: "v\\d+\\.\\d+\\.\\d+", comparator: "REGEXP"
}
steps {
sh '''
pip install twine
twine upload --repository-url https://pypi.company.com \
-u $PYPI_CREDS_USR -p $PYPI_CREDS_PSW dist/*
'''
}
}
}
}
Azure DevOps
# azure-pipelines.yml
trigger:
tags:
include:
- v*
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
- script: |
pip install build twine
python -m build
displayName: 'Build package'
- task: TwineAuthenticate@1
inputs:
artifactFeed: 'MyFeed'
- script: |
twine upload -r MyFeed --config-file $(PYPIRC_PATH) dist/*
displayName: 'Publish to Azure Artifacts'
Troubleshooting
Package Not Found
# Check if package exists in private repo
pip index versions my-package --index-url https://private.company.com/simple/
# Try with verbose output
pip install my-package -vvv --index-url https://private.company.com/simple/
Authentication Failures
# Test credentials
curl -u user:pass https://private.company.com/simple/
# Check pip is using correct config
pip config list
# Clear pip cache
pip cache purge
SSL/Certificate Issues
# For self-signed certificates
pip install --trusted-host private.company.com --cert /path/to/ca.crt my-package
# Or configure globally
# pip.conf
[global]
cert = /path/to/ca-bundle.crt
trusted-host = private.company.com
Next Steps
- Learn about package publishing in our PyPI Publishing Guide
- Explore dependency management in our Dependency Management Tools Guide
- See the complete ecosystem in our Python Packaging Complete Guide
For more Python development guides, explore our complete Python packaging series.