A Pulp repository viewer UI, inspired by Docker Hub.
Supports container (OCI) repositories, file repositories, and pull-through cache display (DockerHub, Quay.io, PyPI, npm) with client configuration commands.
Image on DockerHub
- Docker with Docker Compose
- pip/pipx (for pulp-cli)
# 1. Start Pulp + PulpHub
docker compose -f docker-compose.demo.yml up -d
# Wait ~30s for Pulp to fully start
# 2. Populate Pulp with test data
pip install pulp-cli[container]
# configured by default to work with the docker-compose Pulp instance
./bin/setup.sh
# populates the Pulp instance configured via ./bin/setup.sh
./bin/seed.sh
# 3. Open PulpHub
# http://localhost:8080
# Pulp URL: http://localhost:8081
# Credentials: admin / admindocker run -d -p 8080:80 \
-e PULP_URL=https://your.pulp.example.com \
docker.io/estb/pulp-hub:latestOpen http://localhost:8080 and log in.
CORS: the Pulp instance must allow cross-origin requests from PulpHub's origin. See nginx/pulp-cors-proxy.conf for an example reverse proxy configuration.
PulpHub is configured at runtime via environment variables — no rebuild needed when changing the target Pulp instance. See docs/configuration.md for the full reference.
| Variable | Required | Description |
|---|---|---|
PULP_URL |
✅ | Public URL of the Pulp API as seen by the user's browser (see docs/configuration.md) |
Example with docker-compose.yml:
services:
pulphub:
image: docker.io/estb/pulp-hub:latest
ports:
- '8080:80'
environment:
PULP_URL: http://localhost:8081The container fails to start with a clear error message if PULP_URL is missing.
PulpHub supports two authentication modes:
- Session auth (preferred): logs in via Django's
/auth/login/endpoint, then uses asessionidcookie. The password is sent once and not stored client-side. Detected automatically — if the Pulp instance exposes/auth/login/, session auth is used. - Basic auth (fallback): sends
Authorization: Basicheader on every request. Used when session auth is not available.
If PulpHub and Pulp are on different origins (e.g. hub.example.com and registry.example.com), the Pulp reverse proxy needs extra configuration:
# Specific origin (not wildcard) + credentials
add_header Access-Control-Allow-Origin "https://hub.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-CSRFToken" always;
# Rewrite Django cookies for cross-origin use (requires nginx >= 1.19.3)
proxy_cookie_flags sessionid SameSite=None Secure;
proxy_cookie_flags csrftoken SameSite=None Secure;HTTPS is required for cross-origin session auth (
SameSite=Nonecookies requireSecure).
See nginx/pulp-cors-proxy.conf for the full example with commented-out session auth lines.
To test an image pull through the pull-through cache over HTTP (local dev):
# Login (required once)
podman login --tls-verify=false localhost:8081 -u admin -p admin
# Pull through the cache
podman pull --tls-verify=false localhost:8081/dockerhub-cache/library/nginx:latestNote:
--tls-verify=falseis required because Pulp is exposed over HTTP. In production with TLS, this flag is not needed.
To bypass Docker Hub rate limiting during seeding, seed.sh supports authentication via environment variables:
# Edit .env with your credentials
cp .env.example .env
./bin/seed.shThe password is a Personal Access Token, not the account password.
Without these variables, seed.sh works normally in anonymous mode.
Il faut lancer un Pulp de dev et s'y connecter, par défaut le Pulp de dev tourne sur localhost:8081.
- Dev Containers CLI (
npm install -g @devcontainers/cli) - Docker
make create-pulp # First time: creates Pulp + CORS proxy on http://localhost:8081
# Wait ~30s for Pulp to start
make start-pulp # Restart after a stopmake up # Start the devcontainer
make setup # Configure pulp-cli (default URL: http://host.docker.internal:8081)
make seed # Populate Pulp with test data
make dev # Start the dev server (http://localhost:5173)make stop-pulpmake helpmake test # E2E Playwright
make test-record # Re-record tapesSee docs/e2e-tests.md for the Talkback setup, the
mocked /auth/* endpoints, and the recommended workflow for adding new
tests without re-recording the whole suite.
Pulp operates in on_demand mode: during a sync, only metadata (manifests, tags) are downloaded. Layers (blobs) are fetched on demand during a pull.
Consequence: only synced tags are available. To add a new tag:
# Add a tag to the remote filter
pulp container remote update \
--name "dockerhub/library/alpine" \
--include-tags '["3.17","3.18","3.19","latest"]'
# Re-sync the repo
pulp container repository sync \
--name "dockerhub/library/alpine" \
--remote "dockerhub/library/alpine"Only tags filtered via --include-tags on the remote are synced.
A pull on a non-synced tag will return manifest unknown.
Without authentication, Docker Hub limits to ~100 pulls/6h.
Each synced tag consumes pulls (manifests + layers).
This is why --include-tags filters on a small number of tags in seed.sh.
Without this filter, syncing alpine would pull hundreds of tags and exhaust the quota.