The Image resource handles OCI image pulling and caching in Incus.
Images go through three stages:
default project unless overridden via --image-cache)Projects are created with features.images=true, so each project keeps its own
image store. Creating an instance copies the image from the cache into the
active project. These per-project copies are removed on down (see
Delete); the cache itself lives in a separate project and persists.
This design provides:
down/up cycles and project deletionImages report their status via Status():
| Status | Description |
|---|---|
| Unknown | Not downloaded yet |
| Cached | In cache project |
Configuration for image sources:
type ImageConfig struct {
// Source is the image server to copy the image from.
Source incusClient.ImageServer
// CacheServer is an image server to use as cache (for library users).
// Takes precedence over CacheProject.
CacheServer incusClient.InstanceServer
// CacheProject is the project name to use as cache (for CLI users).
// The project will be created if it doesn't exist.
// Ignored if CacheServer is set.
CacheProject string
// Remote is the domain part of the image reference.
Remote string
// Image is the image reference without the remote prefix.
Image string
}
--image-cache / INCUS_COMPOSE_IMAGE_CACHEdefault project// Library usage - provide your own cache server
img, _ := project.Resource(client.KindImage, "docker.io/nginx:alpine", &client.ImageConfig{
Source: imageServer,
CacheServer: myCacheServer,
})
// CLI usage - specify cache project name
img, _ := project.Resource(client.KindImage, "docker.io/nginx:alpine", &client.ImageConfig{
Source: imageServer,
CacheProject: "my-image-cache",
})
Docker-style references are parsed using github.com/distribution/reference:
// Input: "nginx:alpine"
// Parsed:
// Remote: "docker.io"
// Image: "library/nginx:alpine"
// Input: "docker.io/library/alpine:3.18"
// Parsed:
// Remote: "docker.io"
// Image: "library/alpine:3.18"
// Input: "ghcr.io/myorg/myapp:v1.0"
// Parsed:
// Remote: "ghcr.io"
// Image: "myorg/myapp:v1.0"
// Input: "alpine" (no tag)
// Parsed:
// Remote: "docker.io"
// Image: "library/alpine:latest"
Config can override parsing:
img, _ := project.Resource(client.KindImage, "custom-name", &client.ImageConfig{
Source: imageServer,
Remote: "custom.registry.io",
Image: "myimage:v2",
})
Before downloading, check if image alias exists in cache:
alias, eTag, err := config.cache.GetImageAlias(incusName)
if err == nil {
// Already cached
return nil
}
If image not found and OptionCreate() not set, returns ErrNotFound:
// Fails if image not cached
err := client.RunAction(img, client.ActionEnsure)
// Downloads if not cached
err := client.RunAction(img, client.ActionEnsure, client.OptionCreate())
Images are copied from source (registry) to cache:
copyArgs := &incusClient.ImageCopyArgs{
Aliases: []incusApi.ImageAlias{{Name: incusName}},
AutoUpdate: true,
Public: false,
Mode: "pull",
}
op, err := config.cache.CopyImage(config.Source, *imgInfo, copyArgs)
The Source field requires an ImageServer from Incus CLI config:
conf, _ := cliconfig.LoadConfig("")
imageServer, _ := conf.GetImageServer("docker.io")
img, _ := project.Resource(client.KindImage, "docker.io/nginx:alpine", &client.ImageConfig{
Source: imageServer,
})
Registries must be configured as Incus remotes:
incus remote add --protocol oci docker.io https://docker.io
incus remote add --protocol oci ghcr.io https://ghcr.io
Calling Ensure with OptionCreate() but no Source returns an error:
img, _ := project.Resource(client.KindImage, "docker.io/nginx:alpine", &client.ImageConfig{
// No Source!
})
err := client.RunAction(img, client.ActionEnsure, client.OptionCreate())
// err: "image source not configured"
Delete removes the per-project copy of the image from the active project (the
copy left behind by CreateInstanceFromImage). It is idempotent: if no copy
exists, it is a no-op. The cache lives in a separate project and is never touched
by Delete, so cached images persist across down/up cycles. Cache cleanup is a
separate concern (e.g. a future prune command).
err := client.RunAction(img, client.ActionDelete) // removes active-project copy, keeps cache
--pull)up --pull (and redeploy) force a fresh pull from the source registry before
creating instances. When the cached alias already exists, Ensure with the Pull
option deletes the stale cache entry and re-copies from the OCI source (via
skopeo copy). This is more reliable than RefreshImage: Incus fingerprints OCI
images by hashing layer digest strings, not manifest SHAs, so a registry update
that only changes manifest metadata would be invisible to RefreshImage
("already up to date") even though the tag points to a newer image. Without
--pull, an already-cached image is reused as-is.
Images with "localhost" remote (common in podman) are converted to "local":
// Input: "localhost/myimage:latest"
// Remote becomes: "local"
Images have priority 1024, placing them after profiles but before networks.
When Stack.Run processes multiple images, they download in parallel via WorkerPool.
Images are configured with AutoUpdate: true. Incus periodically checks the source registry and refreshes the cached image. Running containers are not affected; new containers use the updated image.