OCI image packages
Starting with Fission v1.26.0, a package’s deployment archive can be an OCI image instead of a zip archive. You build an image whose filesystem contains your function code, push it to any OCI registry, and reference it when creating the package:
$ fission package create --name hello --env python \
--oci registry.example.com/myteam/hello-code:v1
Everything else — environments, functions, triggers, entry points — works exactly as with archive-based packages.
flowchart TB ci["You / CI<br/>(--oci)"]:::user -->|"push code image"| reg["OCI Registry"]:::store build["Fission Builder"]:::fission -->|"publish on build"| reg reg -->|"pull at cold start"| pod["Function Pod"]:::pod classDef user fill:#ffffff,stroke:#94a3b8,color:#1f2a43 classDef fission fill:#e8f0fe,stroke:#2d70de,color:#1f2a43 classDef pod fill:#e6f7f1,stroke:#11a37f,color:#1f2a43,stroke-dasharray:5 3 classDef store fill:#fff7e0,stroke:#dba514,color:#1f2a43,stroke-dasharray:5 3
Why deliver code as an image?
- Faster, cache-friendly cold starts: nodes and registries cache image layers, so repeated fetches of the same code are cheap, and there is no zip download + extract step from Fission’s internal storage.
- Standard supply-chain tooling: code images can be signed (
cosign), scanned, replicated, and promoted with the same tooling you already use for runtime images. - Registry-native workflows: CI pipelines that already push images need no extra upload step to Fission’s storage service.
OCI delivery is opt-in: it applies per package via --oci, or cluster-wide for every build once you configure a package registry.
With neither configured, archive-based packages remain the default and are unaffected.
Building a compatible code image
The image’s filesystem must contain exactly what an extracted deployment archive would contain — your code files at the image root (or under a sub-path, see below). The environment’s runtime still comes from the environment image; the code image carries only your code.
For a Python function with a main entry point in hello.py:
# hello.py
def main():
return "Hello, world!\n"
# Dockerfile
FROM scratch
COPY hello.py /
$ docker build -t registry.example.com/myteam/hello-code:v1 .
$ docker push registry.example.com/myteam/hello-code:v1
Multi-file packages work the same way — copy the whole directory:
FROM scratch
COPY src/ /
You can also build code images without a Docker daemon using crane:
$ tar -cf code.tar hello.py
$ crane append --base scratch --new_layer code.tar \
--new_tag registry.example.com/myteam/hello-code:v1
Base image recommendations
- Use
FROM scratch. The code image is never executed as a container — Fission only reads its filesystem — so it needs no shell, libc, or OS layer. A scratch-based code image is a few kilobytes, pulls fast, and has no CVE surface. - Do not base the code image on the environment/runtime image. The runtime is supplied by the environment; duplicating it in the code image wastes pull time and storage and changes nothing at runtime.
- If your build pipeline cannot produce
FROM scratchimages, any minimal base works — Fission extracts the merged filesystem, so whatever the image contains beyond your code is extracted too. Keep it small and put code under a dedicated directory combined withsubPath(below) so OS files are excluded.
Creating packages and functions
Create the package and reference it from a function as usual:
$ fission package create --name hello --env python \
--oci registry.example.com/myteam/hello-code:v1
$ fission fn create --name hello --pkg hello --entrypoint hello.main
$ fission route create --name hello --function hello --url /hello --method GET
$ curl http://$FISSION_ROUTER/hello
Hello, world!
fission fn create --oci <ref> is a shortcut that creates the package and the function in one step, and fission package update --name hello --oci <ref:v2> switches a package to a new image (followed by fn update to roll running functions, exactly like archive updates).
The package spec exposes a few more fields than the CLI flag; use spec files for these:
apiVersion: fission.io/v1
kind: Package
metadata:
name: hello
spec:
environment:
name: python
deployment:
type: oci
oci:
image: registry.example.com/myteam/hello-code:v1
# Optional: pin the exact content; the pull fails on any mismatch.
digest: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
# Optional: code lives under /app inside the image (must be a directory).
subPath: app
# Optional: registry credentials, resolved in the function namespace.
imagePullSecrets:
- name: regcred
Pin digests in production.
A package references an image; re-pushing the same tag with different content is not detected automatically (functions roll only on package update).
Setting digest makes the reference immutable and the pull verifiable.Automatic OCI delivery for built packages
The --oci flow above is per package — you build and push the image yourself.
You can also have Fission do this for every build cluster-wide: configure a package registry and each successful build publishes its deployment archive as a digest-pinned OCI image, and functions cold-start by pulling it instead of downloading a tarball from the storage service.
Enable it with Helm:
packageRegistry:
enabled: true
# Repository root. Packages are pushed to <repositoryPrefix>/<namespace>/<package>.
repositoryPrefix: ghcr.io/myorg/fission-packages
# Secret names (kubernetes.io/dockerconfigjson) in the builder namespace.
# pushSecret needs write access (build time); pullSecret needs read access
# (attached to packages for the pull). Keep them separate.
pushSecret: registry-push
pullSecret: registry-pull
# On a failed push: true keeps the build and stores a tarball instead
# (the package gets condition OCIPublished=False); false fails the build.
fallbackToStorage: true
| Helm value | Default | Meaning |
|---|---|---|
packageRegistry.enabled | false | Publish built deployment archives as OCI images. When unset, builds use the storage-service tarball path unchanged. |
packageRegistry.repositoryPrefix | "" | Repository root; images are pushed to <repositoryPrefix>/<namespace>/<package>. |
packageRegistry.pushSecret | "" | dockerconfigjson secret (in the builder namespace) with write access, used at build time. |
packageRegistry.pullSecret | "" | dockerconfigjson secret with read access, stamped into each package’s imagePullSecrets. |
packageRegistry.fallbackToStorage | true | On push failure, keep the build and fall back to a tarball (package gets OCIPublished=False); false fails the build. |
packageRegistry.publishedPrefix | "" | Advanced: node-visible prefix recorded in packages when nodes reach the registry at a different address than pods do (e.g. an in-cluster registry via NodePort). |
packageRegistry.insecureHosts | "" | Comma-separated host[:port] list allowed to use plain HTTP instead of TLS. |
To keep a single package on the tarball path regardless of the cluster setting, annotate it:
$ kubectl annotate package <name> -n <ns> fission.io/package-delivery=tarball
Private registries
Code-image pulls resolve credentials in this order:
- The
fission-fetcherservice account’simagePullSecrets— the cluster-wide default for all packages. - The package’s own
imagePullSecrets— per-package credentials, set in the package spec. - Anonymous access.
Both secret sources are resolved in the namespace the function pods run in.
For functions in the default namespace that is the configured function namespace (fission-function in many installs); for functions in other namespaces it is the function’s own namespace.
Confirm with kubectl get pods -l environmentName=<env> -A if unsure.
Step 1 — create a registry secret
Create a standard docker-registry secret in the function-pod namespace (see the Kubernetes guide for registry-specific details):
$ kubectl create secret docker-registry regcred \
--namespace fission-function \
--docker-server=registry.example.com \
--docker-username=ci-bot \
--docker-password="$REGISTRY_TOKEN"
Step 2a — cluster-wide: attach the secret to the fetcher service account
Patch the fission-fetcher service account in the same namespace; every OCI package pull then uses it without any per-package configuration:
$ kubectl patch serviceaccount fission-fetcher \
--namespace fission-function \
-p '{"imagePullSecrets": [{"name": "regcred"}]}'
Step 2b — per-package: reference the secret in the package spec
For credentials scoped to one package (e.g. different teams pulling from different registries), set imagePullSecrets in the package spec instead:
apiVersion: fission.io/v1
kind: Package
metadata:
name: hello
spec:
environment:
name: python
deployment:
type: oci
oci:
image: registry.example.com/myteam/hello-code:v1
imagePullSecrets:
- name: regcred
Verifying
Create a function on the package and invoke it; on a credential problem the function returns a 5xx and the fetcher log names the registry error:
$ kubectl logs <function-pod> -c fetcher -n fission-function | grep -i "error extracting OCI image"
Fission does not validate that the referenced secrets exist or hold working credentials — a missing or wrong secret surfaces only at pull time. With image volumes enabled, the kubelet performs the pull using the same two secret sources (the pod inherits both), so the same setup keeps working — but pull errors then appear as pod events (kubectl describe pod,ErrImagePull) rather than fetcher logs.
Runtime/environment images are pulled by the kubelet independently of package images; for those, see Pull an Image From a Private Registry.
Insecure (plain-HTTP) registries
Plain-HTTP registries are refused by default. To allow specific hosts (e.g. an in-cluster registry for development), set the Helm value:
fetcher:
allowInsecureRegistries: "registry.dev.svc.cluster.local:5000"
This is a comma-separated host allowlist, not a global switch — every other registry still requires TLS. Localhost and private (RFC-1918) IP addresses are implicitly trusted by the underlying client, matching Docker’s behavior.
Optional: mount code with Kubernetes image volumes
By default the per-pod fetcher pulls and extracts the image (this works on every supported Kubernetes version). On Kubernetes 1.33+ you can instead let the kubelet mount the code image directly into function pods as an image volume, removing the fetch-and-extract step from the cold-start path entirely:
executor:
enableOCIImageVolume: true
On clusters below 1.33 the setting is detected as unsupported and packages silently stay on the fetcher path. Be aware of the behavioral differences when image volumes are active:
- The kubelet pulls the image, not Fission.
Image references resolve with the node’s DNS and containerd’s registry configuration — a registry reachable only through cluster DNS (a ClusterIP
Servicename) will not resolve. Use a registry address that nodes can reach. - Poolmgr functions that reference Secrets or ConfigMaps, and functions on v1 environments, automatically fall back to the fetcher path (those features need the fetcher inside the pod).
- The code mount is read-only. Runtimes that write next to the code (Python bytecode caches, JVM work files) should write elsewhere; the standard Fission environments handle this.
subPathmust point to a directory inside the image (kubelets reject file sub-paths).- The
digestpin is enforced by the kubelet through the volume’s image reference.
Limitations
- OCI delivery applies to deployment archives only; source packages (built by a builder on the cluster) keep using archives.
- The container executor is unrelated to OCI packages — it already runs your full container image and ignores packages entirely.
--ocicannot be combined with--code,--src, or--deploy; a package has exactly one code source.