Skip to content

Will my image run?

Updated

Most images that work under Docker work as an AppVM unchanged. This page is the check to run before you import one, and the explanation when something exits straight away.

  • Service images such as nginx, redis, postgres, or your own service: import and run. Remember to attach a volume for anything that must persist.
  • Base and runtime images such as alpine, debian, ubuntu, node, or python: these exit immediately. Their entrypoint is an interactive shell or REPL with nothing attached to it. Turn on keepalive if you want to stay inside and poke around, or use an image with a real entrypoint to run a service.
  • Non-root images run fine. To write to a data volume, the image must already contain that directory owned by the image’s user.
  • Distroless and other images with no shell: the workload runs. What you lose is everything that needs /bin/sh, which means no web terminal, no keepalive, no shell-form entrypoint, and no shell-form health check. Reading and writing the machine’s files, and starting a program by name, do not need a shell, and neither does a TCP or HTTP health probe.
  • An image whose entrypoint is an init, systemd or an equivalent: the machine boots it and it runs its own services, as covered in Init images and container runtimes.
  • A container runtime as the workload (containerd, podman, k3s): the guest mounts cgroup2 and offers the namespace abilities a runtime checks for before it starts. What was measured is spelled out in Init images and container runtimes.
Image Runs What to know
Alpine and derivatives Yes A bare alpine entrypoint is a shell, so it needs keepalive
Debian / Ubuntu Yes Bare images exit immediately and need keepalive; service images are fine. The image’s own /etc/hosts is empty; the guest writes one at every boot
nginx Yes Runs unchanged. Its declared stop signal is honoured, so shutdown drains gracefully
PostgreSQL Yes, with care See the note below about PGDATA
Redis Yes Attach a volume at /data for persistence. A very large save may not finish inside the stop grace period
Node / Python Yes Service images are fine; a bare node or python3 entrypoint is a REPL and exits
Go distroless Yes, with limits Runs, including a numeric user, but has no shell
Images with a health check Yes Probing and status work. Automatic recovery follows the restart policy, which starts at no
Images that fork children Yes Child processes are reaped and signals reach the group
Images that ignore SIGTERM Yes Escalates to a forced stop after the grace period
An image whose entrypoint is an init Yes systemd boots and runs its own services. See Init images and container runtimes
A container runtime as the workload Yes containerd, podman, and k3s all look for the cgroup mount the guest now provides; k3s is measured

A brand new volume takes its owner and mode from the directory the image ships at that mount point. If the image does not contain the directory, the mount point is created owned by root, and a non-root workload will not be able to write to its own volume.

Two ways out: use an image that ships the directory with the right owner, or use an image whose entrypoint starts as root and adjusts ownership itself, which is what the postgres and redis images do.

An AppVM is one image with one main process, and it is not a container engine: Virtainer Free does not schedule several containers inside one machine, and an image’s own processes share that machine’s single filesystem and network. What the guest does provide is a normal Linux kernel with the things a container runtime asks for before it starts.

The guest mounts a unified cgroup hierarchy. A runtime looks for that path before it does anything else, and an empty /sys/fs/cgroup made it quit on the spot with an error that reads like missing kernel support. The kernel has always had the ability; the mount was what was missing.

A template no longer carries the build container’s /run. Flattening an image used to pull in that directory’s leftovers, which are runtime mounts rather than image content. One of them, /run/systemd/notify, is a directory where systemd wants a socket, so an init-style entrypoint took an address-in-use error and froze on the spot: the machine showed as running, the log was one line long, and nothing was up. Image authors cannot work around it, because those paths are live mounts at build time and refuse to be deleted. The host now clears /run when it converts an image, so templates you convert from here on are clean. A template you imported or built earlier still holds what it was given: rebuild or re-import it to apply the change, then redeploy the machines that use it.

What that has been measured with, on a real host:

  • A single-node k3s reached Ready using the image’s own command, with no wrapper entrypoint, and a pod on the flannel network produced logs through kubectl. The same image previously died right after signing its certificates.
  • A four-node Kubernetes cluster built with kubeadm, on Ubuntu 24.04 images with systemd managing the kubelet and one node per AppVM, reached Ready on all four nodes. Before the /run change, systemd froze as the first step.

Two things to expect if you take this on:

  • An exec terminal starts in the machine’s root process tree. An init-style workload puts the services it manages into namespaces of their own, so talking to that init from a terminal is not always direct: on the cluster above, systemctl only answered once it was run against the init’s process through nsenter.
  • A container runtime pulls the images it needs over the network, from inside the machine, so it depends on that machine’s own network and egress rather than on what this host already imported.
  • Automatic anonymous volumes. A VOLUME declaration does not create a volume for you. Persistent data needs an explicitly attached volume. The create form does warn when the image declares a path as persistent and you have not attached anything to it.

  • Images built for a platform other than linux/amd64. Admission names the platform it found and says which one it needs.

  • Health checks declared by OCI-format images. The format does not carry the field. You can add one yourself when you create the machine.

  • Interactive stdin and TTY entrypoints. Keepalive is an escape hatch for debugging, not a way to run an interactive program as a service.

  • Several containers as several units in one machine. One image is one machine, with one main process that owns its health and its result. Sidecars you declare are extra processes inside that one environment, not separate containers with their own disk, network, or restart policy. Running a container runtime as your workload is a different thing, and it works.

    A machine takes up to eight sidecars. Each needs a name of 1 to 32 lowercase letters, digits or hyphens, starting and ending with a letter or digit, and main is reserved for the workload itself. Each declares its own command; it inherits the image’s environment, working directory and user, so a redeploy onto a newer build re-inherits those from the new image. An older guest does not understand named sidecars, and the machine does not quietly start without them: it fails to boot with FATAL [capability] guest did not acknowledge named sidecars within 10 seconds. Redeploy or reboot it to bring the guest up to date.

  • Virtual machines inside an AppVM. The guest has no /dev/kvm, which is deliberate: a container runtime inside a machine is supported, a hypervisor inside it is not.

If the image declares a stop signal, that is what gets sent, so postgres gets its fast-shutdown signal and nginx gets its graceful-drain signal. The grace period defaults to 10 seconds and can be raised to 300 when you create the machine. After it expires the workload is killed and the machine is torn down.