Staging Pipeline¶
The Staging page is the recommended way to get images into your registry:
instead of docker pull + docker push blind, Portalcrane pulls the image
server-side, scans it, and only then lets you decide where it goes.
sequenceDiagram
participant U as User
participant P as Portalcrane
participant S as Source (Docker Hub / external registry)
participant T as Trivy
participant R as Destination registry
U->>P: Search image (Docker Hub)
U->>P: POST /api/staging/pull (image, tag, source)
P->>S: skopeo copy → OCI layout
P->>T: Scan OCI layout
T-->>P: Vulnerability report
U->>P: Review report, POST /api/staging/push
P->>R: skopeo copy → registry
1. Search¶
GET /api/staging/search/dockerhub?q=<term> proxies the public Docker Hub
search API, returning name, description, star/pull counts, and
official/automated flags.
2. Pull¶
curl -X POST http://<host>:8000/api/staging/pull \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"image": "library/redis", "tag": "7.2"}'
skopeo downloads the image as an OCI layout under
DATA_DIR/cache/staging/<job_id>/ — no Docker daemon is involved on the
server. The source is resolved in this order:
source_registry_id— a saved external registry (its stored credentials and TLS settings are used automatically).source_registry_host(+ optionalsource_registry_username/password) — an ad-hoc registry, not saved anywhere.- Neither field set — defaults to Docker Hub.
# Pull from a saved external registry instead of Docker Hub
curl -X POST http://<host>:8000/api/staging/pull \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"image": "myorg/myimage",
"tag": "1.4.0",
"source_registry_id": "<registry_id>"
}'
Permission checks on pull¶
- Pulling from the local registry (i.e. the resolved source host is
Portalcrane's own registry) requires
can_pullon the destination folder. - Pulling from a genuinely external source (Docker Hub, a saved or
ad-hoc registry) requires the
can_pull_externalcapability on any folder the user has it on — see Folder-Based Access Control.
3. Scan¶
Once the pull completes, Trivy scans the OCI layout automatically (unless
disabled — see Vulnerability Scanning).
The job's scan_result / vuln_result fields populate with grouped
vulnerabilities and CVSS scores. A per-job override can relax or tighten
the policy for this one pull only:
{
"image": "library/redis",
"tag": "7.2",
"vuln_scan_enabled_override": true,
"vuln_severities_override": ["CRITICAL"]
}
4. Push¶
Review the scan result in the UI, then push the staged image to its final destination — the local registry or a genuinely external one:
# Push to the local registry, into the "backend" folder, under a new name/tag
curl -X POST http://<host>:8000/api/staging/push \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"job_id": "<job_id>",
"target_image": "backend/redis",
"target_tag": "7.2",
"folder": "backend"
}'
# Or push straight to an external registry instead
curl -X POST http://<host>:8000/api/staging/push \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"job_id": "<job_id>",
"external_registry_id": "<registry_id>",
"target_image": "myorg/redis",
"target_tag": "7.2"
}'
When pushing externally, the destination's username replaces the source
namespace in the image path (e.g. library/redis staged for user myorg
becomes myorg/redis), matching how most external registries scope
repository names to an account/organization.
Permission checks on push¶
- Pushing to the local registry requires
can_pushon the destination folder. - Pushing to a genuinely external destination requires
can_push_external. - Non-admin users can only push (or delete) jobs they own — job ownership is recorded at pull time.
External push can't become a backdoor to the local registry
If an "external" destination actually resolves to Portalcrane's own
registry (the hidden __local__ system entry, or an ad-hoc host that
happens to point at localhost:5000), Portalcrane enforces the
local can_push check instead of the external one — otherwise a
pull-only user could push simply by picking "External → Local
Registry" in the destination selector.
Managing jobs¶
GET /api/staging/jobs # list jobs visible to the caller (admins see all, others see their own)
GET /api/staging/jobs/{job_id} # inspect a single job
DELETE /api/staging/jobs/{job_id} # delete the job record and its OCI layout directory
Deleting a job removes its staging directory from disk. Leftover directories from crashed jobs are cleaned up separately — see System Maintenance → Orphan OCI cleanup.
Staging vs. Transfer¶
Staging is built for the "one image, reviewed by a human" workflow: search, pull, scan, decide, push. For bulk or scheduled synchronization between registries (no interactive review step), use Transfers instead — same underlying pull → scan → push pipeline, but built to move many images in one request.