Simple modernized NuGet server implementation.
A simple NuGet server implementation built on Node.js that provides essential NuGet v3 API endpoints.
Compatible with dotnet restore
and standard NuGet clients for package publishing, querying, and manually downloading.
A modern browser-based UI is also provided:
- You can refer to registered packages. You can check various package attributes.
- You can download packages by version.
- You can also publish (upload) packages.
- You can manage user accounts.
Browse package list:
Publishing packages:
User account managements:
- Easy setup, run NuGet server in 10 seconds!
- NuGet V3 API compatibility: Support for modern NuGet client operations
- No need database management: Store package file and nuspecs into filesystem directly, feel free any database managements
- Package publish: Flexible client to upload
.nupkg
files viaHTTP POST
using cURL and others - Basic authentication: Setup authentication for publish and general access when you want it
- Reverse proxy support: Configurable trusted reverse proxy handling for proper URL resolution
- Modern Web UI with enhanced features:
- Multiple package upload: Drag & drop multiple .nupkg files at once
- User account management: Add/delete users, reset passwords (admin only)
- API password regeneration: Self-service API password updates
- Password change: Users can change their own passwords
- Package importer: Included package importer from existing NuGet server
- Docker image available
Node.js 20.18.0 or later
Uses stacks: Node.js, Typescript, Vite, Vitest, prettier-max, screw-up, Fastify, Passport, zxcvbn, React, React MUI, react-infinite-scroll, notistack, typed-message, dayjs, commander, adm-zip, xml2js
By implementing everything in a TypeScript environment, we keep our development environment and development cycle simple.
npm install -g nuget-server
For using Docker images, refer to a separate chapter.
# Start server on default port 5963
nuget-server
# Custom port
nuget-server --port 3000
# Multiple options
nuget-server --port 3000 --users-file config/users.json --max-upload-size-mb 500
The NuGet V3 API is served on the /v3
path.
- Default nuget-server served URL (Show UI):
http://localhost:5963
- Actual NuGet V3 API endpoint:
http://localhost:5963/v3/index.json
The default URL provided by nuget-server can be changed using the --base-url
option.
This is particularly necessary when public endpoint service using a reverse proxy. For details, refer to below chapter.
nuget-server only supports the NuGet V3 API. Therefore, NuGet clients must always access it using the V3 API.
If you do not explicitly specify to use the V3 API, some implementations may fall back to the V3 API while others may not, potentially causing unstable behavior. Therefore, you must always specify it. Example below.
Add as package source:
For HTTP endpoints:
dotnet nuget add source http://localhost:5963/v3/index.json \
-n "local" --protocol-version 3 --allow-insecure-connections
For HTTPS endpoints:
dotnet nuget add source https://packages.example.com/v3/index.json \
-n "packages" --protocol-version 3
Or specify in nuget.config
:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="local" value="http://localhost:5963/v3/index.json"
protocolVersion="3" allowInsecureConnections="true" />
</packageSources>
</configuration>
Upload packages by HTTP POST
method, using cURL or any HTTP client with /api/publish
endpoint:
# Upload "MyPackage.1.0.0.nupkg" file
curl -X POST http://localhost:5963/api/publish \
--data-binary @MyPackage.1.0.0.nupkg \
-H "Content-Type: application/octet-stream"
You may be dissatisfied with publishing using this method. The dotnet command includes dotnet nuget push
, which is the standard approach.
However, in my experience, this protocol uses multipart/form-data
for transmission, which has caused issues with gateway services, reverse proxies, load balancers, and similar components.
Therefore, the current nuget-server does not implement this method and instead uses the simplest binary transmission procedure.
Another advantage is that when authentication is enabled, you don't need to manage Basic authentication and V3 API keys separately. You might still feel issue with managing read operations and publish operation with the same key, but in that case, you can simply separate the users.
For authentication feature, please refer to below chapter.
By default, packages are stored in the ./packages
directory relative to where you run nuget-server.
You can customize this location using the --package-dir
option:
# Use default ./packages directory
nuget-server
# Use custom directory (relative or absolute path)
nuget-server --package-dir /another/package/location
Packages are stored in the filesystem using the following structure:
packages/
├── PackageName/
│ ├── 1.0.0/
│ │ ├── PackageName.1.0.0.nupkg
│ │ ├── PackageName.nuspec
│ │ └── icon.png # Package icon (if present)
│ └── 2.0.0/
│ ├── PackageName.2.0.0.nupkg
│ ├── PackageName.nuspec
│ └── icon.jpg # Package icon (if present)
└── AnotherPackage/
└── 1.5.0/
├── AnotherPackage.1.5.0.nupkg
├── AnotherPackage.nuspec
└── icon.png # Package icon (if present)
You can backup the package directory using simply tar
or other achiver:
cd /your/server/base/dir
tar -cf - ./packages | lz4 > backup-packages.tar.lz4
Restore is simply extract it and re-run nuget-server with the same package directory configuration, because nuget-server does not use any specialized storage such as databases.
nuget-server supports configuration through command-line options, environment variables, and JSON file.
Settings are applied in the following order (highest to lowest priority):
- Command-line options
- Environment variables
config.json
- Default values
You can specify a custom configuration file:
# Using command line option
nuget-server --config-file /path/to/config.json
# or short alias
nuget-server -c /path/to/config.json
# Using environment variable
export NUGET_SERVER_CONFIG_FILE=/path/to/config.json
nuget-server
If not specified, nuget-server looks for ./config.json
in the current directory.
Create a config.json
file:
{
"port": 5963,
"baseUrl": "http://localhost:5963",
"packageDir": "./packages",
"usersFile": "./users.json",
"realm": "Awsome nuget-server",
"logLevel": "info",
"trustedProxies": ["127.0.0.1", "::1"],
"authMode": "none",
"sessionSecret": "<your-secret-here>",
"passwordMinScore": 2,
"passwordStrengthCheck": true,
"maxUploadSizeMb": 100
}
All fields are optional. Only include the settings you want to override.
Both packageDir
and usersFile
paths can be absolute or relative. If relative, they are resolved from the directory containing the config.json
file.
nuget-server also supports authentication.
Authentication Mode | Details | Auth Initialization |
---|---|---|
none |
Default. No authentication required | Not required |
publish |
Authentication required only for package publishing | Required |
full |
Authentication required for all operations (must login first) | Required |
To enable authentication on the NuGet server, first register an initial user using the --auth-init
option.
Create an initial admin user interactively:
nuget-server --auth-init
This command will:
- Prompt for admin username (default:
admin
) - Prompt for password (with strength checking, masked input)
- Create
users.json
- Exit after initialization (server does not start)
When enabling authentication using a Docker image, use this option to generate the initial user.
Initializing authentication...
Enter admin username [admin]:
Enter password: ********
Confirm password: ********
============================================================
Admin user created successfully!
============================================================
Username: admin
Password: *********************
============================================================
Users added with --auth-init
automatically become administrator users.
Administrator users can add or remove other users via the UI. They can also reset user passwords.
While administrator users can also be assigned API passwords (described later), we recommend separating users for management whenever possible.
The NuGet server distinguishes between the password used to log in to the UI and the password used by NuGet clients when accessing the server. The password used by NuGet clients when accessing the server is called the "API password," and access is granted using the combination of the user and the API password.
Please log in by displaying the UI in the browser. Select the “API password” menu from the UI menu to generate an API password. Using this API password will enable access from the NuGet client.
Here is an example of using the API password:
# Add source with API password
dotnet nuget add source http://localhost:5963/v3/index.json \
-n "local" \
-u admin \
-p xxxxxxxxxxxxxxxxxxxxxx \
--protocol-version 3 --store-password-in-clear-text --allow-insecure-connections
Or specify nuget.config
with credentials:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="local" value="http://localhost:5963/v3/index.json"
protocolVersion="3" allowInsecureConnections="true" />
</packageSources>
<packageSourceCredentials>
<local>
<add key="Username" value="reader" />
<add key="ClearTextPassword" value="xxxxxxxxxxxxxxxxxxxxxx" />
</local>
</packageSourceCredentials>
</configuration>
For package publishing:
# Publish packages with API password
curl -X POST http://localhost:5963/api/publish \
-u admin:xxxxxxxxxxxxxxxxxxxxxx \
--data-binary @MyPackage.1.0.0.nupkg \
-H "Content-Type: application/octet-stream"
When publishing a package, you can send the package by setting Basic authentication in the Authorization
header.
nuget-server uses the zxcvbn
library to enforce strong password requirements:
- Evaluates password strength on a scale of 0-4 (Weak to Very Strong)
- Default minimum score: 2 (Good)
- Checks against common passwords, dictionary words, and patterns
- Provides real-time feedback during password creation
Configure password requirements in config.json
:
{
"passwordMinScore": 2, // 0-4, default: 2 (Good)
"passwordStrengthCheck": true // default: true
}
The NuGet server stores both "password" and "API password" as SALT hashed information, so no plaintext passwords are ever saved.
However, if you do not use HTTPS (TLS), be aware that the Authorization
header will contain the plaintext password, making it vulnerable to sniffing.
When makes public endpoint, protect communications using HTTPS.
Import all packages from another NuGet server to your local nuget-server instance. This feature can be used when migrating the foreign NuGet server to nuget-server.
Import packages interactively in CLI:
nuget-server --import-packages --package-dir ./packages
This command will:
- Prompt for source NuGet server URL
- Ask if authentication is required
- If needed, prompt for username and password (masked input)
- Discover all packages from the source server
- Download and import all packages to local storage
- Display progress for each package (1% intervals)
- Exit after import (server does not start)
- Existing packages with the same version will be overwritten
- Failed imports are logged with error details
- Progress is reported at 1% intervals to reduce log noise
- Package icons are preserved during import
Parallel downloads are not done. This is to avoid making a large number of requests to the repository.
This feature is a type of downloader.
Therefore, it does not need to be run on the actual host where it will operate.
You can perform the import process in advance on a separate host and then move the packages
directory as-is.
Starting package import...
Enter source NuGet server URL [http://host.example.com/repository/nuget/]: https://nexus.example.com/repository/nuget/
Does the server require authentication? [y/N]: y
Enter username: reader
Enter password: **********
============================================================
Import Configuration:
Source: https://nexus.example.com/repository/nuget/
Target: ./packages
Authentication: reader (password hidden)
============================================================
Start importing packages? (existing packages will be overwritten) [y/N]: y
Discovering packages from source server...
Found 125 packages with 563 versions total.
Starting package import...
Progress: 100/563 packages (17%) - [email protected]
Progress: 563/563 packages (100%) - [email protected]
============================================================
Import Complete!
============================================================
Total packages: 125
Total versions: 563
Successfully imported: 563
Failed: 0
Time elapsed: 125.3 seconds
============================================================
The server supports running behind a reverse proxy.
For example, when you have a public URL like https://nuget.example.com
and run nuget-server on a host within your internal network via a gateway.
In such cases, you MUST specify the base URL of the public URL to ensure the NuGet V3 API can provide the correct sub-endpoint address.
The server resolves URLs using the following priority order:
- Fixed base URL (highest priority): When
--base-url
option is specified, it always takes precedence - Trusted proxy headers: When trusted proxies are configured with
--trusted-proxies
:- HTTP
Forwarded
header (proto, host, port) - Traditional
X-Forwarded-*
headers (X-Forwarded-Proto
,X-Forwarded-Host
,X-Forwarded-Port
)
- HTTP
- Standard request information (fallback): Uses
Host
header when proxy headers are not available
For example --base-url
option:
- nuget-server served public base URL:
https://packages.example.com
- Actual NuGet V3 API endpoint:
https://packages.example.com/v3/index.json
# Configure served base URL (do not include /v3 path)
nuget-server --base-url https://packages.example.com
# Add as NuGet source (HTTPS - no --allow-insecure-connections needed)
dotnet nuget add source https://packages.example.com/v3/index.json \
-n "packages" --protocol-version 3
Another option, you can configure with trusted proxy addresses:
# Configure trusted proxies for proper host header handling
nuget-server --trusted-proxies "10.0.0.1,192.168.1.100"
Environment variables are also supported:
export NUGET_SERVER_BASE_URL=https://packages.example.com
export NUGET_SERVER_TRUSTED_PROXIES=10.0.0.1,192.168.1.100
export NUGET_SERVER_CONFIG_FILE=/path/to/config.json
export NUGET_SERVER_USERS_FILE=/path/to/users.json
export NUGET_SERVER_SESSION_SECRET=your-secret-key-here
export NUGET_SERVER_MAX_UPLOAD_SIZE_MB=500
Docker images are available for multiple architectures:
linux/amd64
(x86_64)linux/arm64
(aarch64)
When pulling the image, Docker automatically selects the appropriate architecture for your platform.
Suppose you have configured the following directory structure for persistence (recommended):
docker-instance/
├── data/
│ ├── config.json
│ └── user.json
└── packages/
└── (package files)
Execute as follows:
# Pull and run the latest version
docker run -d -p 5963:5963 \
-v $(pwd)/data:/data \
-v $(pwd)/packages:/packages \
kekyo/nuget-server:latest
# Or with Docker Compose
cat > docker-compose.yml << EOF
version: '3'
services:
nuget-server:
image: kekyo/nuget-server:latest
ports:
- "5963:5963"
volumes:
- ./data:/data
- ./packages:/packages
environment:
- NUGET_SERVER_AUTH_MODE=publish
EOF
docker-compose up -d
Your NuGet server is now available at:
- Web UI:
http://localhost:5963
- NuGet V3 API:
http://localhost:5963/v3/index.json
The Docker container runs as the nugetserver
user (UID 1001) for security reasons. You need to ensure that the mounted directories have the appropriate permissions for this user to write files.
Set proper permissions for mounted directories:
# Create directories if they don't exist
mkdir -p ./data ./packages
# Set ownership to UID 1001 (matches the container's nugetserver user)
sudo chown -R 1001:1001 ./data ./packages
Important: Without proper permissions, you may encounter 500 Permission Denied
errors when:
- Creating or updating user accounts
- Publishing packages
- Writing configuration files
# Run with default settings (port 5963, packages and data stored in mounted volumes)
docker run -p 5963:5963 \
-v $(pwd)/data:/data \
-v $(pwd)/packages:/packages \
kekyo/nuget-server:latest
# With authentication (users.json will be created in /data)
docker run -p 5963:5963 \
-v $(pwd)/data:/data \
-v $(pwd)/packages:/packages \
-e NUGET_SERVER_AUTH_MODE=publish \
kekyo/nuget-server:latest
You can also change settings using environment variables or command-line options, but the easiest way to configure settings is to use config.json
.
Since the Docker image has mount points configured, you can mount /data
and /packages
as shown in the example above and place /data/config.json
there to flexibly configure settings. Below is an example of config.json
:
{
"port": 5963,
"baseUrl": "http://localhost:5963",
"realm": "Awsome nuget-server",
"logLevel": "info",
"authMode": "publish"
}
When initializing credentials or importing packages, configure config.json
and perform the operation via the CLI before launching the Docker image:
# Initialize authentication
nuget-server -c ./data/config.json --auth-init
/data
: Default data directory forconfig.json
,users.json
and other persistent data/packages
: Default package storage directory (mounted to persist packages)
Default behavior: The Docker image runs with --users-file /data/users.json --package-dir /packages
by default.
Configuration priority (highest to lowest):
- Custom command line arguments (when overriding CMD)
- Environment variables (e.g.,
NUGET_SERVER_PACKAGE_DIR
) config.json
file (if explicitly specified)- Default command line arguments in Dockerfile
Various methods exist for automatically starting containers with systemd. Below is a simple example of configuring a systemd service using Podman. This is a simple service unit file used before quadlets were introduced to Podman. By placing this file and having systemd recognize it, you can automatically start the nuget-server:
/etc/systemd/system/container-nuget-server.service
:
# container-nuget-server.service
[Unit]
Description=Podman container-nuget-server.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
RestartSec=30
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
--cidfile=%t/%n.ctr-id \
--cgroups=no-conmon \
--rm \
--sdnotify=conmon \
--replace \
-d \
-p 5963:5963 \
--name nuget_server \
-v /export/data:/data -v /export/packages:/packages docker.io/kekyo/nuget-server:latest
ExecStop=/usr/bin/podman stop \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
The build of the nuget-server Docker image uses Podman.
Use the provided multi-platform build script that uses Podman to build for all supported architectures:
# Build for all platforms (local only, no push)
./build-docker-multiplatform.sh
# Build and push to Docker Hub
./build-docker-multiplatform.sh --push
# Build for specific platforms only
./build-docker-multiplatform.sh --platforms linux/amd64,linux/arm64
# Push with custom Docker Hub username
OCI_SERVER_USER=yourusername ./build-docker-multiplatform.sh --push
# Inspect existing manifest
./build-docker-multiplatform.sh --inspect
Important: For cross-platform builds, QEMU emulation must be configured first:
# Option 1: Use QEMU container (recommended)
sudo podman run --rm --privileged docker.io/multiarch/qemu-user-static --reset -p yes
# Option 2: Install system packages
# Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y qemu-user-static
# Fedora/RHEL:
sudo dnf install -y qemu-user-static
# Verify QEMU is working:
podman run --rm --platform linux/arm64 alpine:latest uname -m
# Should output: aarch64
Without QEMU, you can only build for your native architecture.
- Ubuntu 24.04 x86-64
- Ubuntu 24.04 x86-64 (podman hosted container)
- Ubuntu 22.04 x86-64
- Ubuntu 24.04 arm64
- Ubuntu 24.04 arm64 (podman hosted container)
- Cloudflare tunnel serviced global IPv4/IPv6 endpoint
- Direct global IPv4 endpoint
The server implements a subset of the NuGet V3 API protocol:
- Service index:
/v3/index.json
- Package content:
/v3/package/{id}/index.json
- Package downloads:
/v3/package/{id}/{version}/{filename}
- Registration index:
/v3/registrations/{id}/index.json
The --auth-init
and --import-packages
options require interactive responses from the operator.
Therefore, attempting to automate these may not work properly.
In such cases, you can provide credentials via environment variables:
export NUGET_SERVER_ADMIN_USERNAME=admin
export NUGET_SERVER_ADMIN_PASSWORD=MySecurePassword123!
nuget-server --auth-init --config-file ./config.json
This allows initialization in CI/CD pipelines without user interaction.
For special configurations (or to support persistent sessions), you can set a fixed session secret. Specify a sufficiently long value for the secret:
export NUGET_SERVER_SESSION_SECRET=$(openssl rand -base64 32)
nuget-server
(Or use config.json
.)
If not set, a random secret is generated (warning will be logged).
By default, nuget-server responds to NuGet V3 API requests for non-existent package IDs by returning an empty package list (endpoint: /v3/package/{id}/index.json
).
This violates the NuGet V3 API specification: PackageBaseAddress/3.0.0 response
This behavior exists to avoid a weird implementation where NuGet clients, when configured with multiple package sources, return an error immediately if any single source returns an error like a 404, without waiting for results from other sources.
For details, see the following discussion:
To prevent users from being troubled by this behavior (I too was greatly troubled), I deliberately modified the response to violate the V3 API specification. If a fully V3-compliant response (i.e., returning a 404 error) is required, you can achieve this by setting the missingPackageResponse
configuration to not-found
.
{
“missingPackageResponse”: “not-found” // Default is “empty-array”
}
Alternatively, you can use the CLI option --missing-package-response <mode>
or the environment variable NUGET_SERVER_MISSING_PACKAGE_RESPONSE
.
All configuration options can be set via CLI arguments, environment variables, or config.json. The priority order is: CLI > Environment Variable > config.json > Default.
CLI Option | Environment Variable | config.json Key | Description | Valid Values | Default |
---|---|---|---|---|---|
-p, --port <port> |
NUGET_SERVER_PORT |
port |
Server port number | 1-65535 | 5963 |
-b, --base-url <url> |
NUGET_SERVER_BASE_URL |
baseUrl |
Fixed base URL for API endpoints (overrides auto-detection) | Valid URL | Auto-detect |
-d, --package-dir <dir> |
NUGET_SERVER_PACKAGE_DIR |
packageDir |
Package storage directory | Valid path | ./packages |
-c, --config-file <path> |
NUGET_SERVER_CONFIG_FILE |
N/A | Path to config.json file | Valid path | ./config.json |
-u, --users-file <path> |
NUGET_SERVER_USERS_FILE |
usersFile |
Path to users.json file | Valid path | None |
-r, --realm <realm> |
NUGET_SERVER_REALM |
realm |
Authentication realm | String | nuget-server [version] |
-l, --log-level <level> |
NUGET_SERVER_LOG_LEVEL |
logLevel |
Logging verbosity level | debug , info , warn , error , ignore |
info |
--trusted-proxies <ips> |
NUGET_SERVER_TRUSTED_PROXIES |
trustedProxies |
Comma-separated list of trusted proxy IPs | IP addresses | None |
--auth-mode <mode> |
NUGET_SERVER_AUTH_MODE |
authMode |
Authentication mode | none , publish , full |
none |
N/A | NUGET_SERVER_SESSION_SECRET |
sessionSecret |
Secret key for session management (required for auth) | String | None |
N/A | NUGET_SERVER_PASSWORD_MIN_SCORE |
passwordMinScore |
Minimum password strength score | 0-4 | 2 |
N/A | NUGET_SERVER_PASSWORD_STRENGTH_CHECK |
passwordStrengthCheck |
Enable password strength checking | true , false |
true |
N/A | NUGET_SERVER_DUPLICATE_PACKAGE_POLICY |
duplicatePackagePolicy |
Policy for handling duplicate package uploads | overwrite , ignore , error |
ignore |
--max-upload-size-mb <size> |
NUGET_SERVER_MAX_UPLOAD_SIZE_MB |
maxUploadSizeMb |
Maximum package upload size in MB | 1-10000 | 100 |
--missing-package-response <mode> |
NUGET_SERVER_MISSING_PACKAGE_RESPONSE |
missingPackageResponse |
Response mode for missing packages | empty-array , not-found |
empty-array |
N/A | NUGET_SERVER_AUTH_FAILURE_DELAY_ENABLED |
N/A | Enable progressive delays for failed auth attempts | true , false |
true |
N/A | NUGET_SERVER_AUTH_FAILURE_MAX_DELAY |
N/A | Maximum delay for failed auth attempts (ms) | Number | 10000 |
--auth-init |
N/A | N/A | Initialize authentication with interactive admin user creation | Flag | N/A |
--import-packages |
N/A | N/A | Import packages from another NuGet server interactively | Flag | N/A |
NuGet is the .NET package system, and NuGet servers host it. There are likely differing opinions about NOT building this with .NET.
I chose this configuration as the minimal means to achieve the goal of requiring a private NuGet server. In fact, I feel it ensured the shortest development time relative to scale while providing sufficient functionality, so I'm very satisfied.
There are other reasons too, but first, just give it a try. I think it'll be a great fit for anyone who thinks they need a private NuGet server.
For discussions, please refer to the GitHub Discussions page. We have currently stopped issue-based discussions.
Pull requests are welcome! Please submit them as diffs against the develop
branch and squashed changes before send.
Under MIT.