Skip to content

Repository files navigation

.NET Docker

PubNet

Self-hosted pub.dev alternative.

Contents


PubNet Homepage

Development

The src folder of this repository contains a docker-compose.yml, which contains three services used to aid in developing PubNet:

  • database: A postgres database (user: pubnet, password: pubnet)
  • adminer: A webinterface for managing the database
  • seq: A useful logging service

The only service you need to start is database, though the other services help a lot.

Note

You can also use a database you set up on your host, but currently only postgres is supported.

Architecture

The solution consists of a few projects, three of them compose the whole PubNet service:

  • PubNet.API: provides the API used by the dart pub command line tool to fetch and upload new packages
  • PubNet.Frontend: contains a Blazor WebAssembly project to act as a frontend for the API
  • PubNet.Worker: executes tasks (scheduled and unscheduled) to analyze packages and do general housekeeping

Debugging

You can use any IDE you want, as long as it supports debugging .NET 10 or higher.

For the PubNet.Frontend project, it is recommended to run it using dotnet watch to hot-reload changes.

Database migrations

This project uses Entity Framework Core with the code-first approach, so migrations are added using dotnet ef migrations add <name> and executed using dotnet ef database update in the PubNet.Database project folder. Currently, the database project expects the credentials and database name to all be equal to pubnet.

Contributions

If you want to contribute improvements or bugfixes, fork this repository, create a branch, commit your changes to it and open a pull request here on GitHub.

Hosting

Note

In case you only need a simple, privately hosted pub package API, the API project alone is sufficient.

When you are ready to deploy PubNet, you may want to review the OpenRegistration setting in the backend-appsettings.json to toggle whether anyone is able to register an account. This setting can be changed at runtime.

Authentication

PubNet is a private package host: reading packages and authors requires an account, not just publishing. Every package and author endpoint is authenticated, including the version listing and the archive download that dart pub get uses.

Consumers therefore need a token for the instance:

dart pub token add https://pubnet.example.com

The dart pub client handles this on its own: it sends the token when resolving a package, and when it receives a 401 while downloading an archive it retries the request with the token. For CI, dart pub token add <url> --env-var PUB_TOKEN reads the token from the environment instead of storing it on disk.

Note

Because reads are authenticated, closing registrations (OpenRegistration: false) is what actually keeps an instance private. The first-time setup creates the administrator account; further accounts are created by registration.

Hosted upstream

PubNet can fall back to an upstream hosted repository for package metadata and archives that are not stored locally.

  • The default upstream is https://pub.dev/api/.
  • You can point it at unpub by setting HostedUpstream:BaseUrl to the upstream API base URL, for example https://unpub.example.com/api/.
  • Environment variables override JSON configuration in ASP.NET Core, so Docker deployments can use HostedUpstream__BaseUrl.
  • If the configured value is missing or invalid, PubNet falls back to https://pub.dev/api/.

Example values:

{
  "HostedUpstream": {
    "BaseUrl": "https://unpub.example.com/api/"
  }
}
export HostedUpstream__BaseUrl="https://unpub.example.com/api/"

Important behavior notes:

  • The upstream URL must be the API base URL, not just the site root.
  • PubNet still stores and manages its own local packages, authors, and permissions.
  • Only package lookups and archive fallback use the upstream. Package list pages and author data remain local to PubNet.
  • Mirrored metadata is served by PubNet and is authenticated like every other package lookup, so which upstream packages an instance has looked up is not readable without an account.
  • Mirrored archives are not: PubNet answers the download with a redirect to the upstream, and the client fetches those bytes straight from there without a PubNet token. That content is public upstream anyway and is not hosted by PubNet, so this only applies to packages that are not stored locally. Archives of locally published packages are served by PubNet and stay authenticated.

Using docker-compose.yml

docker-compose.yml template

Create a docker-compose.yml with the following contents:

volumes:
  postgres_data:
  pubnet_packages:
  caddy_data:
  caddy_config:

