This guide assumes a Windows or Linux host that can serve a folder over SMB (or just a local path on a single machine), and Powershell 5.1 or 7+ on every machine that will consume the repository.

If you've outgrown this setup, the Gitlab and Gitea posts cover repositories backed by real NuGet feeds.

PSResourceGet vs PowerShellGet v2: this post uses the modern Register-PSResourceRepository / Publish-PSResource / Find-PSResource / Install-PSResource cmdlets from Microsoft.PowerShell.PSResourceGet. PowerShell 7.4+ ships the module in the box; on 5.1/7.2+ install it with Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope CurrentUser -Force.

Why a File Share

The PSResourceGet provider doesn't actually need a NuGet server. It's perfectly happy treating any folder full of .nupkg files as a repository. That makes a UNC share (\\fileserver\PSModules$) or even a local path the simplest possible distribution mechanism no Gitea, no GitLab, no tokens, nothing to patch and nothing to monitor.

When this is the right answer:

  • Air-gapped or offline environments where pulling from the internet isn't an option.
  • Tiny teams (fewer than ~5 admins) where the operational overhead of running a registry isn't worth it.
  • Disaster recovery / break-glass scenarios where you want a known-good copy of every module on a USB stick.

When to outgrow it:

  • You need fine-grained per-module access control.
  • You publish hundreds of versions a month.
  • You want signed-release vs nightly channels.
  • You need an audit trail of who installed what.

For everything else, a folder works.

Create the Share

On Windows

$share = 'C:\PSModules'
$null = New-Item -ItemType Directory -Path $share -Force
$SmbShare = @{
    Name       = 'PSModules$'
    Path       = $share
    ReadAccess = 'Domain Computers'
    FullAccess = 'CORP\ps-publishers'
}

New-SmbShare @SmbShare

The trailing $ makes the share hidden it won't show up in network browsing. Discoverability is a feature for files; for a module repository, it's just noise.

NTFS permissions matter as much as share permissions:

$acl = Get-Acl $share
$acl.SetAccessRuleProtection($true, $false)
$rules = @(
    [System.Security.AccessControl.FileSystemAccessRule]::new(
        'CORP\Domain Computers','ReadAndExecute','ContainerInherit,ObjectInherit','None','Allow'),
    [System.Security.AccessControl.FileSystemAccessRule]::new(
        'CORP\ps-publishers','Modify','ContainerInherit,ObjectInherit','None','Allow'),
    [System.Security.AccessControl.FileSystemAccessRule]::new(
        'BUILTIN\Administrators','FullControl','ContainerInherit,ObjectInherit','None','Allow')
)
foreach ($r in $rules)
{
    $acl.AddAccessRule($r)
}
Set-Acl -Path $share -AclObject $acl

On Linux (Samba)

# /etc/samba/smb.conf
[PSModules$]
    path             = /srv/psmodules
    browseable       = no
    read only        = no
    valid users      = @ps-publishers, @domain-computers
    write list       = @ps-publishers
    create mask      = 0664
    directory mask   = 0775
    force group      = ps-publishers
sudo mkdir -p /srv/psmodules
sudo chgrp ps-publishers /srv/psmodules
sudo chmod 2775 /srv/psmodules
sudo systemctl reload smbd

Local-Only

For single-machine setups, skip SMB entirely and use a local folder. The same Register-PSResourceRepository works against C:\PSModules.

Register the Repository

On every consumer machine:

$PSResourceRepository = @{
    Name =     'fileshare' 
    Uri =      '\\fileserver\PSModules$' 
    Trusted = $true 
 }

Register-PSResourceRepository @PSResourceRepository

Verify:

Get-PSResourceRepository -Name fileshare

For local paths:

$PSResourceRepository = @{
    Name =     'local' 
    Uri =      'C:\PSModules' 
    Trusted = $true 
 }

Register-PSResourceRepository @PSResourceRepository

PSResourceGet collapses the old v2 -SourceLocation and -PublishLocation parameters into a single -Uri. If you had a setup where reads and writes pointed at different paths, you'll need to re-architect the modern cmdlets assume a single repository URI.

Publish a Module

Publish-PSResource -Path '.\MyModule' -Repository 'fileshare'

For a binary module from the C# publishing post:

Publish-PSResource -Path './out/MyModule/1.4.7' -Repository 'fileshare'

