While common OCI container runtimes have a much larger ecosystem, environments already using incus for system containers or VMs may prefer to use it as a unified tool for all container workloads.
About OCI support in incus
Incus can pull OCI images and run them as containers just fine, but it does differ from fully compliant OCI runtimes. The differences do not matter for most use cases, but you should be aware of them before choosing incus over docker or podman.
Incus does not preserve container image layers after importing images, instead squashing them into a single directory tree and storing that in the incus image format. It can talk to OCI image registries and pull their images, but is unable to use layer caching, meaning it cannot skip downloading shared layers across images. The conversion to incus format also means there is no way to turn the incus container back into an OCI image. There is no feature equivalent to docker commit and uploading altered OCI containers as images to OCI registries is not supported.
Incus provides only minimal image pulling and container runtimes, omitting anything not strictly necessary to get an OCI container up and running. If you want to build container images from Containerfile/Dockerfile definitions, you will have to reach for tools like buildah, and rely on skopeo for advanced OCI registry interactions.
Container logs are kept in a memory buffer and not written to disk by incus in any way. As a consequence, you can only see a limited amount of most recent messages, and all output is lost when the host restarts.
Lastly, incus structures all instances in projects, a group with optionally limited access to hardware resources, storage and networks. You can choose to ignore them for the most part and use the implicit default project.
Adding OCI registries
Incus has builtin support for standard OCI image registries - as long as you only want to pull images. Since there is no default registry for OCI containers, you should add the public docker hub:
incus remote add docker https://docker.io --protocol=ociThis adds the public docker hub registry under the name docker.
If you need access to images only available when authenticated, you can create a PAT (personal access token) on docker hub and provide that when adding the registry:
incus remote add docker-authed https://docker.io \
--protocol=oci \
--token 'your_pat_here'Note that the --token flag is unreliable, because it uses an OAuth2 token refresh API endpoint that is not part of the original container registry standard.
If you are self-hosting a container registry like the ones built into gitea or gitlab, chances are the --token authentication will not work for you.
In that case, you can always fall back to the basicauth mechanism actually mandated by the registry standard, which is supported everywhere:
incus remote add my-gitea \
https://username:pat_token_here@my-gitea.com \
--protocol=ociEven though basicauth works for every registry, you should prefer --token where possible, since the basicauth credentials will be visible when listing available registries with incus remote ls, whereas --token remains hidden from such commands.
Be aware that the username:PAT@ addition before the registry URL depends on the registry you are using. For harbor, it is your username and password, whereas gitea will ignore the username and expect a PAT as the password instead. Other systems may require the PAT name or a static string like "token" in place of the username. Check the documentation of your specific registry, implementations vary a lot.
Basic container operation
Assuming you have set up the public docker registry from the previous step, you can now launch containers from it:
incus launch docker:nginx my-http-serverThis starts a new container my-http-server from the nginx image.
You can also start a temporary interactive container instead (similar to docker run --rm -it bash:
incus launch docker:bash --ephemeral --consoleAfter exiting the interactive container, it is automatically removed.
To view the logs of an existing container, add the --show-log flag to the incus console command:
incus console --show-log my-http-serverNote that logs are only stored in a limited memory buffer, losing older messages as output grows, or output on host restarts.
Running a command in an existing container is done using incus exec:
incus exec my-http-server -- bash**Note the -- separating the incus command and possible flags from the command you want to run inside the my-http-server container.
Finally, stop and remove the container:
incus stop my-http-server
incus rm my-http-serverIf you want to stop and remove a container at the same time, you can force remove it to save time:
incus rm -f my-http-serverThis may exit the container uncleanly, but since you want to remove it anyway, that's likely not a problem.
Viewing container state
You can view all running instances:
incus lsThe output will list incus instances regardless of type, including system containers and virtual machines:
+-----------------+---------+---------------------+------------------------------------------------+-----------------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+-----------------+---------+---------------------+------------------------------------------------+-----------------+-----------+
| my-http-server | RUNNING | 10.98.76.205 (eth0) | fd42:aee8:b3c5:f567:1266:6aff:fe2b:c7db (eth0) | CONTAINER (APP) | 0 |
+-----------------+---------+---------------------+------------------------------------------------+-----------------+-----------+
| opensuse | STOPPED | | | CONTAINER | 0 |
+-----------------+---------+---------------------+------------------------------------------------+-----------------+-----------+
| rockylinux | STOPPED | | | CONTAINER | 0 |
+-----------------+---------+---------------------+------------------------------------------------+-----------------+-----------+To get only OCI containers, you have to filter the output:
incus ls | grep 'CONTAINER (APP)'Be aware that you will lose the table column header names in this case:
| my-http-server | RUNNING | 10.98.76.205 (eth0) | fd42:aee8:b3c5:f567:1266:6aff:fe2b:c7db (eth0) | CONTAINER (APP) | 0 |For a live view of resource consumption:
incus topOutput:
+----------------+-------------+----------+------+
| INSTANCE NAME | CPU TIME(s) | MEMORY | DISK |
+----------------+-------------+----------+------+
| my-http-server | 0.04 | 10.51MiB | |
+----------------+-------------+----------+------+
Press 'd' + ENTER to change delay
Press 's' + ENTER to change sorting method
Press CTRL-C to exit
Delay: 10s
Sorting Method: AlphabeticalDetailed information about a container's configuration and state
incus info my-http-serverOutput may look similar to:
Name: my-http-server
Description:
Status: RUNNING
Type: container (application)
Architecture: x86_64
PID: 499737
Created: 2026/08/19 08:06 CEST
Last Used: 2026/08/19 08:06 CEST
Started: 2026/08/19 08:06 CEST
Resources:
Processes: 14
Disk usage:
root: 584.00KiB
CPU usage:
CPU usage (in seconds): 0
Memory usage:
Memory (current): 26.32MiB
Network usage:
eth0:
Type: broadcast
State: UP
Host interface: veth9fce21b4
MAC address: 10:66:6a:2b:c7:db
MTU: 1500
Bytes received: 3.88kB
Bytes sent: 1.98kB
Packets received: 31
Packets sent: 17
IP addresses:
inet: 10.98.76.205/24 (global)
inet6: fd42:aee8:b3c5:f567:1266:6aff:fe2b:c7db/64 (global)
inet6: fe80::1266:6aff:fe2b:c7db/64 (link)
lo:
Type: loopback
State: UP
MTU: 65536
Bytes received: 0B
Bytes sent: 0B
Packets received: 0
Packets sent: 0
IP addresses:
inet: 127.0.0.1/8 (local)
inet6: ::1/128 (local)This is similar to docker inspect, although a lot shorter and notably missing event information like OOM kill and restart counters.
Configuring containers
Most configuration is abstracted into a simple key=value format in incus, including environment variables, resource limits and OCI specific settings.
You can either set them when launching an instance:
incus launch docker:sample-image -c environment.MY_VAR=testor add them to an existing container:
incus config set my-container environment.FOO=barNote that changing environment variables may require a container restart to take effect.
Limits work the exact same way:
incus config set my-container \
limits.cpu=4 \
limits.memory=2GiBThe limits category contains a lot more options like disk iops or cpu scheduling quotas. Note that limiting container/volume storage is treated as a device property, not instance configuration.
There are also a few config parameters specific to OCI containers:
incus config set my-container \
oci.entrypoint=/myapp/run.sh \
oci.cmd=run.sh \
oci.cwd=/myapp \
oci.uid=0 \
oci.gid=0Check the incus instance configuration documentation for a full list of available options.
Accessing files
Copying files between containers and the host is supported out of the box in three different ways. The easiest is to simply mount an entire container directory onto the host:
incus file mount my-container/path/in/container /path/on/hostOr alternatively edit a single file in the container:
incus file edit my-container/path/in/containerThe launched editor is chosen by system defaults and can be overridden with the EDITOR environment variable.
To instead copy files between them without mounting:
# pull from container to host
incus file pull -pr my-container/path/in/container /path/on/host
# push from host into container
incus file push -pr my-container/path/in/container /path/on/hostNote the addition of -p to automatically create missing directories and -r to support copying directories recursively.
Lastly, incus provides more fine-grained container filesystem access using file create and file remove, but you very rarely need them.
Adjusting devices
Network interfaces, port forwarding and storage are all treated as virtual devices. This may be unfamiliar to you if you are coming from docker, since incus treats container root filesystems, storage volumes and host bind mounts all as disk devices.
Device configuration can get fairly complex, so we only go over the most common use cases for OCI containers here.
Limit container filesystem size:
incus config device set my-container root size=10GiBBind-mount directory from host into container, optionally readonly:
incus config device add my-container my-bind-mount disk \
path=/path/in/container \
source=/path/on/host \
readonly=trueCreate and attach a storage volume to a container:
incus storage volume create default my-volume size=1GiB
incus config device add my-container my-volume-mount disk \
path=/path/in/container \
pool=default \
source=my-volumePort-forward an HTTP application from container port :80 to host port :8080:
incus config device add my-container http proxy \
listen=tcp:0.0.0.0:8080 \
connect=tcp:127.0.0.1:80All of these commands create and attach devices to the container. You can view the entire configuration with:
incus config show my-container --expandedMake sure to include the --expanded flag to get complete device information.
To unmount a volume or remove a port forward, simply remove the associated device:
incus config device remove my-container my-device-nameCheck the device documentation for a more complete guide on incus device types and options.
Compose projects
If you are already familiar with the docker-compose syntax and ecosystem and don't want to learn about incus-specific instance handling, incus-compose may be all you need.
The incus-compose project is designed as a drop-in replacement for docker-compose, reusing the same familiar commands and syntax.
Note that incus-compose comes with several requirements. It only runs on incus >=7, requires docker or podman to build images and needs the incus HTTP API to be exposed on port 8443.
The last part can be done with a single command:
incus config set core.https_address=:8443While incus-compose is considered stable and ready for production use, its dependencies make it cumbersome to work with in current operating systems. This is only compounded by the fact that neither incus 7 nor incus-compose are available from the default apt repository for debian 13. You should probably stick to docker-compose or podman-compose for now.