twine upload returns 401, or pip install says No matching distribution found—usually the package is fine and the private index is not wired: service down, upload URL confused with the install index, username not __token__, or a missing /simple/ suffix.
This article walks through AngusRepo Community self-hosted in order: install → repos and token → .pypirc / pip.conf → minimal-package upload and install → verify. Examples use repo.example.com—swap your domain.
1. Install (get a working console)
Prerequisites: Docker Engine + Compose v2; about 2 CPU / 4 GB; open 80; resolve gm.example.com and repo.example.com (labs can use /etc/hosts). Full steps: First run.
curl --fail --location --progress-bar -o AngusRepo-Community-1.0.0.zip \
https://repo.anguskit.com/raw/raw-public/AngusKit/repo/AngusRepo-Community-1.0.0.zip
unzip AngusRepo-Community-1.0.0.zip
cd AngusRepo-1.0.0
./install.sh
Wizard aligned with quickstart: Compose; Access → 2; TLS → 4 (HTTP :80); Database → Compose MySQL; Public DNS name → example.com. After Install finished.:
./bin/angusctl.sh doctor # wait for doctor: OK
Open http://gm.example.com/, then http://repo.example.com/. For HTTPS see Install & deploy.
The rest assumes HTTP self-hosted. On HTTP, pip usually needs trusted-host; after real TLS, switch to https:// and drop it. Create repos and a token before client config.
2. Repos, token, and topology
twine upload only goes to HOSTED; PROXY and GROUP are read-only. Create:
| Example name | Type | Role |
|---|---|---|
pypi-hosted |
HOSTED | twine upload target; disable overwrite |
pypi-proxy |
PROXY | Proxy PyPI (https://pypi.org) |
pypi-all |
GROUP | Unified pip entry; hosted → proxy |
Keep the two paths distinct:
-
Upload:
/pypi/<repo>/(HOSTED root) -
Install:
/pypi/<repo>/simple/(PEP 503; trailing/simple/required)
Also prepare:
- [ ] Access token (shown once—save it)
- [ ] CI Write on
pypi-hosted; developers Read onpypi-all - [ ] GROUP order hosted → pypi-proxy
export REPO_TOKEN="your_access_token"
export REPO_HOST="repo.example.com"
Field detail: PyPI Format.
3. Install: pip.conf / index-url
Default: install via GROUP (pypi-all/simple/), upload via HOSTED.
[global]
index-url = http://repo.example.com/pypi/pypi-all/simple/
trusted-host = repo.example.com
One-shot:
pip install some-pkg \
--index-url "http://$REPO_HOST/pypi/pypi-all/simple/" \
--trusted-host "$REPO_HOST"
Internal-only with public still on PyPI:
pip install your-package \
--index-url https://pypi.org/simple \
--extra-index-url "http://$REPO_HOST/pypi/pypi-hosted/simple/" \
--trusted-host "$REPO_HOST"
index-url / extra-index-url must end with /simple/.
4. Publish: .pypirc and twine
username is always __token__; put the access token in password.
[distutils]
index-servers =
angusrepo
[angusrepo]
repository = http://repo.example.com/pypi/pypi-hosted/
username = __token__
password = your_access_token
python3 -m pip install build twine
python3 -m build
twine upload \
--repository-url "http://$REPO_HOST/pypi/pypi-hosted/" \
--username __token__ \
--password "$REPO_TOKEN" \
dist/*
Or twine upload -r angusrepo dist/*. Same-version re-upload should fail—bump the version.
5. Verify the loop (minimal package)
mkdir -p /tmp/angus-pypi-demo && cd /tmp/angus-pypi-demo
cat > pyproject.toml <<'EOF'
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "angus-demo-utils"
version = "1.0.0"
description = "AngusRepo PyPI smoke package"
requires-python = ">=3.9"
EOF
mkdir -p angus_demo_utils
echo 'OK = True' > angus_demo_utils/__init__.py
python3 -m pip install -q build twine
python3 -m build
twine upload \
--repository-url "http://$REPO_HOST/pypi/pypi-hosted/" \
--username __token__ \
--password "$REPO_TOKEN" \
dist/*
Clean install:
python3 -m venv /tmp/angus-pypi-venv && source /tmp/angus-pypi-venv/bin/activate
pip install angus-demo-utils==1.0.0 \
--index-url "http://$REPO_HOST/pypi/pypi-all/simple/" \
--trusted-host "$REPO_HOST"
python -c "import angus_demo_utils; print(angus_demo_utils.OK)"
Expect True. The console should list 1.0.0.
Minimal CI snippet
- run: |
python -m pip install build twine
python -m build
twine upload \
--repository-url "http://$REPO_HOST/pypi/pypi-hosted/" \
--username __token__ \
--password "$REPO_TOKEN" \
dist/*
Poetry: point the primary source at GROUP /simple/ with username __token__. Pipelines: Jenkins build, test, publish.
6. Day-two ops
Cleanup — keep last N on HOSTED; Dry Run first—Cleanup Policies.
Cold PROXY — first public pulls are slow; warm if needed.
Ingest checks — light gate in Repo; deeper scanning elsewhere—admission gates and SBOM.
Error cheat sheet
| Symptom | Likely cause | Fix |
|---|---|---|
| twine 401 | username not __token__, or bad token |
Fix credentials; check HOSTED Write |
No matching distribution found |
missing /simple/, wrong repo |
use …/pypi-all/simple/
|
| Upload OK, install fails | still hitting public PyPI only | fix index-url to GROUP |
| SSL errors on HTTP | no trusted-host
|
add host in pip.conf |
| 409 / file exists | same version re-upload | bump version |
FAQ
Can I twine to a GROUP?
No. Upload only to HOSTED; pip install via GROUP /simple/.
Can username be my login name?
Per the PyPI token convention, use __token__. A login name usually yields 401.
Is trusted-host required on HTTP?
Yes. Without TLS, pip still expects certificate checks unless you trust the host.
Takeaways
- Install console → HOSTED + PROXY + GROUP → token.
- Upload HOSTED root; install GROUP
/simple/. -
__token__in username; smoke-test build → twine → pip.
Top comments (0)