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-PSResourcecmdlets fromMicrosoft.PowerShell.PSResourceGet. PowerShell 7.4+ ships the module in the box; on 5.1/7.2+ install it withInstall-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
-SourceLocationand-PublishLocationparameters 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.
-Pathmust point at the folder containing the.psd1, not at the.psd1itself 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_PATHon Windows consumers. Long module paths underDocuments\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:
- 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) andSave-PSResourcethem onto the share. The first time the internet is down, you will be glad those five exist locally. - Schedule the mirror. A weekly task running the curated
Save-PSResource | Publish-PSResourceloop 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. - Document the registration step. New laptops need one
Register-PSResourceRepositorycall 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.


