Self-hosted service
pypiserver/pypiserver avatar
pypiserver/pypiserver

pypiserver's type-check target cannot fail, and its README says MIT while its classifiers say BSD

Minimal PyPI server for uploading & downloading packages with pip/easy_install

2,070 stars337 forksPythonNOASSERTION

At a glance

What is it?
A minimal PyPI-compatible index that serves packages from ordinary directories with no database, where four different licence statements coexist, the default compose service has no authentication at all, and the documented way to add authentication means overwriting the command that says where packages live.
Who is it for?
pypiserver is a good fit for one job: serving a directory of packages to a closed set of machines that already trust you, where pip is configured with an index URL and nothing else changes. It is a poor fit for anything that needs accounts, auditing or a package lifecycle, because the index is a filesystem and the catalogue is a directory listing.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 8, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The index is a directory listing, and that decides everything else

pypiserver is a web application that implements the PyPI interfaces closely enough that pip and twine treat it as an index. What sits behind those interfaces is not a database. The readme says it is based on a single microframework and that it serves packages from regular directories.

So the entire state of the index is a set of files on disk: wheels, source distributions, eggs, and the PGP signatures that accompany them, all in ordinary directories you can browse.

That single fact explains most of the project's behaviour and most of its limits. There is no metadata store to query, so the catalogue is generated by walking a filesystem. There is no user table, so authentication is a layer in front of the application rather than part of it. There is no upload record, so deleting a file is how you unpublish a package, and nothing logs that it happened. And there is no cache to invalidate, because there is nothing cached.

Upload is correspondingly permissive. The readme names five ways to put a package in: pip, setuptools, twine, a dedicated uploader tool, or copying the file over with a secure shell copy. The last of those is worth sitting with. If the documented path for publishing includes a file copy, then every property that makes this project useful, simplicity and being a directory, also means the security model is whatever is in front of the port.

This is a deliberate trade, not an oversight, and for the use case the project targets it is the right one.

The default compose service publishes a port with no authentication

The compose file in this repository is not one configuration. It is several, laid out as separate services with a comment block above each explaining the delta. The first one is the baseline:

yaml
    pypiserver-default:
        image: pypiserver/pypiserver:latest
        ports:
            - "8080:8080"

Three lines, and what is absent is the point. There is no command override, no credential file, no authentication provider, no proxy. Whatever access control exists in this configuration exists somewhere the compose file does not mention.

The next service in the file adds authentication by mounting a locally created password file into the container and rewriting the command. The comment above it explains why the rewrite is necessary, and this is the sentence worth reading twice: because the default command is also what tells pypiserver where to serve packages from, overriding it to add authentication means you have to put that path back yourself.

That is the sharpest edge in the whole deployment story. A user who copies the authenticated example gets a password file and a server that is suddenly looking in the wrong directory for packages. Nothing warns them at runtime. The fix is to carry the package directory into the new command, and the comment does say so, but it is one clause in a long explanatory block.

Two more details. The default image tag is `latest`, so the baseline service tracks the newest published build, and the file still carries a top-level version key that modern Compose treats as obsolete. And the comment warns that the data volume survives restarts but is destroyed by removing volumes, which is the correct warning to give about the one thing that cannot be regenerated.

The type-check target cannot fail, and the Makefile says why

The developer tooling is in a Makefile rather than in the manifest, which means the interesting parts are not visible in the package metadata at all.

Here is the entire type-check target:

makefile
check-types: pyproject.toml ./pypiserver ./docker ./tests
	@echo ">>> 📋 Checking the Python types"
	uv run mypy pypiserver tests docker || echo "--- 🫣 Fixing type errors is still in progress"
	uv run mypy docker/test_docker.py pypiserver/config.py tests/test_init.py bin/bumpver.py --follow-imports="skip"

Read the first mypy invocation carefully. It runs over the package, the tests and the docker helpers, and then, if it exits non-zero, the failure is swallowed by a shell `or` that prints a message and returns success. So this target cannot fail on type errors, and the message it prints when mypy complains is an admission that the work is unfinished rather than a report of a problem.

The second invocation is the one that matters. It runs mypy again over four specific files with imports skipped, which is the configuration that makes a partial check meaningful: you get diagnostics for those files without being flooded by everything they pull in. That the authors know which four files are close enough to clean is itself informative, and naming them is more useful than a blanket suppression.

The difference between those two invocations is the honest part of this target. A project that had switched type checking off would not have left the second line in place. A project that had fixed everything would not need the first line's escape hatch.

The rest of the Makefile is more conventional and includes one sharp edge of its own: the readme formatting target copies the file to a hardcoded path under the system temporary directory, makes that directory, reformats, diffs and removes it. A fixed path in a shared temporary directory collides with anything else using the same name.

Four licence statements, three of which disagree

The licence picture in this repository needs to be read carefully, because it does not resolve.

The repository's licence field comes back as unrecognised rather than as a name. The readme's own header table gives the licence as a combination of two permissive licences. The package classifiers declare two OSI-approved licences, one of them the zlib and libpng licence and the other the BSD licence. And the manifest itself uses the file-reference form of the licence key, pointing at a licence file, with no SPDX expression anywhere.

So the readme says one pair, the classifiers say another pair, and neither matches the other. The BSD classifier in particular has no counterpart in the readme's statement, and the metadata field that tooling reads is the one that says nothing at all.

What is not in dispute is that a licence file exists and that the readme links to its raw form, so there is a text to read and the disagreement is about how it should be labelled rather than about whether the code is licensed.

For an organisation running its own compliance process, the practical instruction is to read the file rather than to read the metadata, and to treat the classifier list as unreliable until it matches. This is exactly the case the project does not resolve for you, and it is worth resolving once for your own use rather than per-installation.

