Back to articles
Technology Insight

Building a Self-Hosted Private NuGet and PyPI Server on an ARM VPS with BaGet and Pypiserver

June 4, 2026

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:

  1. 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.
  2. A registered domain name or subdomain (e.g., registry.yourcompany.com) with A/AAAA records pointed to your VPS IP address.
  3. Docker and Docker Compose installed on the server.
  4. 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 -y

Install 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 docker

2. 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-registry

3. 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.db

4. 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_user

5. 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: bridge

6. 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 -d

Developer 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 CompanyInternal

Publishing 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_HERE

Configuring 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_PASSWORD

Upload 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/) simplejson

Conclusion 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.