Which open-source alternative to GitHub Packages fits the package types you publish?
No single open-source product covers every GitHub Packages registry type equally, so the choice is made per package type. Gitea and Forgejo include built-in package registries that cover npm, Maven and container images among their supported types, Pulp is a multi-format repository manager with plugins for npm, Maven, Gem and others, and Harbor is the OCI-focused option for container images. Your team will usually end up with one of these for each format it publishes, or with a deliberate choice to consolidate on the one that covers most of them.
Start by listing every package type you publish to GitHub Packages today, along with the consumers of each one: CI jobs, internal developers, production deployments and external users. The maturity of each option differs by format. In Pulp, for example, the pulp_npm and pulp_maven plugins are still 0.x releases, and the pulp_rust plugin is a tech preview whose APIs may change without notice, so Cargo is not a format to plan around yet. Confirm the current state of each plugin you need in its repository before you commit, and check whether NuGet and RubyGems are covered by your chosen platform at all, since they are not in the Pulp plugin list supplied for this page.
The rest of this section is covered below: how each registry type maps to a candidate, what Maven publishing requires in Pulp, how billing and token rules change, and what migration involves for existing packages and consumers.
How each GitHub Packages registry maps to open-source options
This table maps each GitHub Packages registry to the self-hosted option that covers it, with coverage notes for Gitea or Forgejo, Pulp and Harbor. Treat each row as a starting point for testing, since package-type support depends on the version you deploy. The main takeaway is that npm, Maven and container images have the clearest paths, while Gradle, NuGet and RubyGems need confirmation before you commit.
| GitHub Packages registry | Open-source option and maturity | What to confirm before deciding |
|---|---|---|
| npm | Gitea and Forgejo list npm among their package types. Pulp's pulp_npm is 0.10.2, a 0.x release. | Scoped package naming, publish tokens and access rules in the exact version you plan to run. |
| RubyGems | Gitea and Forgejo package types must be checked for RubyGems. Pulp's pulp_gem is 0.8.0, a 0.x release. | Whether Gitea or Forgejo serves gem push and gem install in your version, and whether a Pulp gem plugin meets your publish needs. |
| Maven | Gitea and Forgejo list Maven among their package types. Pulp's pulp_maven is 0.35.0, a 0.x release, and can act as a pull-through cache. | Pulp's Maven deploy path does not use authentication and does not separate SNAPSHOT from release repositories, so confirm publish controls. |
| Gradle | Gradle publishes through Maven repository layouts, so coverage depends on the Maven support in each option. | Gitea or Forgejo support for Gradle publishing and the Gradle metadata they serve, tested with your own build. |
| NuGet | No NuGet plugin appears in Pulp's list of content plugins. Gitea and Forgejo NuGet support must be confirmed. | Whether Gitea or Forgejo serves the NuGet protocol in your version, and what replaces it if it does not. |
| Docker and OCI containers | Harbor is the dedicated option for OCI images. Pulp's pulp_container 2.30.0 syncs and hosts images, but its pull-through cache is a tech preview. | Gitea or Forgejo container registry limits, Harbor's fit for your image workflow and whether your team needs the Pulp cache mode. |
When Gitea or Forgejo, Pulp or Harbor fits better
GitHub Packages bundles several registry types into one service tied to your repositories. The self-hosted options split that job: a forge with built-in registries, a multi-format repository manager, or a container registry. Each card below describes where one option fits, what it gives up, and what to verify before you commit.
Gitea built-in package registries
Gitea is a good fit when packages should sit next to the repositories, issues and Gitea Actions that already publish them, and when one small team wants a single server to run. Gitea supports more than 20 registry types, including npm, PyPI, Maven and container images. The trade-off is that its registries are a feature of the forge, so the depth of each format depends on Gitea's own implementation. Confirm that each format your build uses, including the exact Maven or npm client behavior, works in a test project before migration. Commercial support for Gitea comes from CommitGo, Inc.
Forgejo built-in package registries
Forgejo fits teams that want the same forge-hosted model as Gitea but under GPL-3.0-or-later from v9.0 onward, with governance independent of any single company. Its registries are built into the forge, so publishing and access follow repository permissions. Forgejo Actions is not compatible with GitHub Actions, so any workflow that publishes packages must be rewritten, not just moved. Check which registry formats your pipeline needs against Forgejo's current documentation, because format coverage is the part most likely to differ from GitHub Packages.
Pulp for multi-format repositories
Pulp fits organizations that publish several package types, such as npm, Maven, Ruby gems, Python and RPM, and need one repository manager with a single REST API under /pulp/api/v3/. It is licensed GPLv2+ and describes itself as completely free and open source. Maturity varies by plugin: pulp_rpm and pulp_python are past version 1.0, while pulp_maven, pulp_npm and pulp_gem are still 0.x. The pulp_rust plugin is a tech preview without authentication support, so do not rely on it for private registries. Pulp's publish flow for Maven also lacks authentication, so restrict network access or test the workflow first.
Harbor for container images
Harbor fits teams whose main GitHub Packages use is container images and who want an OCI registry as a separate service from their forge. Pulp's container plugin covers Docker and OCI syncing and hosting, but its pull-through cache mode is a tech preview, so Harbor may be the simpler choice for a production image cache. Harbor's format coverage, license terms and current release line were not part of the supplied research, so confirm them directly with the Harbor project before you plan around them. Check image signing, vulnerability scanning and replication requirements against your own policy.
What GitHub Packages billing and token rules change for your team
Moving registries changes which downloads are free and which are billed. GitHub's billing documentation lists storage and data transfer quotas by plan: 500 MB of storage and 1 GB of data transfer per month on Free, 2 GB and 10 GB on Team, and 50 GB and 100 GB on Enterprise Cloud. Storage is shared with GitHub Actions artifacts, so a team that keeps large build outputs in Actions also consumes its package quota. Downloads are free for public packages, for data transferred in, and for GitHub Actions jobs that authenticate with GITHUB_TOKEN. Self-hosted runners that pull packages with a personal access token are billed. Check which of your pipelines use a personal access token today, because those are the costs that move first.
The GITHUB_TOKEN pattern disappears when packages move. Your own registry needs its own credentials, so plan a service account or deploy token per pipeline, stored in your CI secret store, with read and publish rights scoped to the repositories it needs. Gitea and Forgejo registries and Pulp or Harbor each use their own authentication model, so confirm the token types, expiry settings and per-format client configuration (for example, the .npmrc or Maven settings.xml entries your developers already use) in a test project before you change any build. Remove the old tokens from workflows once the new ones work, and rotate any that were shared between teams.
On your own registry the bill becomes an operating cost you own. You pay for servers, block or object storage that grows with every published version, backups of the registry database and artifacts, upgrades and the staff time to run them, and any support contract you choose. Storage and backup volume are driven by retention rules, the number of versions kept per package and how often pipelines republish. Ask any vendor or partner for a written scope that states who patches the registry, how backups are tested and what happens during an outage, and compare that scope with the per-plan quotas you are leaving. Confirm the current GitHub terms yourself, since GitHub has promised at least one month's notice before changing free container storage and bandwidth.
Registry-by-registry paths for npm, Maven, NuGet, RubyGems and containers
For npm packages, the candidate is Pulp with the pulp_npm plugin. It mirrors packages from upstream registries such as registry.npmjs.org, offers pull-through caching and accepts packages published with npm publish or yarn publish. Most teams keep their package scopes and change the registry endpoint, so the visible change is mainly in .npmrc and in the credentials used by CI.
- Plugin maturity: pulp_npm is at 0.10.2, a 0.x release. Pin the version in your deployment and expect configuration details to change between releases.
- Publish and install commands:
npm publishandnpm installkeep their syntax, but theregistryvalue in.npmrcmoves from the GitHub endpoint to your Pulp content URL. - Upstream mirroring: confirm that public dependencies resolve through the mirror, including scoped packages, and that lockfile integrity hashes still match.
- Publish credentials:
GITHUB_TOKENpublishing from Actions does not carry over. Create a dedicated publish credential for each pipeline and store it in your secrets manager.
Before cutover, test publish, install, and access control for each scope, and confirm how deprecation and removal of versions are handled in your setup.
For Maven packages, the candidate is Pulp with the pulp_maven plugin. It can act as a pull-through cache for Maven Central or any other Maven repository, and Pulp documents a deploy path using the Maven Deploy Plugin. That deploy path does not use authentication and does not distinguish SNAPSHOT from release repositories, so the publishing controls for these repositories must be built around Pulp rather than assumed from it.
- Plugin maturity: pulp_maven is at 0.35.0, a 0.x release. Treat its APIs as provisional and test upgrades in staging.
- Pull-through cache: point
settings.xmlmirror settings at the cache and verify checksums on a sample of artifacts. - Deploy without authentication: do not expose the deploy path to publishers unless a reverse proxy enforces identity in front of it. Confirm this design with the Pulp project before relying on it.
- SNAPSHOT and release handling: test that a release version cannot be overwritten and that snapshot builds are not promoted into release repositories by accident.
- Gradle projects: Gradle is listed among GitHub Packages registry types, but the supplied Pulp facts do not cover it separately. Confirm the publishing tasks and repository credentials in your build files.
Before cutover, run a full release cycle in staging, including a failed deploy and a rollback, and record which repository IDs and credentials each pipeline uses.
For NuGet packages, the gap is the first thing to resolve. The supplied Pulp content plugin list does not include a NuGet plugin, and the supplied Gitea registry list names npm, PyPI, Maven, container images and Helm charts without naming NuGet. Teams that publish .NET packages should not assume a built-in feed exists in the platform version they plan to run until it is confirmed in writing or in a test instance.
- Gap to confirm: ask the candidate vendor or project whether a NuGet feed is available in the version you plan to run, and get the answer in writing.
- Fallback options: if no open-source feed covers NuGet, evaluate a separate NuGet-capable repository manager or keep the existing feed in place until the gap is closed.
- Publish command changes:
dotnet nuget pushtargets the source URL innuget.config, so the source name and API key change in every pipeline. - Restore behavior: test
dotnet restoreagainst the new feed, including package source mapping, since mapping determines which feed resolves each package ID. - API key scope: verify whether publish keys can be scoped to a single pipeline and revoked without affecting other pipelines.
Before cutover, confirm feed support and key management in writing, then test restore, push and symbol package handling for one representative library.
For RubyGems, the candidate is Pulp with the pulp_gem plugin. The plugin is a 0.x release, and the supplied Pulp content plugin list includes Gem, so it is a credible starting point for a test. The supplied Gitea registry list does not name RubyGems, so confirm the Gitea or Forgejo option separately if that platform is part of your plan.
- Plugin maturity: pulp_gem is at 0.8.0, a 0.x release. Plan for configuration changes and pin the version.
- Publish command changes:
gem pushuses the host and credentials you configure, so update the credentials file and any publish scripts that reference the GitHub endpoint. - Gemfile sources: update the
sourcelines in each Gemfile and confirm that Bundler resolves the same versions from the new host. - Lockfile checks: compare
Gemfile.lockresults before and after the move to catch resolution differences. - Yank and removal behavior: confirm whether version yanking works the same way in your chosen registry, since teams often depend on it for withdrawing bad releases.
Before cutover, test a full publish, install and yank cycle in staging, and confirm the authentication method with the platform documentation before moving production gems.
For container images, the topic names Harbor as the self-hosted choice for OCI content, and Pulp can also host images through pulp_container. The pulp_container plugin is past version 1.0 at 2.30.0. It syncs Docker and OCI images from Docker Hub, Quay, Google Container Registry or any registry that implements the registry API, syncs Cosign signatures stored as OCI images and hosts pushed images. Its pull-through cache mode is a tech preview.
- Plugin maturity: pulp_container is at 2.30.0, past 1.0, but its pull-through cache mode is a tech preview, so keep that mode out of production paths.
- Image signatures: pulp_container syncs Cosign signatures stored as OCI images. Test verification in your admission controls before you rely on it.
- Command changes:
docker loginanddocker pushtarget the new hostname, so update tags, pull secrets and CI variables. - Current GitHub pricing: GitHub currently makes container storage and bandwidth in the Container registry free, with at least one month's notice promised before any change. Use this only to plan the timing of the move.
- Harbor features: confirm the replication, scanning, retention and tag immutability features you need in Harbor's own documentation before choosing it.
Before cutover, test pulls from every cluster and runner, including Kubernetes pull secrets, tag immutability and signature checks on a representative image.
Keeping the rest of your GitHub setup while packages move
Moving packages off GitHub touches more than the registry. CI pipelines, source hosting and identity all depend on the same GitHub tokens and accounts, so each needs a decision before the first package is republished. The cards below cover what each piece costs to keep, what breaks if it is left alone, and what to confirm with the platform vendor.
CI pipelines using GITHUB_TOKEN
Workflows that publish with the built-in GITHUB_TOKEN will fail once the registry is no longer GitHub Packages, and the token itself only exists inside GitHub Actions runs. Rewrite each publish step to authenticate against the new registry with a scoped credential stored as a CI secret. Gitea Actions is designed to be compatible with GitHub Actions, so many workflow files may run with few changes, but confirm each action you use is available on the Gitea instance. Test a publish from a branch before switching the default branch.
Actions compatibility differs by forge
Gitea Actions is designed to be compatible with GitHub Actions, while Forgejo Actions is designed to be familiar to GitHub Actions users but is not compatible with them. If your publish workflows depend on marketplace actions or GitHub-specific contexts, a Forgejo move means rewriting those steps and running separate runners. Inventory every uses: line across publish workflows before choosing the forge. Ask the vendor which runner types and labels are supported for your build images.
Repository hosting can stay on GitHub
Source hosting does not have to move in the same change as packages. Keeping repositories on GitHub while the registry runs elsewhere means publish jobs call an external registry from GitHub-hosted or self-hosted runners, which adds a network path and a credential that must be kept current. Plan this as an interim state with an end date. Confirm with your platform team whether runners can reach the self-hosted registry over the network you use and whether egress from GitHub-hosted runners is allowed by your policies.
Identity and registry access tokens
GitHub Packages access is tied to personal access tokens, fine-grained tokens and organization roles, so each consumer currently holds a credential that maps to a GitHub identity. The new registry issues its own tokens, and every developer, service account and runner needs a new credential with the same read or write scope. Map each existing role to a registry permission, rotate tokens on a schedule, and remove access for leavers. Confirm with the vendor how tokens are created, whether they can be scoped per repository or per package, and whether they can be linked to your single sign-on provider.
Billing and quota changes at cutover
GitHub Packages bills storage and data transfer on plan allowances, and self-hosted runners that download with a personal access token are billed. Once packages leave, those charges stop, but the storage and bandwidth of the new registry become your own cost. Before cutover, list which consumers pull packages through GitHub, including self-hosted runners, so none fail when the allowance no longer applies. Confirm the current GitHub billing terms on the GitHub Packages billing page before setting a cutover date.
How to move published packages and tokens off GitHub Packages
This sequence moves each published package and version to a self-hosted registry with verified checksums, switches consumers and CI to the new source, and retires GitHub publishing only after both registries have run side by side.
Inventory every package, version and consumer
Before anything moves, list each package by registry type (npm, Maven, NuGet, RubyGems or container image), its published versions, its owning repository and every consumer that installs it, including build pipelines, deployment jobs and developer machines. Record which versions are still referenced and which are deprecated or unused. Ask the GitHub account owner to confirm the export path available for your account and plan, because packages cannot be assumed to export in bulk. The inventory is done when every package has a row listing its versions, consumers and owner, and a named person has signed off on it.
Stand up the target registry in a test environment
Choose the target by format. Gitea or Forgejo's built-in package registries can cover npm, Maven, NuGet, RubyGems and container images for a single forge, while Pulp suits multi-format repositories and Harbor suits OCI images. Deploy the chosen platform in a non-production environment with the same authentication model you plan to use in production, and confirm in its documentation that each registry type your inventory lists is supported. The step is complete when a test package can be published and installed from a clean machine using the new endpoint.
Republish or mirror versions and verify checksums
Move each version either by republishing the original artifact or by mirroring it where the target supports upstream sync. Keep the original artifact files rather than rebuilding them from source, so the bytes stay the same. Compare the checksum of every migrated file against the value recorded from GitHub, and keep the comparison log with the inventory. Treat any mismatch as a blocker and investigate before continuing. The step is complete when every version in the inventory has a matching checksum record.
Handle deprecated versions and tokens explicitly
Decide what happens to versions marked deprecated and to any version that will not be migrated, and document that decision for each one. Create new access tokens on the target registry scoped to the least privilege each consumer needs, and do not copy GitHub personal access tokens into the new system. Revoke GitHub tokens only after the cut-over step. The step is complete when a written list shows every deprecated version and every token with its owner, scope and replacement.
Update consumer configuration and CI secrets
Point each package manager configuration at the new registry URL, such as the registry setting in a project's npm or Maven configuration, and replace the GitHub token references in CI with the new secrets. Check that build runners can reach the new endpoint from their network. Pilot the change on one low-risk repository first and confirm that installs and publishes succeed with the new credentials. The step is complete when a pilot build installs and publishes through the new registry without any GitHub credential in use.
Run both registries in parallel for a defined window
Keep GitHub Packages available for reads while consumers gradually move to the new registry. Set the window length in writing before it starts, and agree who monitors failed installs and failed publishes during it. Watch for consumers still pulling from GitHub, using the dependency and build logs as evidence. Confirm that GitHub billing and storage figures are stable during the window, since the storage and transfer allowances in GitHub's billing rules apply to packages still hosted there. The step is complete when the monitoring log shows no remaining consumers on GitHub endpoints.
Cut over and retire GitHub publishing
Switch publishing to the new registry, disable publish workflows that still target GitHub, and revoke the GitHub tokens listed in the earlier step. Archive the inventory, checksum log and deprecation decisions with the change record so auditors can trace each artifact. Keep read access to the old packages for the period your records require, then remove them. The step is complete when publishing runs only on the new registry, the GitHub tokens show as revoked, and the change record is filed.
What buyers ask before moving packages off GitHub
What does moving packages off GitHub Packages cost, and which parts drive it?
The cost splits into one-time work and recurring running costs. One-time work covers mapping each registry type, migrating or re-publishing packages, updating CI and developer tooling, and testing. Recurring costs cover the servers or VMs, object storage for package blobs, backups, upgrades, and the staff time or support contract that keeps the registry running. GitHub Packages bills storage and data transfer that are shared with Actions artifacts, with plan-based allowances listed in GitHub's billing documentation, so your current invoice and usage report are the baseline to compare against. Ask any quote to itemize each component separately and to state which parts are fixed and which scale with package volume and download traffic.
How long does a package cutover take, and what sets that time?
The duration depends on the number of registry types, the number and size of packages, how many CI pipelines and developer machines point at the old registry, and how many consumers rely on pinned versions. Teams usually run both registries in parallel during cutover so that existing builds keep resolving while new publishes move. Pulp can mirror or pull through upstream registries, which can shorten the overlap for some formats. Ask for a written plan with phases, a list of consumers, and a rollback step for each registry type before agreeing on a schedule.
What does the internal team need to provide during the move?
The internal team has to supply a complete inventory of what is published, who publishes it, and which pipelines use it. Engineers need to confirm package naming, scopes, version policies and how SNAPSHOT or pre-release builds are used. A platform owner should approve hosting, storage sizing, backup targets and identity integration. Security staff should review who may publish and who may read each repository. Without these inputs, the scope cannot be fixed in writing.
Can our own engineers migrate packages without an outside partner?
Yes, for a small set of registries with a limited number of consumers. Gitea and Forgejo include package registries for common formats, and Harbor or Pulp can host OCI images and multi-format packages, so a capable platform team can run a pilot. The harder parts are the operational ones: upgrades, backups, access control, monitoring and the cutover of CI pipelines. Teams that lack these skills often choose a fixed-scope engagement for those parts. DeployOpen plans, implements, hardens, backs up and documents self-hosted platforms in your environment under written scope, and a quote follows from that scope.
Which package registry types are not yet mature enough for production?
Maturity varies by format and by plugin. In Pulp, pulp_rpm, pulp_python, pulp_container and pulp_deb are past version 1.0, while pulp_maven, pulp_npm, pulp_gem, pulp_hugging_face and pulp_ansible are still 0.x releases, so their APIs may still change. The pulp_rust plugin is labeled a tech preview whose APIs may change without notice, and it does not yet support authentication, authorization or full registry syncing. The pulp_maven deploy feature does not use authentication and does not separate SNAPSHOT from release repositories. Pulp's container pull-through cache is also a tech preview. Confirm the current status of each plugin before you commit a format to production.
How do tokens, permissions and CI pipelines change after the move?
Each registry has its own authentication model, so GitHub tokens and GITHUB_TOKEN-based workflows will not carry over unchanged. You need to create service accounts or deploy tokens in the new registry, store them as CI secrets, and update the publish and install settings in npm, Maven, NuGet, RubyGems and container tooling. Gitea and Forgejo Actions can run workflows, but Forgejo Actions is not compatible with GitHub Actions, and Gitea Actions is designed to be compatible, so confirm each workflow's behavior in a test run. Plan token rotation and revocation of the old GitHub tokens as part of the cutover.
What does a fixed-scope DeployOpen engagement cover for packages?
A DeployOpen engagement is agreed in writing before work starts. The written scope typically names the registry types in use, the target platform, the hosting environment, the integration with your identity provider, the CI changes, the cutover and rollback steps, backup and upgrade procedures, and the documentation handed over. DeployOpen does not publish a price list, so a quote follows from that written scope. Ongoing support, upgrades and continuous testing are available as retainers. Anything outside the agreed scope, such as formats you have not listed, should be scoped separately so that cost and timing stay clear.
How DeployOpen scopes and delivers a package registry replacement
DeployOpen plans, implements, integrates, hardens, backs up and documents self-hosted open-source platforms inside your own cloud accounts, data centers or air-gapped networks. For a GitHub Packages replacement, we start by inventorying which package formats your teams publish and consume, then map each one to a registry that runs in your environment.
How we scope
- We inventory every npm, Maven, NuGet, RubyGems and container package your teams publish, with consumers and CI jobs that depend on each one.
- Engagements are fixed-scope, and the written scope names the registries, storage backend, identity integration and cutover deliverables before work begins.
- We confirm with you which plugin maturity levels and tech-preview features your formats depend on, so no one is surprised after go-live.
How we build
- We deploy the chosen registries, such as Gitea or Forgejo package registries, Pulp or Harbor, in your infrastructure with the storage and database you specify.
- We configure authentication tokens, access rules and proxy or mirror settings to match your current GitHub token and billing model.
- We test publish and install paths for each format, including CI pipelines, and run security testing against the registry endpoints before cutover.
How we hand over
- You receive architecture, runbooks, backup and restore procedures, and configuration records for every registry we deploy.
- We document the client-side changes developers must make, such as registry URLs and token setup for npm, Maven and NuGet clients.
- We prepare evidence of configuration and controls for your compliance assessors, while independent auditors remain responsible for any audit opinion.
How we support
- Ongoing support, upgrades and continuous testing are available as a retainer after handover, covering registry versions and plugin updates.
- We can back up and upgrade the registry platform on a schedule agreed in the written scope, with changes documented for your team.
- Ask for a written quote from a defined scope that lists components, responsibilities and what recurs after go-live, since we do not publish a price list.