services:
  database:
    image: postgres:18
    restart: always
    environment:
      POSTGRES_USER: "pubnet"
      POSTGRES_PASSWORD: "pubnet"
    volumes:
      - postgres_data:/var/lib/postgresql

  backend:
    image: ghcr.io/ricardoboss/pubnet/api:main
    restart: always
    environment:
      HostedUpstream__BaseUrl: "https://unpub.example.com/api/"
    volumes:
      - "./backend-appsettings.json:/app/appsettings.Production.json"
      - "pubnet_packages:/app/packages"
    depends_on:
      - database
      - caddy

  worker:
    image: ghcr.io/ricardoboss/pubnet/worker:main
    restart: always
    volumes:
      - "./worker-appsettings.json:/app/appsettings.Production.json"
      - "pubnet_packages:/app/packages"
    depends_on:
      - database

  frontend:
    image: ghcr.io/ricardoboss/pubnet/frontend:main
    restart: always
    depends_on:
      - backend
      - caddy

  # you can choose any reverse proxy you want, Caddy is not required
  caddy:
    image: caddy
    restart: always
    volumes:
      - "./Caddyfile:/etc/caddy/Caddyfile"
      - "caddy_data:/data"
      - "caddy_config:/config"
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"

Note

In this example, caddy is used as a reverse-proxy.

You can also host the backend and frontend on different ports, and publish them directly, removing the need to configure a reverse proxy entirely.

Reverse proxy configuration (Caddyfile)

In case you want a reverse proxy, configure it appropriately (in this case using a Caddyfile):

*:80, *:443 {
    reverse_proxy /api/* backend:80
    reverse_proxy /* frontend:80
}
backend-appsettings.json template
{
  "AllowedOrigins": [
    "http://localhost"
  ],
  "ConnectionStrings": {
    "PubNet": "Host=database;Database=pubnet;Username=pubnet;Password=pubnet"
  },
  "Jwt": {
    "Issuer": "http://localhost",
    "Audience": "http://localhost",
    "SecretKey": "GenerateASecureKey!"
  },
  "PackageStorage": {
    "Path": "./packages"
  },
  "HostedUpstream": {
    "BaseUrl": "https://pub.dev/api/"
  },
  "OpenRegistration": true,
  "SmtpAccount": {
    "Host": "localhost",
    "Port": 1025,
    "TLS": false,
    "Login": "myMtaLogin",
    "Password": "myMtaPassword"
  }
}
worker-appsettings.json template

Add a worker-appsettings.json:

You could use the same appsettings for the worker and the backend as their options shouldn't interfere.

{
  "ConnectionStrings": {
    "PubNet": "Host=database;Database=pubnet;Username=pubnet;Password=pubnet"
  },
  "PackageStorage": {
    "Path": "./packages",
    "PendingMaxAge": "00:05:00"
  }
}
Docker environment variable override

If you do not want to store the upstream in backend-appsettings.json, set it directly in the backend container environment:

services:
  backend:
    image: ghcr.io/ricardoboss/pubnet/api:main
    environment:
      HostedUpstream__BaseUrl: "https://unpub.example.com/api/"

When HostedUpstream__BaseUrl is empty, missing, or not a valid absolute http/https URL, PubNet falls back to https://pub.dev/api/.

Finally, start your own PubNet using

docker-compose up -d

and access it at https://localhost.

Other approaches

It should be possible to host the API using IIS, but it is not supported.

Same goes for the Worker: it should be possible to run it as a Windows service/systemd unit, but is not supported.

The frontend can be hosted from anywhere, as long as the backend-appsettings.json contains the domain to allow CORS.

License

This project is licensed under the Apache 2.0 license. For more information, see LICENSE.

Screenshots

This is a screenshot of how a package looks like after uploading and analysis:

Page for the package 'nmea'

This screenshot shows different versions in a table:

Page for the package 'nmea' versions

LLM generated code

This repository contains code generated by LLMs. All code is human-reviewed before merging. PRs using LLM code are allowed.

Releases

Packages

Used by

Contributors

Languages