Skip to content

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:

/etc/docker/daemon.json
{ "insecure-registries": ["YOUR_HOST:5000"] }

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:

docker login YOUR_HOST:5000 -u admin -p YourPassword1!

Expected output:

Login Succeeded

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:

Dockerfile
FROM alpine:3.19
CMD ["echo", "Hello from Repod's container registry!"]
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:

Hello from Repod's container registry!

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 push protocol — no repod-specific tooling required
  • Confirmed the image via the API and the web UI
  • Pulled it back down and ran it

Next steps