Skip to content

Commit 9e85d6b

Browse files
committed
feat(vmm): add OCI image packaging script and registry docs
- Add scripts/dstack-image-oci.sh for pushing guest images to OCI registries - Document registry setup in guest-image-setup.md (push, VMM config, pull flow)
1 parent b17435a commit 9e85d6b

2 files changed

Lines changed: 270 additions & 0 deletions

File tree

docs/tutorials/guest-image-setup.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,59 @@ The `image_path` should point to `/var/lib/dstack/images`.
230230

231231
If VMM isn't finding the images, verify the path in the configuration matches where you installed them.
232232

233+
## OCI Registry Setup
234+
235+
Guest images can be stored in any OCI-compatible container registry (Docker Hub, GHCR, Harbor, etc.), allowing VMM to discover and pull images directly from the web UI.
236+
237+
### Pushing Images to a Registry
238+
239+
Use the `dstack-image-oci.sh` script to package and push a guest image directory:
240+
241+
```bash
242+
# Push a standard image (auto-tags: version + sha256-hash)
243+
./scripts/dstack-image-oci.sh --registry ghcr.io push /var/lib/dstack/images/dstack-0.5.8
244+
245+
# Push an nvidia variant
246+
./scripts/dstack-image-oci.sh --registry ghcr.io push /var/lib/dstack/images/dstack-nvidia-0.5.8
247+
248+
# Push with a custom tag
249+
./scripts/dstack-image-oci.sh --registry ghcr.io push /var/lib/dstack/images/dstack-0.5.8 --tag latest
250+
251+
# List tags in the registry
252+
./scripts/dstack-image-oci.sh --registry ghcr.io list
253+
```
254+
255+
The script reads `metadata.json` and `digest.txt` from the image directory and auto-generates tags:
256+
257+
| Image directory | Generated tags |
258+
|---|---|
259+
| `dstack-0.5.8` | `0.5.8`, `sha256-<hash>` |
260+
| `dstack-dev-0.5.8` | `dev-0.5.8`, `sha256-<hash>` |
261+
| `dstack-nvidia-0.5.8` | `nvidia-0.5.8`, `sha256-<hash>` |
262+
263+
Requirements: `docker` CLI (for building), `python3`, registry login (`docker login`).
264+
265+
### Configuring VMM to Use a Registry
266+
267+
Add the `[image]` section to `vmm.toml`:
268+
269+
```toml
270+
[image]
271+
# Local image directory (default: ~/.dstack-vmm/image)
272+
# path = "/var/lib/dstack/images"
273+
274+
# OCI registry for discovering and pulling images
275+
registry = "ghcr.io/your-org/guest-image"
276+
```
277+
278+
After restarting VMM, click **Images** in the web UI to browse the registry. Click **Pull** to download an image — it will be extracted to the local image directory automatically.
279+
280+
### How It Works
281+
282+
- **Push**: The script builds a `FROM scratch` Docker image containing the guest image files (kernel, initrd, rootfs, firmware, metadata) and pushes it to the registry.
283+
- **Pull**: VMM fetches the OCI manifest via the Registry HTTP API v2, downloads each layer blob, and extracts the tar contents into the local image directory. No Docker daemon required on the VMM host.
284+
- **Discovery**: VMM queries the registry's tag list API to show available versions alongside locally installed images.
285+
233286
## Managing Multiple Image Versions
234287

235288
You can have multiple image versions installed simultaneously:

