Push and pull your first blob¶
In this tutorial, we will push a blob to a container registry running on your machine, confirm the registry has it, and pull it back with digest verification. By the end you will have used the library's three core operations from a small Go program you wrote yourself.
Prerequisites¶
No registry account is needed. Everything runs locally and is deleted at the end.
Step 1: Start a local registry¶
We need a registry to talk to. Run one in Docker:
Confirm it answers:
You should see:
That empty JSON object is the registry saying it speaks the OCI distribution API.
Step 2: Create a Go program¶
Create a new directory and module:
mkdir blob-tutorial && cd blob-tutorial
go mod init blobtutorial
go get github.com/imgoci/go-oci-blob
Create main.go with exactly this content:
package main
import (
"bytes"
"context"
"fmt"
"io"
"log"
blob "github.com/imgoci/go-oci-blob"
"github.com/opencontainers/go-digest"
)
func main() {
ctx := context.Background()
client := blob.New(blob.WithPlainHTTP(true))
repo := blob.Repository{Host: "localhost:5001", Name: "tutorial/hello"}
data := []byte("Hello, OCI!\n")
dgst := digest.FromBytes(data)
fmt.Println("digest:", dgst)
// Push the blob.
err := client.Push(ctx, repo, dgst, int64(len(data)), bytes.NewReader(data))
if err != nil {
log.Fatal(err)
}
fmt.Println("pushed", len(data), "bytes")
// Ask the registry whether it has the blob now.
ok, err := client.Exists(ctx, repo, dgst)
if err != nil {
log.Fatal(err)
}
fmt.Println("exists:", ok)
// Pull it back. The reader verifies the digest as bytes flow.
rc, err := client.Pull(ctx, repo, dgst)
if err != nil {
log.Fatal(err)
}
defer rc.Close()
pulled, err := io.ReadAll(rc)
if err != nil {
log.Fatal(err)
}
fmt.Printf("pulled: %q\n", pulled)
}
Step 3: Run it¶
You should see:
digest: sha256:9e9181d7aef394d410d7442402afd34d6865e4b7ca1825b9f3d442b3ec0df766
pushed 12 bytes
exists: true
pulled: "Hello, OCI!\n"
Three things happened against a real registry: Push uploaded the bytes under
their SHA-256 digest, Exists confirmed the registry stored them, and Pull
streamed them back.
Step 4: Ask for a blob the registry does not have¶
Every blob is addressed by the digest of its content. Let's see what happens when we ask for one the registry never stored.
In main.go, find the Pull call:
and replace its digest argument with the digest of different content — the empty blob:
Run it again:
You should see the program fail:
digest: sha256:9e9181d7aef394d410d7442402afd34d6865e4b7ca1825b9f3d442b3ec0df766
pushed 12 bytes
exists: true
2026/08/12 20:13:11 pulling blob sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 from localhost:5001/tutorial/hello: registry returned 404: BLOB_UNKNOWN: blob unknown to registry
The registry never stored a blob under that digest, so Pull failed with an
error matching blob.ErrNotFound before any bytes flowed. (The timestamp
comes from log.Fatal in our program; yours will differ.)
Restore the original line before moving on.
Step 5: Clean up¶
Remove the registry container:
What we learned¶
blob.Newbuilds a client; options such asWithPlainHTTPconfigure it.- A
blob.Repositoryis a host plus a repository name. Pushuploads bytes under their digest;ExistsandPullfind them again.Pullverifies content against the digest you asked for; a digest the registry does not have isErrNotFound.
Next steps¶
- How to authenticate to a registry — real registries need credentials.
- Registry compatibility — what nine real registries were verified to support.
- Design — why the API looks the way it does.