Skip to content

description: Run pypiron in production on S3, GCS, or Azure: one config file, the cloud's own credentials, Docker Compose or systemd, and nodes behind a load balancer.

Deploy on cloud storage

Private packages and cached public PyPI from one URL, on one config file and one bucket. A node carries no state — the bucket holds the packages — so you add or replace nodes freely.

The config

private-prefix = "acme"

[serve]
bind-addr = "0.0.0.0:8080"
buckets = ["s3://acme-pypiron@us-east-1"]
proxy-upstream = "https://pypi.org"

[mirror]
exclude-newer = "7 days"

private-prefix reserves your package names. proxy-upstream serves public packages on demand: a cache miss comes from PyPI once, then stays local. exclude-newer is the dependency cooldown: fresh releases wait a week, on by default. Keeping it in the file makes the one useful mirror policy visible. To pre-load an approved list instead of proxying, see Package sources.

Point buckets at a bucket that already exists. On AWS there is usually nothing else to set: credentials come from the standard chain — environment, web identity, instance role, or task role. GCS (gs://) and Azure (az://) are the same shape with their own credentials: Storage.

Run it

PYPIRON_ADMIN_PASS enables publishing.

Docker Compose

services:
  pypiron:
    image: ghcr.io/blackthorn-interstellar/pypiron:latest
    command: serve --config /etc/pypiron/pypiron.toml
    ports:
      - "8080:8080"
    environment:
      PYPIRON_ADMIN_PASS: ${PYPIRON_ADMIN_PASS}
    volumes:
      - ./pypiron.toml:/etc/pypiron/pypiron.toml:ro

systemd

[Unit]
Description=pypiron
After=network-online.target
Wants=network-online.target

[Service]
Environment=PYPIRON_ADMIN_PASS=change-me
ExecStart=/usr/local/bin/pypiron serve --config /etc/pypiron/pypiron.toml
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

More nodes

Run more containers with the same config behind a load balancer. Point the load balancer's health check at /ready: it turns 503 the moment a node starts draining, so the balancer stops sending it traffic before shutdown.

Point clients at it

Publish to /legacy/, install from /simple/ — private and public packages from the same URL:

uv publish --publish-url http://HOST:8080/legacy/ \
  --username admin --password "$PYPIRON_ADMIN_PASS" dist/*

uv add --default-index http://HOST:8080/simple/ requests acme-widgets

With the proxy on, do not point clients at PyPI as an extra index. pypiron owns resolution and keeps private names private.

Behind a corporate proxy

If the server only reaches the internet through a corporate forward proxy, set the standard HTTPS_PROXY/HTTP_PROXY/NO_PROXY environment variables — the proxy upstream, sync, and the advisory feed all honor them. If that proxy intercepts TLS with a private CA, add --upstream-ca-cert /path/to/corp-ca.pem so it validates without turning verification off. Details: Behind a forward proxy.

Survive a region outage

One bucket rides out any node dying. To ride out the bucket's region — or a whole cloud — give every node the same ordered list of buckets and pypiron keeps them in sync and fails over on its own: Survive a region or cloud outage.