Building a Self-Hosted Private NuGet and PyPI Server on an ARM VPS with BaGet and Pypiserver
Introduction: The Case for Self-Hosted Internal Package Registries
In modern software development, enterprise teams constantly build and share modular code components across projects. While public registries like NuGet.org and PyPI are excellent for open-source libraries, proprietary enterprise code requires strict confidentiality, rapid access, and reliable uptime. Relying on cloud-based private registries can quickly become cost-prohibitive as teams scale, introducing vendor lock-in and potential security vulnerabilities.
Setting up an internal, self-hosted package registry on an ARM-based Virtual Private Server (VPS) offers a compelling alternative. ARM architecture provides exceptional compute efficiency and performance at a fraction of the cost of traditional x86 infrastructure. This comprehensive guide walks you through deploying BaGet (for .NET/NuGet) and Pypiserver (for Python/PyPI) on a single ARM VPS, establishing a secure, lightweight, and highly cost-effective artifact repository for your engineering team.
Why Choose ARM, BaGet, and Pypiserver?
Before diving into the implementation details, it is crucial to understand why this specific stack represents an optimal choice for cost-conscious, high-performance engineering teams:
- ARM Architecture (Ampere Altra / AWS Graviton): Provides superior multi-core performance, lower power consumption, and up to 40% better price-to-performance metrics compared to x86 instances.
- BaGet: A lightweight, open-source, and cross-platform implementation of a NuGet server built on .NET Core. It runs flawlessly on ARM architectures and utilizes minimal memory while providing a clean web UI and comprehensive API support.
- Pypiserver: A minimalist, highly efficient Python package index server. It is incredibly lightweight, requires virtually zero configuration to start, and handles high-throughput internal package distribution with ease.
Prerequisites and Environment Setup
To follow this guide, ensure your infrastructure meets the following baseline requirements:
- An ARM64 VPS (e.g., Oracle Cloud Free Tier Ampere, AWS EC2 Graviton, or Hetzner ARM64) running Ubuntu 22.04 LTS or 24.04 LTS.
- A registered domain name or subdomain (e.g.,
registry.yourcompany.com) with A/AAAA records pointed to your VPS IP address. - Docker and Docker Compose installed on the server.
- Basic familiarity with Linux terminal administration and SSH protocols.
Security Note: Never expose your private package registries directly to the public internet without an encrypted reverse proxy and strict authentication. This guide implements Nginx and Let's Encrypt SSL to enforce data encryption in transit.
Step-by-Step Deployment Guide
1. System Preparation and Docker Installation
First, log in to your ARM VPS via SSH and update your system packages to the latest versions:
sudo apt update && sudo apt upgrade -yInstall Docker and Docker Compose if they are not already present on the system. Since we are using an ARM architecture, using the official Docker repository ensures we get native ARM64 binaries:
sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable --now docker2. Configuring the Directory Structure
We will organize our configuration and storage directories logically under a single workspace directory to simplify backups and maintenance:
mkdir -p ~/private-registry/{baget/data,pypi/packages,nginx}
cd ~/private-registry3. Setting Up BaGet for NuGet Packages
Create a configuration file for BaGet named baget.env in the ~/private-registry/baget directory. This file will securely store environment variables, including your administrative API key:
# ~/private-registry/baget/baget.env
ApiKey=YOUR_SECURE_NUGET_API_KEY_HERE
Storage__Type=FileSystem
Storage__Path=/var/baget/data
Database__Type=Sqlite
Database__ConnectionString=Data Source=/var/baget/data/baget.db4. Setting Up Pypiserver for Python Packages
Pypiserver uses htpasswd-style files for authentication. Install standard utilities to generate a secure password hash for your Python developers:
sudo apt install -y apache2-utils
htpasswd -sc ~/private-registry/pypi/.htpasswd developer_user5. Orchestrating Services with Docker Compose
Create a centralized docker-compose.yml file in the root of your workspace (~/private-registry). This file defines the BaGet, Pypiserver, and Nginx containers, ensuring they share an internal bridge network:
version: '3.8'
services:
baget:
image: loicsharma/baget:latest
container_name: nuget-server
restart: always
env_file:
- ./baget/baget.env
volumes:
- ./baget/data:/var/baget/data
networks:
- registry-net
pypiserver:
image: pypiserver/pypiserver:latest
container_name: pypi-server
restart: always
volumes:
- ./pypi/packages:/data/packages
- ./pypi/.htpasswd:/data/.htpasswd
command: -p 8080 -P /data/.htpasswd -a update,download,list /data/packages
networks:
- registry-net
nginx:
image: nginx:alpine
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- baget
- pypiserver
networks:
- registry-net
networks:
registry-net:
driver: bridge6. Nginx Reverse Proxy and SSL Configuration
To safely route traffic based on subdomains or paths, configure Nginx. Create ./nginx/nginx.conf to route traffic efficiently to each container. For simplicity, we demonstrate a path-based routing configuration below:
events { worker_connections 1024; }
http {
client_max_body_size 100M;
server {
listen 80;
server_name registry.yourcompany.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name registry.yourcompany.com;
ssl_certificate /etc/letsencrypt/live/[registry.yourcompany.com/fullchain.pem](https://registry.yourcompany.com/fullchain.pem);
ssl_certificate_key /etc/letsencrypt/live/[registry.yourcompany.com/privkey.pem](https://registry.yourcompany.com/privkey.pem);
# BaGet NuGet Routing
location /nuget/ {
proxy_pass http://baget:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Pypiserver PyPI Routing
location /pypi/ {
proxy_pass http://pypiserver:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}Note: Obtain your free SSL certificate using Certbot (sudo certbot certonly --standalone -d registry.yourcompany.com) before spinning up the Nginx container to ensure the path references remain valid.Launch your entire architecture with a single command:
docker compose up -dDeveloper Integration Workflow
Configuring the Client Side for .NET / NuGet
To publish a private library, .NET developers simply register the new internal source via the CLI:
dotnet nuget add source [https://registry.yourcompany.com/nuget/v3/index.json](https://registry.yourcompany.com/nuget/v3/index.json) --name CompanyInternalPublishing a built package is straightforward using your predefined API key:
dotnet nuget push YourPackage.1.0.0.nupkg --source CompanyInternal --api-key YOUR_SECURE_NUGET_API_KEY_HEREConfiguring the Client Side for Python / PyPI
To upload Python wheels or source distributions, leverage Twine. Configure the credentials in your local ~/.pypirc file:
[distutils]
index-servers =
internal
[internal]
repository: [https://registry.yourcompany.com/pypi/](https://registry.yourcompany.com/pypi/)
username: developer_user
password: YOUR_PASSWORDUpload packages directly via the terminal:
twine upload -r internal dist/*To download packages during local development or within CI/CD pipelines, specify the extra index URL:
pip install --extra-index-url [https://registry.yourcompany.com/pypi/](https://registry.yourcompany.com/pypi/) simplejsonConclusion and Maintenance Best Practices
By hosting your own private package managers on an ARM VPS, your business achieves full data sovereignty, ultra-low latency within local dev environments, and negligible infrastructure costs. To maintain systemic health over time, implement scheduled automated database backups (baget.db and the PyPI packages volume) directly to an off-site object storage tier. Monitor storage consumption closely as your build engineering systems evolve, and enforce regular rotate schedules for API keys and basic auth tokens to ensure your internal intellectual property remains perfectly secure.
