Push your first container image¶
What you'll learn:
- How to log in to Repod's built-in OCI registry with the standard Docker CLI
- How to tag and push a small image
- How to confirm it landed, and pull it back down
Time: ~10 minutes
Prerequisites: Repod running locally with the OCI registry overlay enabled
(docker compose -f docker-compose.yaml -f docker-compose.oci.yml up -d — see
Container Registry if you haven't set this up
yet), Docker installed locally, curl, jq
Step 1 — Point Docker at Repod's registry¶
Repod's registry (Zot, under the hood) listens on its own port, separate from
the API (8000) and the APT/RPM repo server. In a default local setup that's
port 5000 (OCI_REGISTRY_PORT in your .env, mapped straight to the
depot-oci container).
docker login refuses plain HTTP by default, so for local testing (no TLS
yet), tell the Docker daemon to trust this one host:
Restart Docker after editing this file (sudo systemctl restart docker on
Linux, or restart Docker Desktop). Replace YOUR_HOST with localhost if
you're running everything on the same machine.
Going to production?
Don't ship insecure-registries to a real deployment. Add
docker-compose.tls.yml and use the dedicated :5443 TLS port instead —
see the "Deployment" section of
Container Registry.
Now log in with your normal Repod account:
Expected output:
Behind the scenes, the Docker CLI called GET /v2/token on the Repod backend,
which validated your credentials and handed back a short-lived signed bearer
token — the same protocol Docker Hub and GHCR use. Nothing to configure by
hand.
Step 2 — Build a tiny image¶
Any image works, but here's a minimal one so this tutorial doesn't depend on anything else being installed:
mkdir -p ~/hello-repod-oci && cd ~/hello-repod-oci
cat > Dockerfile << 'EOF'
FROM alpine:3.19
CMD ["echo", "Hello from Repod's container registry!"]
EOF
docker build -t hello-repod:1.0.0 .
Step 3 — Tag and push¶
docker tag hello-repod:1.0.0 YOUR_HOST:5000/hello-repod:1.0.0
docker push YOUR_HOST:5000/hello-repod:1.0.0
Expected output (digests will differ):
The push refers to repository [YOUR_HOST:5000/hello-repod]
...
1.0.0: digest: sha256:xxxxxxxx... size: 1234
The repository (hello-repod) didn't need to exist beforehand — it's created
automatically on the first successful push, the same dynamic-repository model
Repod uses for Maven, PyPI, and npm.
SaaS accounts
On Repod SaaS the registry is one shared address for every organization,
so both your login username and your image name carry your organization
slug (YOUR_ORG_SLUG+YOUR_USERNAME for login, YOUR_ORG_SLUG/hello-repod
for the image name). See the "SaaS accounts" notes in
Container Registry for the exact
syntax. On-premise/CE, use a plain username and image name as shown above.
This push doesn't get scanned immediately
Unlike a .deb/.rpm upload or an imported image (see below), the
Docker CLI talks straight to the registry — the backend is not in the
data path for a native docker push, so there's no synchronous ClamAV/CVE
step here. If your deployment has retroactive OCI scanning enabled
(settings.json["oci_retroactive_scan"], opt-in), natively-pushed images
get swept and scanned on that job's own schedule. If you want to see the
full validated pipeline (antivirus + CVE + CVE policy) run synchronously,
use the registry's Importer instead of docker push — that's exactly
what Walk through a CVE remediation
does.
Step 4 — Verify it landed¶
TOKEN=$(curl -s -X POST http://YOUR_HOST:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YourPassword1!"}' \
| jq -r .access_token)
curl -s -H "Authorization: Bearer $TOKEN" \
http://YOUR_HOST:8000/api/v1/oci/repositories/hello-repod | jq .
Expected response:
{
"name": "hello-repod",
"tag_count": 1,
"tags": [
{
"tag": "1.0.0",
"digest": "sha256:xxxxxxxx...",
"size_bytes": 3400000,
"created": "2026-08-20T10:00:00Z"
}
]
}
Or in the web UI: Container Registry, click into hello-repod.
Step 5 — Pull it back¶
Remove your local copy first so you know the pull actually came from Repod, not Docker's local cache:
docker rmi YOUR_HOST:5000/hello-repod:1.0.0 hello-repod:1.0.0
docker pull YOUR_HOST:5000/hello-repod:1.0.0
docker run --rm YOUR_HOST:5000/hello-repod:1.0.0
Expected output:
Pulling required no login: by default any repository is open to pull. See "Access control" below if you need to restrict that.
Access control (optional)¶
By default, any repository name is open to push for uploader/maintainer/admin
accounts and open to pull for anyone. To restrict hello-repod to a specific
role or group, use POST /api/v1/oci/repositories/hello-repod/access
(admin only) — see Container Registry.
Troubleshooting¶
docker login fails with 'server gave HTTP response to HTTPS client'
Docker is trying HTTPS against a plain-HTTP registry. Make sure
insecure-registries in /etc/docker/daemon.json includes your exact
host:port, and that you restarted the Docker daemon after editing it.
docker push fails with 'unauthorized: authentication required'
Your login either expired or targeted the wrong host/port — Docker
tokens issued by GET /v2/token are short-lived
(OCI_TOKEN_TTL_SECONDS, default 300s). Run docker login again right
before pushing.
Push succeeds but the repository doesn't show up in GET /oci/repositories
Repository names are RBAC-filtered per user — make sure you're querying
with the same account (or an admin account) that performed the push, and
that the account's role wasn't restricted after the fact by
distribution_access-style rules.
What you just did¶
- Configured Docker to trust Repod's registry for local testing
- Pushed an image through the standard
docker pushprotocol — no repod-specific tooling required - Confirmed the image via the API and the web UI
- Pulled it back down and ran it
Next steps¶
- Container Registry (full reference) → — access control, importing public images, SaaS namespacing
- Walk through a CVE remediation → — see the synchronous scan-and-review pipeline in action, via the registry Importer
- Security pipeline explained →