Deploy your own private PyPI-compatible package registry on Railway in under 2 minutes.
Based on devpi, with persistent storage and full compatibility with pip, uv, and twine.
- A private Python package index served by
devpi-server+devpi-web(web UI + search). - Standard tooling: publish with
twineordevpi upload, install withpiporuv. - On-demand PyPI mirror: any package fetched through your index is cached in your volume.
- Persistent storage on a Railway volume — redeploys don't wipe your packages or logins.
- One service, one container, one volume. No Postgres, no reverse proxy, no moving parts.
- A Railway account.
- Local
pip/uv/twine/devpi-clientfor publishing and installing.
- Deploy the template on Railway (button above, or
railway initfrom a fork of this repo). - Attach a Volume to the service and mount it at
/data. This is mandatory — without it, your packages and logins are lost on every redeploy. - Set
DEVPI_ROOT_PASSWORDin the service variables before the first deploy. This seeds the devpirootaccount. If you skip it,rootstarts with an empty password and you'll have to change it manually viadevpi-client. - Generate a public domain in the Railway service settings (Networking → Generate Domain). Railway does not assign one automatically.
- Wait for the health check on
/+apito go green. You're live athttps://<your-domain>/.
The web UI is browse-only. devpi-web shows your indexes, packages, and search — but there's no web login form. All authenticated actions (logging in, creating users, creating indexes, uploading packages) go through the
devpi-clientCLI described below. This is by design in devpi.
pip install devpi-client
devpi use https://<your-domain>
devpi login root --password="$DEVPI_ROOT_PASSWORD"By default the server runs with --restrict-modify root, so only root can create users and indexes. While still logged in as root:
devpi user -c alice password=s3cret email=alice@example.com
devpi index -c alice/prod bases=root/pypiThen switch to the new user to publish:
devpi login alice --password=s3cret
devpi use alice/prodbases=root/pypi makes your index fall through to the on-demand PyPI mirror, so pip installing a public dependency through your index Just Works.
Want any authenticated user to create their own indexes? Set the service variable
DEVPI_RESTRICT_MODIFY=""and redeploy.
With devpi upload (builds + uploads from the current project):
devpi upload --formats sdist,bdist_wheelWith twine (from an existing dist/):
twine upload \
--repository-url https://<your-domain>/alice/prod/ \
--username alice --password s3cret \
dist/*With pip:
pip install \
--index-url https://alice:s3cret@<your-domain>/alice/prod/+simple/ \
my-private-pkgWith uv:
uv pip install \
--index https://alice:s3cret@<your-domain>/alice/prod/+simple/ \
my-private-pkgFor CI, prefer a scoped token over a password: see devpi-tokens.
| Variable | Default | Purpose |
|---|---|---|
PORT |
3141 |
Set by Railway. The server binds 0.0.0.0:$PORT. |
DEVPI_ROOT_PASSWORD |
(unset) | One-shot bootstrap of the root password on first init. Changing it later has no effect — use devpi user -m root password=.... |
DEVPI_SERVERDIR |
/data/server |
Where devpi stores packages, indexes, and metadata. Must be on the mounted volume. |
DEVPI_SECRETFILE |
/data/.secret |
Persistent server secret. Required so login tokens survive a redeploy. |
DEVPI_RESTRICT_MODIFY |
root |
Only root can create users/indexes. Set to an empty string to allow any logged-in user to create their own. |
DEVPI_THEME_DIR |
/app/theme |
Folder passed to devpi-server's --theme. Ships with a built-in modern theme (system fonts, dark mode). Point it at a folder under /data to use your own, or set to an empty string for the stock devpi look. |
DEVPI_OUTSIDE_URL |
(auto-detected) | Full public URL of the service, used by devpi-web to build absolute links. On Railway it is auto-derived from RAILWAY_PUBLIC_DOMAIN. Set it explicitly only if you front the service with a custom domain or another proxy. |
DEVPI_TRUSTED_PROXY |
* |
Passed to devpi-server --trusted-proxy. * trusts any proxy — required so X-Forwarded-Proto from Railway's edge is honoured and links are generated as https:// instead of http://. |
DEVPI_TRUSTED_PROXY_COUNT |
1 |
Number of proxies in front of the service. |
DEVPI_TRUSTED_PROXY_HEADERS |
x-forwarded-proto x-forwarded-for x-forwarded-host |
Forwarded headers devpi-server will trust. |
Everything stateful lives under /data:
/data/
├── server/ # devpi serverdir: packages, indexes, users
├── .secret # generated on first boot, keeps login tokens valid across redeploys
└── .initialized # sentinel so devpi-init runs exactly once
If you ever want a clean slate, delete the volume (or wipe .initialized) and redeploy.
- SQLite-backed (devpi default). Fine for small-to-medium teams. Postgres backend is not configured.
- No reverse proxy, no
devpi-lockdown. Anonymous reads of public indexes are allowed by devpi's default ACLs. - Single replica — no HA.
- Credentials in install URLs are visible in shell history. Use
devpi-tokensfor CI.
These are intentional trade-offs to keep the template simple and reliable. Fork it if you need more.
"I get a 404 at my domain." You forgot to Generate Domain in the Railway UI, or the health check on /+api hasn't gone green yet.
"My login tokens keep getting invalidated after a redeploy." The secret file isn't persisted — check that your volume is actually mounted at /data and that /data/.secret survives a restart.
"devpi upload says permission denied." You're using the wrong index. Run devpi use <user>/<index> and devpi login <user> before uploading.
"pip install can't find my private package." Double-check you're hitting /<user>/<index>/+simple/ (the +simple suffix is mandatory for PEP 503).
"Redeploy wiped my packages." No volume attached, or it's mounted somewhere other than /data. Re-check the service settings.
./scripts/smoke-test.shBuilds the image, boots a throwaway container, exercises the full user/index/upload/install flow, then restarts the container on the same volume to verify persistence.
MIT. See LICENSE.