scripts/dstack-image-oci.sh

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/bin/bash
2+
# SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# dstack guest image OCI packaging tool
6+
# Pack and push dstack guest OS images to an OCI-compatible container registry.
7+
set -euo pipefail
8+
9+
REGISTRY="${DSTACK_IMAGE_REGISTRY:-}"
10+
REPO="${DSTACK_IMAGE_REPO:-dstack/guest-image}"
11+
12+
usage() {
13+
cat <<EOF
14+
Usage: $0 <command> [options]
15+
16+
Commands:
17+
push <image-dir> [--tag <tag>] Pack and push image to registry
18+
list [--filter <pattern>] List available tags in registry
19+
20+
Global options:
21+
--registry <url> Registry host (env: DSTACK_IMAGE_REGISTRY)
22+
--repo <name> Repository path (default: $REPO, env: DSTACK_IMAGE_REPO)
23+
24+
Examples:
25+
$0 --registry ghcr.io push ./dstack-0.5.8
26+
$0 --registry ghcr.io push ./dstack-nvidia-0.5.8 --tag nvidia-0.5.8
27+
$0 --registry ghcr.io list
28+
$0 --registry ghcr.io list --filter nvidia
29+
EOF
30+
exit 1
31+
}
32+
33+
# Parse global options first, then dispatch to subcommand
34+
COMMAND=""
35+
ARGS=()
36+
while [ $# -gt 0 ]; do
37+
case "$1" in
38+
--registry) REGISTRY="$2"; shift 2 ;;
39+
--repo) REPO="$2"; shift 2 ;;
40+
push|list)
41+
COMMAND="$1"; shift
42+
ARGS=("$@")
43+
break
44+
;;
45+
-h|--help) usage ;;
46+
*) echo "Unknown option: $1"; usage ;;
47+
esac
48+
done
49+
50+
[ -z "$COMMAND" ] && usage
51+
[ -z "$REGISTRY" ] && { echo "Error: --registry is required (or set DSTACK_IMAGE_REGISTRY)"; exit 1; }
52+
53+
IMAGE_REF="${REGISTRY}/${REPO}"
54+
55+
# --- PUSH ---
56+
cmd_push() {
57+
local image_dir=""
58+
local extra_tag=""
59+
60+
while [ $# -gt 0 ]; do
61+
case "$1" in
62+
--tag) extra_tag="$2"; shift 2 ;;
63+
*)
64+
if [ -z "$image_dir" ]; then
65+
image_dir="$1"; shift
66+
else
67+
echo "Unexpected argument: $1"; exit 1
68+
fi
69+
;;
70+
esac
71+
done
72+
73+
[ -z "$image_dir" ] && { echo "Error: image directory required"; exit 1; }
74+
[ -d "$image_dir" ] || { echo "Error: $image_dir is not a directory"; exit 1; }
75+
76+
local metadata="$image_dir/metadata.json"
77+
[ -f "$metadata" ] || { echo "Error: metadata.json not found in $image_dir"; exit 1; }
78+
79+
# Read image info
80+
local version
81+
version=$(python3 -c "import json; print(json.load(open('$metadata'))['version'])")
82+
local digest_file="$image_dir/digest.txt"
83+
local os_image_hash=""
84+
if [ -f "$digest_file" ]; then
85+
os_image_hash=$(tr -d '\n\r' < "$digest_file")
86+
fi
87+
88+
# Detect image variant from directory name
89+
local dirname
90+
dirname=$(basename "$image_dir")
91+
local variant=""
92+
if [[ "$dirname" == *-nvidia-dev-* ]]; then
93+
variant="nvidia-dev"
94+
elif [[ "$dirname" == *-nvidia-* ]]; then
95+
variant="nvidia"
96+
elif [[ "$dirname" == *-dev-* ]]; then
97+
variant="dev"
98+
elif [[ "$dirname" == *-cloud-* ]]; then
99+
variant="cloud"
100+
fi
101+
102+
# Build tag list
103+
local tags=()
104+
if [ -n "$extra_tag" ]; then
105+
tags+=("$extra_tag")
106+
else
107+
# Auto-generate tags from variant + version
108+
if [ -n "$variant" ]; then
109+
tags+=("${variant}-${version}")
110+
else
111+
tags+=("${version}")
112+
fi
113+
if [ -n "$os_image_hash" ]; then
114+
tags+=("sha256-${os_image_hash}")
115+
fi
116+
fi
117+
118+
echo "=== Packing dstack guest image ==="
119+
echo " Source: $image_dir"
120+
echo " Version: $version"
121+
echo " Variant: ${variant:-standard}"
122+
echo " Hash: ${os_image_hash:-<none>}"
123+
echo " Tags: ${tags[*]}"
124+
echo ""
125+
126+
# Create build context in a temp directory
127+
local tmp_dir
128+
tmp_dir=$(mktemp -d)
129+
trap "rm -rf $tmp_dir" EXIT
130+
131+
# Collect all files
132+
local files=()
133+
for f in "$image_dir"/*; do
134+
[ -f "$f" ] && files+=("$(basename "$f")")
135+
done
136+
137+
# Generate Dockerfile
138+
{
139+
echo "FROM scratch"
140+
for f in "${files[@]}"; do
141+
echo "COPY $f /"
142+
done
143+
echo "LABEL org.opencontainers.image.title=\"dstack-guest-image\""
144+
echo "LABEL org.opencontainers.image.version=\"$version\""
145+
echo "LABEL wang.dstack.os-image-hash=\"${os_image_hash}\""
146+
echo "LABEL wang.dstack.variant=\"${variant:-standard}\""
147+
} > "$tmp_dir/Dockerfile"
148+
149+
# Copy files to build context
150+
for f in "${files[@]}"; do
151+
cp "$image_dir/$f" "$tmp_dir/"
152+
done
153+
154+
# Build
155+
local primary_ref="${IMAGE_REF}:${tags[0]}"
156+
echo "Building: $primary_ref"
157+
docker build -t "$primary_ref" "$tmp_dir"
158+
159+
# Tag additional tags
160+
for ((i=1; i<${#tags[@]}; i++)); do
161+
local ref="${IMAGE_REF}:${tags[$i]}"
162+
echo "Tagging: $ref"
163+
docker tag "$primary_ref" "$ref"
164+
done
165+
166+
# Push all tags
167+
for tag in "${tags[@]}"; do
168+
local ref="${IMAGE_REF}:${tag}"
169+
echo "Pushing: $ref"
170+
docker push "$ref"
171+
done
172+
173+
echo ""
174+
echo "=== Done ==="
175+
for tag in "${tags[@]}"; do
176+
echo " ${IMAGE_REF}:${tag}"
177+
done
178+
}
179+
180+
# --- LIST ---
181+
cmd_list() {
182+
local filter=""
183+
while [ $# -gt 0 ]; do
184+
case "$1" in
185+
--filter) filter="$2"; shift 2 ;;
186+
*) echo "Unexpected argument: $1"; exit 1 ;;
187+
esac
188+
done
189+
190+
echo "=== Tags for ${IMAGE_REF} ==="
191+
local tags_json
192+
tags_json=$(skopeo list-tags "docker://${IMAGE_REF}" 2>/dev/null || echo '{"Tags":[]}')
193+
194+
if [ -n "$filter" ]; then
195+
echo "$tags_json" | python3 -c "
196+
import json, sys, re
197+
data = json.load(sys.stdin)
198+
for tag in sorted(data.get('Tags', data.get('tags', []))):
199+
if re.search('$filter', tag):
200+
print(f' {tag}')
201+
"
202+
else
203+
echo "$tags_json" | python3 -c "
204+
import json, sys
205+
data = json.load(sys.stdin)
206+
for tag in sorted(data.get('Tags', data.get('tags', []))):
207+
print(f' {tag}')
208+
"
209+
fi
210+
}
211+
212+
# Dispatch
213+
case "$COMMAND" in
214+
push) cmd_push "${ARGS[@]}" ;;
215+
list) cmd_list "${ARGS[@]}" ;;
216+
*) usage ;;
217+
esac

0 commit comments

Comments
 (0)