One related piece of hygiene is done properly: the manifest declares the version as dynamic, and there is a version-bump script in the binary directory. The version is derived rather than typed, which means the header table's version and date come from the same source of truth as the tag.

A minimal server with a twenty-entry table of contents

The readme's contents list is longer than one would expect from something described as minimal, and reading it is the fastest way to understand the project's scope.

Installation and usage come first, with separate subsections for running the server and for updating it. Then client-side configuration, split between configuring pip and configuring easy_install. Then remote upload, split three ways: password-file authentication, upload with setuptools, and upload with a publishing tool.

After that the recipes section. It covers managing the package directory, serving thousands of packages, and running as a service three separate ways: a systemd unit, a supervisor configuration, and the NSSM wrapper for Windows. Serving behind a reverse proxy gets three subsections as well, for Nginx, for terminating HTTPS, and for Traefik. Then there is a section on using a different application server, naming Apache, gunicorn and paste in turn, and a section on the HTTP API with its own subsection for pluggable authentication providers.

The last two entries are the ones that make the list unusual. One is a recipe for using the server from MicroPython, which is a genuinely different deployment constraint from anything above it. The other is a custom health-check endpoint, which is what you build when you expect to run this behind an orchestrator.

Taken together the list says two things. The first is that the project's real users are operators, not developers: the majority of the documentation is about service managers, proxies and server configuration. The second is that the maintenance story is mature. A project that has documented three supervisors, three reverse proxies, three application servers and a Windows service wrapper has been deployed in a lot of different shapes, and the documentation has been written by someone who answered the questions.

The readme names the software you should not use instead

There is a note early in the readme that deserves more attention than it usually gets.

It says the official software powering the public package index is a project called Warehouse. It then says Warehouse is specialised to be that site's own software, should not be used in other contexts, and specifically does not officially support being used as a custom package index for people serving their own packages.

That is an unusual thing for a competing project to write about its more capable alternative, and it is the most useful paragraph in the document. It tells you the honest reason this project exists rather than implying the alternative is inferior: Warehouse does a much larger job, and running it yourself is not supported.

The same paragraph also sets the expectation for everything else. pypiserver implements the same interfaces so that standard tooling works against it unchanged, which is a compatibility promise, not a capability promise. You get pip and twine behaving normally and you do not get accounts, permissions, a web upload form, provenance, or a package lifecycle.

The related note about maintenance status is equally to the point. The package is classified as production stable, its Python floor is 3.10 with classifiers running through 3.14 on both CPython and PyPy, and the container image is built on a Python 3.14 slim Alpine base. The release history is three patch versions over about thirteen months, and the newest tag was published the same afternoon as the last commit to the branch.

The project also says out loud that it is recruiting. The readme names four maintainers and then adds an invitation for someone new with an issue number attached, and the manifest separates the original authors from the currently active maintainers with a comment explaining that the first list is chronology and the second is the present. For a project with this history that is a healthy sign rather than a worrying one.

Editorial conclusion

pypiserver is a good fit for one job: serving a directory of packages to a closed set of machines that already trust you, where pip is configured with an index URL and nothing else changes. It is a poor fit for anything that needs accounts, auditing or a package lifecycle, because the index is a filesystem and the catalogue is a directory listing. Three things to settle before you deploy it. Decide where access control lives, because the default compose service publishes a port with no authentication and the application delegates the entire boundary to a reverse proxy or an htpasswd file in front of it. Then check what you copied when you turned authentication on, because the compose file itself warns that overriding the command to add it also drops the flag saying where packages are served from. And settle the licence question internally, because the readme, the package classifiers and the repository metadata give three different answers and you should not rely on any of them. For anything larger, the readme points at the obvious alternative and is unusually honest that the software running the public index is not the right tool here. Teams wanting a package index they can trust with credentials and lifecycle management should look at that alternative rather than extending this one.

Frequently asked questions

What is pypiserver and does it need a database?

It is a minimal PyPI-compatible index that implements the interfaces pip and twine expect, built on the bottle microframework. It serves wheels, source distributions, eggs and their PGP signatures from ordinary directories, so there is no database and the catalogue is a filesystem.

Does pypiserver have authentication by default?

No. The default service in the compose file publishes a port with no command override, no credential file and no authentication provider. Access control is expected to sit in front of the application, in a reverse proxy or a password file, and the readme documents adding a password file yourself.

What catch comes with enabling authentication on pypiserver?

Overriding the container command to enable authentication also removes the flag telling the server where to serve packages from, because both come from the same default command. The compose file's own comment says you must carry the package directory over into the new command yourself.

How do I upload packages to pypiserver?

Five ways are named: pip, setuptools, twine, a dedicated uploader tool, or copying the file directly with a secure shell copy. Because the last of those is a file copy, the whole security boundary lives outside the application.

What licence is pypiserver under?

The documentation and the package metadata disagree. The readme header gives a combination of two permissive licences, the classifiers declare the zlib and libpng licence together with the BSD licence, the manifest uses the file-reference form with no SPDX expression, and the repository's licence field is unrecognised. A licence file is present and linked.

Can I run pypiserver behind nginx or with a different WSGI server?

Yes, and both are documented as recipes. The readme covers Nginx, HTTPS termination and Traefik behind a reverse proxy, and separately names Apache, gunicorn and paste as alternative application servers, plus systemd, supervisor and NSSM for running as a service.

Official sources

  1. Issues
  2. Project website
  3. pypiserver/pypiserver on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pypiserver-pypiserver.svg)](https://hysenlabs.com/projects/pypiserver-pypiserver)