diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 5666c90..838279a 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -84,7 +84,7 @@ graph LR
Test["tests/test_image.cpp
tests/test_dinov2.cpp
tests/test_dinov2_c.cpp
tests/test_cli.cpp"]
Tool["scripts/bench.sh
scripts/dinov2-to-gguf.py
scripts/publish-gguf.sh"]
Build["CMakeLists.txt
CMakePresets.json
src/stb_image.h
src/stb_image_write.h
src/doctest.h"]
- Tier2["wasm/ (embind shim + demo)
tools/server/ (dinov2-server)
examples/ (dedup, ci-visual-regression)"]
+ Tier2["wasm/ (embind shim + demo)
tools/server/ (dinov2-server)
examples/ (dedup, ci-visual-regression, container)"]
CI[".github/workflows/release.yml
.github/workflows/build.yml
.github/workflows/extras.yml
.github/workflows/convert-and-publish-gguf.yml"]
Doc["README.md
CONTRIBUTING.md
docs/cli.md
docs/build.md
docs/benchmarks.md
docs/hf-publishing.md
docs/stability.md + docs/tiers.md
docs/wasm.md
ARCHITECTURE.md
CHANGELOG.md"]
Asset["assets/logo/
assets/tench.jpg
assets/logo.png"]
@@ -117,7 +117,7 @@ Abridged table:
| `CMakeLists.txt`, `CMakePresets.json`, vendored stb/doctest | Build | CMake build + vendored single-file deps. |
| `wasm/` | Tier-2 | Emscripten embind shim over the C API + browser demo (`DINOV2_BUILD_WASM`). |
| `tools/server/` | Tier-2 | `dinov2-server` HTTP embeddings service over the C API + vendored cpp-httplib (`DINOV2_BUILD_SERVER`). |
-| `examples/` | Tier-2 | Copy-paste examples: `dedup/` (near-duplicate finder), `ci-visual-regression/` (GitHub Action). |
+| `examples/` | Tier-2 | Copy-paste examples: `dedup/` (near-duplicate finder), `ci-visual-regression/` (GitHub Action), `container/` (FROM scratch demo image). |
| `.github/workflows/*` | CI | build, release, convert-and-publish, parity, bench, changelog, extras (tier-2 advisory jobs). |
| `README.md`, `CONTRIBUTING.md`, `docs/*`, `ARCHITECTURE.md`, `RELEASE_NOTES_*.md` | Doc | User + contributor-facing docs. |
| `assets/*` | Asset | Logo + default input image. |
diff --git a/examples/container/Dockerfile b/examples/container/Dockerfile
new file mode 100644
index 0000000..89c0e6b
--- /dev/null
+++ b/examples/container/Dockerfile
@@ -0,0 +1,54 @@
+# syntax=docker/dockerfile:1
+#
+# dinov2.cpp container demo: a FROM scratch image carrying a statically
+# linked dinov2-cli plus the pinned dinov2-small GGUF weight. The build
+# context only needs this directory; the source tree and the model are
+# fetched and verified inside the build stage.
+
+ARG DINOV2_REF=v0.4.0
+ARG MODEL_URL=https://huggingface.co/dinov2-cpp-core/dinov2-small-gguf/resolve/main/model.gguf
+ARG MODEL_SHA256=b7ca009aa416f6be85ea95363f4ef13d3c139999f96ca3fd47226849afb712e6
+
+FROM ubuntu:24.04 AS build
+ARG DINOV2_REF MODEL_URL MODEL_SHA256
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ build-essential ca-certificates cmake curl git \
+ && rm -rf /var/lib/apt/lists/*
+
+# GGUF weights, pinned by sha256 so a silently changed upstream file fails
+# the build instead of shipping different embeddings.
+RUN curl -fSL --retry 3 "$MODEL_URL" -o /model.gguf \
+ && echo "$MODEL_SHA256 /model.gguf" | sha256sum -c -
+
+# dinov2-cli, fully static. BUILD_SHARED_LIBS=OFF makes libdinov2 and the
+# vendored ggml build as .a archives (ggml defaults it ON on Linux), and
+# GGML_BACKEND_DL defaults OFF so the CPU backend is compiled in rather than
+# dlopen'd; -static then covers libc, libstdc++, libgomp and pthread.
+# FindOpenMP records the shared libgomp.so path, which -static cannot use,
+# so OpenMP_gomp_LIBRARY is preset to the gcc-shipped static archive.
+# GGML_NATIVE=OFF avoids baking -march=native so the image stays portable
+# across x86-64 and arm64 hosts.
+RUN git clone --depth 1 --branch "$DINOV2_REF" \
+ https://github.com/espetro/dinov2.cpp /src \
+ && (git -C /src submodule update --init --depth 1 \
+ || git -C /src submodule update --init) \
+ && cmake -S /src -B /src/build -DCMAKE_BUILD_TYPE=Release \
+ -DBUILD_SHARED_LIBS=OFF \
+ -DDINOV2_BUILD_TESTS=OFF \
+ -DGGML_NATIVE=OFF \
+ -DOpenMP_gomp_LIBRARY="$(gcc -print-file-name=libgomp.a)" \
+ -DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++" \
+ && cmake --build /src/build --target dinov2-cli -j"$(nproc)" \
+ && strip -o /dinov2-cli /src/build/bin/dinov2-cli \
+ && if readelf -d /dinov2-cli | grep -q NEEDED; then \
+ echo "error: dinov2-cli is not fully static" >&2; exit 1; \
+ fi
+
+FROM scratch
+COPY --from=build /dinov2-cli /dinov2-cli
+COPY --from=build /model.gguf /model.gguf
+ENTRYPOINT ["/dinov2-cli"]
+CMD ["--help"]
diff --git a/examples/container/README.md b/examples/container/README.md
new file mode 100644
index 0000000..1c1de07
--- /dev/null
+++ b/examples/container/README.md
@@ -0,0 +1,91 @@
+# container: vision embeddings in a tiny scratch image
+
+A `FROM scratch` image that ships a statically linked `dinov2-cli` plus the
+pinned `dinov2-small` GGUF weight. No shell, no libc, no package manager:
+the whole runtime is one binary and one model file, roughly 50 MB total.
+
+Tier-2 example: not part of the shipped binary or CI gates. Build context
+is this directory only; the Dockerfile clones the source at `DINOV2_REF`
+(default `v0.4.0`) and downloads `model.gguf` from Hugging Face with a
+pinned sha256, so the build fails loudly if either upstream changes.
+
+## Build
+
+```bash
+docker build -t dinov2 examples/container/
+```
+
+Multi-stage layout:
+
+1. `ubuntu:24.04` stage fetches `model.gguf` (sha256 verified), clones the
+ repo with the `ggml` submodule, and builds `dinov2-cli` with
+ `-DBUILD_SHARED_LIBS=OFF` and
+ `-DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++"`.
+ A `readelf` guard fails the build if the binary still has dynamic
+ dependencies.
+2. `FROM scratch` copies only `/dinov2-cli` and `/model.gguf`.
+
+`GGML_NATIVE=OFF` keeps the binary portable, so the same Dockerfile
+produces correct images on x86-64 and arm64 hosts (including emulated
+`--platform` builds).
+
+## Run
+
+```bash
+# version / help (no image needed)
+docker run --rm dinov2 --version
+docker run --rm dinov2 # prints usage via CMD ["--help"]
+
+# embed an image; mount a folder of inputs at /imgs
+docker run --rm -v "$PWD/imgs:/imgs" dinov2 \
+ -m /model.gguf -i /imgs/a.jpg --print-embeddings | jq .
+
+# every flag of dinov2-cli works the same way
+docker run --rm -v "$PWD/imgs:/imgs" dinov2 \
+ -m /model.gguf -i /imgs/a.jpg -i /imgs/b.jpg --batch 2 --print-embeddings
+```
+
+The image's `ENTRYPOINT` is `dinov2-cli`, so arguments after the image name
+go straight to the CLI. `ENTRYPOINT ["/dinov2-cli"]` means there is no
+shell inside; pass flags directly, and use `--entrypoint` only if you
+bind-mount your own binary for debugging.
+
+## Size
+
+Measured on an arm64 host (x86-64 is within a few MB):
+
+| image | size |
+| --- | --- |
+| `dinov2` (this demo, scratch, arm64 build) | 45 MB |
+| `gcr.io/distroless/cc-debian12` runtime alone | ~30 MB |
+| `pytorch/pytorch` (CUDA + PyTorch + Python) | ~7 GB |
+
+Same embedding quality as the desktop CLI; the 100x+ difference vs the
+PyTorch image is entirely the runtime: no Python, no CUDA stack, no OS
+userland. If a fully static build ever regresses (for example a future
+ggml backend that requires dlopen), the documented fallback is
+`gcr.io/distroless/cc-debian12` plus the dynamically linked release
+binary, which lands around 80 MB with the same model.
+
+## Build args
+
+| arg | default | purpose |
+| --- | --- | --- |
+| `DINOV2_REF` | `v0.4.0` | git tag/branch/commit cloned in the build stage |
+| `MODEL_URL` | HF `dinov2-small-gguf` resolve URL | where `model.gguf` is fetched |
+| `MODEL_SHA256` | pinned digest | build fails if the download does not match |
+
+```bash
+docker build -t dinov2 --build-arg DINOV2_REF=main examples/container/
+```
+
+## Notes
+
+- The model is baked in for zero config startup. For a slimmer image,
+ drop the `COPY` of `model.gguf` and mount it instead:
+ `docker run -v "$PWD/models:/models" dinov2 -m /models/model.gguf ...`.
+- Static linking covers OpenMP (`libgomp`), pthread, libc and libstdc++.
+ `ggml` links `dl` on Linux, but with `GGML_BACKEND_DL=OFF` no `dlopen`
+ call survives into the binary, so a glibc static link is safe here.
+- Networking is unnecessary at runtime; run with `--network none` for a
+ fully offline encode.