Home/Blog/Private Python Package Repositories: PyPI Alternatives for Enterprise
Software Engineering

Private Python Package Repositories: PyPI Alternatives for Enterprise

Set up private Python package repositories using devpi, AWS CodeArtifact, GCP Artifact Registry, or JFrog Artifactory. Learn authentication, CI/CD integration, and best practices.

By InventiveHQ Team

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

FeaturedevpipypiserverCodeArtifactArtifact RegistryArtifactory
Self-hostedYesYesNoNoYes/Cloud
PyPI cachingYesNoYesYesYes
Multiple indexesYesNoYesYesYes
User managementYesBasicIAMIAMYes
CostFreeFreePay-per-usePay-per-useLicensed
Setup complexityMediumLowLowLowHigh

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

For more Python development guides, explore our complete Python packaging series.

Frequently Asked Questions

Find answers to common questions

When you have proprietary code to share across projects, need to control package versions for compliance, want to cache PyPI packages for reliability, or need to distribute internal tools. Most enterprise teams with multiple Python projects benefit from private repos.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.