Cyber Deals - Get up to 65% off on CKA, CKAD, CKS, KCNA, KCSA exams and courses!

How to Set Up an Offline PyPI Server for Air-Gapped and Disconnected Environments

How to Set Up an Offline PyPI Server for Air-Gapped and Disconnected Environments

In enterprise or government environments, servers are often isolated from the internet for security or compliance reasons. When working in such air-gapped or disconnected environments, installing Python packages using pip the conventional way simply won’t work — there’s no route to pypi.org.

This guide walks you through setting up a self-hosted, offline PyPI server using Apache HTTPD and the PEP 503 simple repository structure. You’ll learn how to download Python packages on a connected machine, organize them correctly, serve them via HTTP, and configure pip on client machines to use your local mirror.

Overview

The process involves three stages:

  1. Download — Pull Python packages (and their dependencies) on an internet-connected machine
  2. Organize & Serve — Set up an HTTP server with the correct PEP 503 directory layout
  3. Configure — Point pip on client machines to your local server

Prerequisites

  • A connected machine (Linux) with Python 3 and pip installed — used for downloading packages
  • A server in your disconnected environment with RHEL/CentOS or similar, running Apache HTTPD
  • Basic familiarity with Linux CLI and dnf/yum

Stage 1 — Download Python Packages on a Connected Machine

On the internet-connected machine, create a staging directory and use pip download to fetch the target package along with all its transitive dependencies. In this example, we’ll use the boto3 package (AWS SDK for Python) as our target:

mkdir ~/pkg-staging
pip3 download -d ~/pkg-staging/ boto3

The output will look something like this:

