A local NuGet feed is just a folder, but if you want it laid out properly (one folder per package, one subfolder per version) dotnet nuget push won’t do that for you out of the box. Seed the folder with one correctly laid-out package and every push after that follows suit.
I wanted a local feed on my Mac for testing packages before publishing them. The obvious command was:
dotnet nuget push "*.nupkg" --source ~/Development/NuGet
It worked, but every package landed flat at the top of the folder. What I wanted was the hierarchical layout NuGet itself uses, which resolves faster once the feed grows and is much tidier to browse.
Why the feed comes out flat
dotnet nuget push doesn’t pick a layout of its own. When the source is a local folder, it looks at what is already there and copies the new package to match. An empty folder is treated as a flat feed, so the first package goes in at the top level, and so does every one after it.
The hierarchical layout it can produce looks like this, with package IDs and versions lowercased:
~/Development/NuGet/
mycompany.mylib/
1.0.0/
mycompany.mylib.1.0.0.nupkg
mycompany.mylib.1.0.0.nupkg.sha512
mycompany.mylib.nuspec
If the folder already contains one package in that shape, push recognises it and lays out everything else the same way.
Seeding the feed
The fix is to lay out the first package by hand. This script does it for you. It reads the ID and version from the package’s own .nuspec, lowercases them to match NuGet convention, and writes the three files push expects. Save it as seed-feed.sh:
#!/bin/bash
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <package.nupkg> [feed-folder]" >&2
exit 1
fi
PKG="$1"
FEED="${2:-$HOME/Development/NuGet}"
NUSPEC=$(unzip -p "$PKG" '*.nuspec' | tr -d '\r')
lower() { tr '[:upper:]' '[:lower:]'; }
ID=$(printf '%s\n' "$NUSPEC" | sed -n 's:.*<id>[[:space:]]*\([^<]*\)[[:space:]]*</id>.*:\1:p' | head -1 | lower)
VER=$(printf '%s\n' "$NUSPEC" | sed -n 's:.*<version>[[:space:]]*\([^<]*\)[[:space:]]*</version>.*:\1:p' | head -1 | lower)
if [ -z "$ID" ] || [ -z "$VER" ]; then
echo "Couldn't read the package ID and version from $PKG" >&2
exit 1
fi
DEST="$FEED/$ID/$VER"
mkdir -p "$DEST"
cp "$PKG" "$DEST/$ID.$VER.nupkg"
openssl dgst -sha512 -binary "$PKG" | base64 > "$DEST/$ID.$VER.nupkg.sha512"
printf '%s\n' "$NUSPEC" > "$DEST/$ID.nuspec"
echo "Seeded $DEST"
Run it against any one of your packages:
chmod +x seed-feed.sh
./seed-feed.sh MyCompany.MyLib.1.0.0.nupkg
The feed path defaults to ~/Development/NuGet; pass another path as the second argument if yours lives elsewhere. The lowercasing uses tr rather than Bash’s ${var,,} because macOS still ships Bash 3.2, which doesn’t support it.
The script uses the version exactly as the .nuspec gives it. NuGet normalises versions for folder names, padding 1.0 to 1.0.0 and dropping any +build metadata. Packages built with dotnet pack are normally already in that form, but if yours aren’t, seed with one that is.
Pushing the rest
With the feed seeded, the plain dotnet workflow does the right thing:
dotnet pack -c Release -o ./artifacts
dotnet nuget push "./artifacts/*.nupkg" --source ~/Development/NuGet
Each package now gets its own lowercase folder. Keep the quotes around the wildcard so dotnet expands it rather than the shell. A local folder needs no API key.
To restore from the feed in your projects, register it as a source once:
dotnet nuget add source "$HOME/Development/NuGet" --name Local
Use $HOME or a full path here, because a literal ~ saved into NuGet.Config isn’t always expanded. Check it took with dotnet nuget list source.
Gotchas
- Clear out flat packages first. If any
.nupkgfiles sit at the top level of the folder, it’s still detected as flat. Move them out before seeding. - Re-pushing a version. Pushing a version that’s already in the feed fails. Add
--skip-duplicateto skip it quietly, or better, bump the version. - Stale restores. If you do replace a package without changing its version, projects may keep restoring the old copy from the global cache. Clear it with
dotnet nuget locals all --clear.
Wrapping up
It’s a one-off step. Seed the folder with a single properly laid-out package, and from then on dotnet nuget push keeps the feed hierarchical and lowercase, with no extra tools to install.
If a package you’ve pushed still doesn’t show up in your projects, see Troubleshooting a Local NuGet Feed on macOS.