No API key needed. The file system permissions are the auth model. If a user can write to the share, they can publish; if they can't, they can't.

-Path must point at the folder containing the .psd1, not at the .psd1 itself and not at a parent folder. This is the most common "why won't it publish" mistake with PSResourceGet.

What Ends Up on the Share

After a publish, the share contains a flat directory of .nupkg files:

\\fileserver\PSModules$\
├── MyModule.1.0.0.nupkg
├── MyModule.1.4.7.nupkg
├── OtherModule.0.2.1.nupkg
└── OtherModule.0.3.0.nupkg

PSResourceGet treats this as a NuGet feed. Find-PSResource enumerates the folder and parses each nupkg's manifest; Install-PSResource extracts the right version into the user's PSModulePath.

Find and Install

Find-PSResource -Repository fileshare
Install-PSResource -Name 'MyModule' -Repository fileshare -Version '[1.4.7]'

PSResourceGet uses NuGet version range syntax [1.4.7] means "exactly 1.4.7". Leave off -Version entirely to install the newest version on the share.

For an entirely offline workflow, Save-PSResource downloads without installing useful for air-gapped transfers:

Save-PSResource -Name 'MyModule' -Repository fileshare -Path 'D:\transfer\modules'

Drop the resulting folder onto a USB stick, copy it onto the destination's $env:PSModulePath, and Import-Module MyModule works without any registration.

Mirroring the PSGallery to Your Share

For air-gapped environments, mirror what you need from the public gallery once, then reference your share from then on:

$wanted = @(
    'Pester', 'PSScriptAnalyzer', 'Microsoft.PowerShell.SecretManagement',
    'Microsoft.PowerShell.SecretStore'
)

foreach ($name in $wanted)
{
    Save-PSResource -Name $name -Repository PSGallery -Path 'C:\Temp\mirror' -TrustRepository
}

# Re-pack each saved folder as a nupkg targeted at the share
foreach ($mod in Get-ChildItem 'C:\Temp\mirror' -Directory)
{
    foreach ($ver in Get-ChildItem $mod.FullName -Directory)
    {
        Publish-PSResource -Path $ver.FullName -Repository fileshare
    }
}

Schedule this monthly. A small, curated mirror is much more useful than a "we have everything" mirror, because the curation step is when you notice that someone added a module nobody actually uses.

Operational Notes

  • Pin versions on production. With no NuGet server in front, "latest" depends entirely on what's in the folder. Install-PSResource -Name MyModule -Repository fileshare -Version '[1.4.7]' is the only safe form.
  • Back up the share. It contains your packaged modules. Losing it doesn't lose your source code, but it does lose every signed binary build you've shipped.
  • Use shadow copies / snapshots on the share. A bad publish that overwrites a release is recoverable in minutes if VSS / ZFS snapshots run nightly.
  • Don't put the share on a domain controller. Tempting, always-on, terrible idea. Pick any other file server.
  • Hide the share with a $ suffix. Stops users from stumbling onto a writable share when browsing the network.
  • Watch out for MAX_PATH on Windows consumers. Long module paths under Documents\PowerShell\Modules\ can hit 260 chars even on modern Windows. Enable long paths via Group Policy or install to a shorter root.

Where to Take This

Three concrete next moves now that the share works:

  1. Mirror the dependencies you actually use. Pick the five modules your scripts call most often (likely Pester, PSScriptAnalyzer, Microsoft.PowerShell.SecretManagement, plus your own helpers) and Save-PSResource them onto the share. The first time the internet is down, you will be glad those five exist locally.
  2. Schedule the mirror. A weekly task running the curated Save-PSResource | Publish-PSResource loop from the Mirroring section keeps the share fresh without manual intervention. Snapshot the share before each run so a bad upstream publish doesn't break you.
  3. Document the registration step. New laptops need one Register-PSResourceRepository call to see the share. Bake it into your machine-setup script (or a startup-script GPO) so nobody has to remember the URI.

When this setup stops fitting (you genuinely need per-module RBAC, audit trails, or pre-release channels), graduate to Gitea for a homelab footprint or GitLab for an org footprint. The publish/install cmdlets stay the same; only the URI changes. Until that day, a folder is a PowerShell repository, and it's the only mechanism that keeps working on day one of a network outage.