Collecting boto3
  Downloading boto3-1.34.69-py3-none-any.whl (139 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 139.4/139.4 kB 1.2 MB/s eta 0:00:00
  Saved ./pkg-staging/boto3-1.34.69-py3-none-any.whl
Collecting botocore<1.35.0,>=1.34.69 (from boto3)
  Saved ./pkg-staging/botocore-1.34.69-py3-none-any.whl
Collecting jmespath<2.0.0,>=0.7.1 (from boto3)
  Saved ./pkg-staging/jmespath-1.0.1-py3-none-any.whl
Collecting s3transfer<0.11.0,>=0.10.0 (from boto3)
  Saved ./pkg-staging/s3transfer-0.10.1-py3-none-any.whl
...
Successfully downloaded boto3 botocore jmespath s3transfer python-dateutil urllib3 six

Info

You can download multiple packages in a single command:

pip3 download -d ~/pkg-staging/ boto3 requests paramiko

Once done, transfer the entire pkg-staging/ directory to your disconnected HTTP server — via USB drive, secure file transfer, or your internal artifact pipeline.

Stage 2 — Set Up Apache HTTPD as a PyPI Mirror

2.1 Install and Enable Apache HTTPD

On the server inside your disconnected environment:

dnf install httpd -y
firewall-cmd --add-service=http
firewall-cmd --add-service=http --permanent

Create the document root directory for your local PyPI mirror:

mkdir -p /var/www/html/pypi

2.2 Enable Directory Indexes for PEP 503 Compatibility

pip follows the PEP 503 Simple Repository API, which means it expects to browse directory listings to discover available package versions. You need to enable Options Indexes for your PyPI directory:

cat > /etc/httpd/conf.d/local-pypi.conf <<EOF
<Directory /var/www/html/pypi>
  Options Indexes FollowSymLinks
  AllowOverride None
  Require all granted
</Directory>
EOF

Restart Apache to apply the configuration:

systemctl enable --now httpd
systemctl restart httpd

2.3 Organize Packages into PEP 503 Directory Structure

PEP 503 requires each package to live in its own subdirectory named after the normalized package name (lowercase, hyphens instead of underscores). The following one-liner processes your downloaded files and places them in the correct locations:

for file in ~/pkg-staging/*; do
  basename_file=$(basename "$file")
  pkg_raw=$(echo "$basename_file" | sed 's/\(.*\)-[0-9].*/\1/')
  pkg_name=$(echo "$pkg_raw" | tr '_' '-' | tr '[:upper:]' '[:lower:]')
  mkdir -p /var/www/html/pypi/"$pkg_name"
  cp "$file" /var/www/html/pypi/"$pkg_name"/
done

What this script does:

  • Strips the version suffix from each filename to get the package name
  • Normalizes the name (lowercase, underscores → hyphens) per PEP 503
  • Creates a per-package subdirectory under /var/www/html/pypi/
  • Copies the wheel or tarball into the appropriate directory

After running it, your directory layout should look like this:

/var/www/html/pypi/
├── boto3/
│   └── boto3-1.34.69-py3-none-any.whl
├── botocore/
│   └── botocore-1.34.69-py3-none-any.whl
├── jmespath/
│   └── jmespath-1.0.1-py3-none-any.whl
├── python-dateutil/
│   └── python_dateutil-2.9.0.post0-py2.py3-none-any.whl
├── s3transfer/
│   └── s3transfer-0.10.1-py3-none-any.whl
├── six/
│   └── six-1.16.0-py2.py3-none-any.whl
└── urllib3/
    └── urllib3-2.2.1-py3-none-any.whl

You can verify the server is working by opening a browser (or using curl) and navigating to http://<server-ip>/pypi/ — you should see a directory listing of all available packages.

Stage 3 — Configure pip on Client Machines

On any client machine inside your disconnected environment, configure pip to use your local server instead of the public PyPI. You can do this globally by creating or editing the pip.conf file:

  • System-wide (all users): /etc/pip.conf
  • Per-user: ~/.config/pip/pip.conf
cat > /etc/pip.conf <<EOF
[global]
index = http://<server-ip>/pypi
index-url = http://<server-ip>/pypi
trusted-host = <server-ip>
EOF

Replace <server-ip> with the actual IP address or hostname of your HTTPD server.

Note on trusted-host: Since we’re using plain HTTP (not HTTPS), pip will warn about an untrusted host. The trusted-host directive suppresses this warning. For production environments, consider setting up TLS with a self-signed or internal CA certificate and using https:// instead.

Verify the Configuration

Test that pip can find and install packages from your local mirror:

pip3 install boto3

You should see pip resolving packages from http://<server-ip>/pypi rather than pypi.org.

Keeping Your Mirror Updated

When you need to add new packages or newer versions to your mirror:

  1. Download the new packages on your connected machine using pip3 download -d ~/pkg-staging/ <new-package>
  2. Transfer the new files to your disconnected server
  3. Re-run the directory organization script to place them in the correct PEP 503 subdirectories
  4. No restart of Apache is needed — the new files are immediately available

Summary

Step Action Tool
1 Download packages + dependencies pip3 download
2 Transfer to disconnected server USB / internal transfer
3 Install and configure Apache HTTPD dnf, httpd.conf
4 Organize into PEP 503 layout Bash one-liner
5 Configure clients pip.conf

Setting up a local PyPI mirror is straightforward and dramatically simplifies Python dependency management in air-gapped environments. Once in place, your teams can install packages with a standard pip install command — no internet access, no workarounds, no friction.

Gineesh Madapparambath

Gineesh Madapparambath

Gineesh Madapparambath is the founder of techbeatly. He is the co-author of The Kubernetes Bible, Second Edition and the author of Ansible for Real Life Automation. He has worked as a Systems Engineer, Automation Specialist, and content author. His primary focus is on Ansible Automation, Containerisation (OpenShift & Kubernetes), and Infrastructure as Code (Terraform). (Read more: iamgini.com)


Note

Disclaimer: The views expressed and the content shared in all published articles on this website are solely those of the respective authors, and they do not necessarily reflect the views of the author’s employer or the techbeatly platform. We strive to ensure the accuracy and validity of the content published on our website. However, we cannot guarantee the absolute correctness or completeness of the information provided. It is the responsibility of the readers and users of this website to verify the accuracy and appropriateness of any information or opinions expressed within the articles. If you come across any content that you believe to be incorrect or invalid, please contact us immediately so that we can address the issue promptly.

Share :