P5: Add devpi k8s manifests and ArgoCD app

- Dockerfile for devpi-server + devpi-web image
- StatefulSet with 50Gi PVC for data persistence
- Tailscale Ingress for pypi.tail8d86e.ts.net
- README with setup and usage instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-01-20 09:50:15 -08:00
commit 173a9134d3
7 changed files with 221 additions and 0 deletions

30
argocd/apps/devpi.yaml Normal file
View file

@ -0,0 +1,30 @@
# devpi PyPI Caching Proxy
# Provides PyPI cache and private package hosting
#
# After first deployment, initialize devpi:
# kubectl -n devpi exec -it devpi-0 -- devpi-init --serverdir /devpi --root-passwd <password>
# kubectl -n devpi rollout restart statefulset devpi
#
# Then create user/index:
# uvx devpi use https://pypi.tail8d86e.ts.net
# uvx devpi login root
# uvx devpi user -c eblume email=blume.erich@gmail.com
# uvx devpi index -c eblume/dev bases=root/pypi
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: devpi
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/devpi
destination:
server: https://kubernetes.default.svc
namespace: devpi
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -0,0 +1,19 @@
FROM python:3.12-slim
# Install devpi-server and devpi-web
RUN pip install --no-cache-dir devpi-server devpi-web
# Create non-root user
RUN useradd -r -u 1000 devpi && mkdir -p /devpi && chown devpi:devpi /devpi
USER devpi
WORKDIR /devpi
# Expose default port
EXPOSE 3141
# Use ENTRYPOINT for flexibility
ENTRYPOINT ["devpi-server"]
# Default args (can be overridden)
CMD ["--serverdir", "/devpi", "--host", "0.0.0.0", "--port", "3141"]

View file

@ -0,0 +1,70 @@
# devpi PyPI Caching Proxy
devpi-server running in Kubernetes, providing:
- PyPI caching proxy at `root/pypi`
- Private package hosting at `eblume/dev`
## Setup
### 1. Deploy via ArgoCD
```bash
argocd app sync apps
argocd app sync devpi
```
### 2. Initialize devpi (first time only)
After the StatefulSet is running, initialize devpi with a root password:
```bash
# Get the root password from 1Password
ROOT_PASSWORD=$(op --vault blumeops item get <item-id> --fields password --reveal)
# Initialize devpi
kubectl -n devpi exec -it devpi-0 -- devpi-init --serverdir /devpi --root-passwd "$ROOT_PASSWORD"
# Restart the pod to pick up the initialized state
kubectl -n devpi rollout restart statefulset devpi
```
### 3. Create user and index
```bash
# Login to devpi
uvx devpi use https://pypi.tail8d86e.ts.net
uvx devpi login root
# Create user
uvx devpi user -c eblume email=blume.erich@gmail.com
# Create private index inheriting from PyPI
uvx devpi index -c eblume/dev bases=root/pypi
```
## Usage
### As pip index (caching proxy)
Configure `~/.config/pip/pip.conf`:
```ini
[global]
index-url = https://pypi.tail8d86e.ts.net/root/pypi/+simple/
trusted-host = pypi.tail8d86e.ts.net
```
### Upload private packages
```bash
# Build and publish
cd ~/code/personal/your-package
uv build
uv publish --publish-url https://pypi.tail8d86e.ts.net/eblume/dev/
```
## URLs
- Web UI: https://pypi.tail8d86e.ts.net
- PyPI cache: https://pypi.tail8d86e.ts.net/root/pypi/+simple/
- Private index: https://pypi.tail8d86e.ts.net/eblume/dev/+simple/

View file

@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: devpi-tailscale
namespace: devpi
annotations:
tailscale.com/proxy-class: "crio-compat"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: devpi
port:
number: 3141
tls:
- hosts:
- pypi

View file

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: devpi
resources:
- statefulset.yaml
- service.yaml
- ingress-tailscale.yaml

View file

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: devpi
namespace: devpi
spec:
selector:
app: devpi
ports:
- name: http
port: 3141
targetPort: 3141
protocol: TCP

View file

@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: devpi
namespace: devpi
spec:
serviceName: devpi
replicas: 1
selector:
matchLabels:
app: devpi
template:
metadata:
labels:
app: devpi
spec:
securityContext:
fsGroup: 1000
containers:
- name: devpi
image: registry.tail8d86e.ts.net/blumeops/devpi:latest
args:
- "--serverdir"
- "/devpi"
- "--host"
- "0.0.0.0"
- "--port"
- "3141"
- "--outside-url"
- "https://pypi.tail8d86e.ts.net"
ports:
- containerPort: 3141
name: http
volumeMounts:
- name: data
mountPath: /devpi
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /+api
port: 3141
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /+api
port: 3141
initialDelaySeconds: 10
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi