Welcome to the forum!
It depends. To counter some of the arguments brought up (or rather, to add to the discussion, as I’m not directly debating “which one is best” flame war bait questions), here are some examples of why I would use one or the other.
I’ll start with the obvious, i.e. running OCI containers in VMs. This offers the security and isolation of a VM and the stability of any direct container operations on an OS. I’d be using this if I don’t trust the workload and want to isolate it from other workloads, but without the investment of dedicated hardware for it. It’d also be a good idea if you want to secure the workload from other workloads, like say a container that runs something that’s supposed to be secret (like a VaultWarden setup for the ultra-paranoid).
But on the plus side, you get actual live migration support (if you cluster), which helps with host maintenance without downtime and if you run a small OS in the VMs (like alpine) or a very slow stable (debian), you won’t be getting lots of updates often (but it’s still something to consider).
The big disadvantage of this is scalability. You have to update the hypervisor, all the VMs and the containers (which can be automated, so it’s not all doom-and-gloom), but the more important part is the hardware requirements. Virtualization is expensive if you plan to run a bunch of workloads, especially if you’re into the low-end hardware that sips power at idle and isn’t a power-hog at full blast (generally the really low-end PC and the SBC market).
That gets us to running OCI containers on baremetal. You get none of the security (other than the built-in cgroups stuff from the kernel and whatever user permissions are handled by unix groups), but you waste no resources for virtualization. If I trust all my workloads (if I run them, that almost always means I trust them) then this is an ok setup. You update the containers as usual and the OS too, but when you update the OS, you’ll need to reboot and take downtime on your workloads.
If you use something like k8s / k3s / k0s / mikrok8s (and whatever k*s there are), then you will get a bit of downtime while the service gets started on a different host and it’s up to your software to handle the failover (i.e. something like vaultwarden will definitely be down for a couple of seconds, while something like a personal website might not even have time to give you a http 500 or site unreachable error).
But now we have a problem. If your host OS is a hypervisor, where you’re not technically supposed to mess with the main OS, then you should try your best to sandbox your workloads, to prevent the main system from real and / or potential failures (although I still encourage people to exercise that freedom, which is why I suggested in the past that people run proxmox for VMs and LXC and install portainer directly on proxmox and run containers on the host - just be mindful that this is not a supported configuration and it’s on you to troubleshoot things).
To get around that “”“problem”“” one can simply run OCI containers inside LXC. You waste a tiny amount of resources on LXC to jump-start your OCI containers, but it’s nowhere near as much as virutalization (it’s mostly a couple of MB of RAM overhead). Some advantages to this method, like Wendell mentioned, is that you can pass some file systems to LXC, then the OCI container will just overlayfs on top of that.
You still get the downsides of VMs, in that you need to update the OS inside LXC and you get none of the benefits like live-migration or security confinement. LXC and OCI containers make use of cgroups, meaning you overlay groups on top of other groups. Sometimes they don’t play along well, sometimes they work. The real advantage here is now your OS is separate from your OCI container workloads and your workload will not affect your host OS, but the OS inside LXC. You also get some portability (to transfer LXC to another host). I believe Proxmox is working on LXC live-migrations through CRIU, but it’s not yet anywhere near ready.
But not all’s bad on the lack on live-migration for both lxc and oci containers. Some people reported jellyfin in lxc to restart on another host and not notice any stream interruption, so even that small delay to restart isn’t really a problem, depending on the workload (for jellyfin, that’s likely because the stream was already buffered enough). However, keep in mind if lxc is highly available and is started on another host, then once it starts, it’ll need to start the OCI containers, meaning you have a slightly bigger delay. Probably still not noticeable, but it’s there.
The disadvantage to OCI in LXC is the complexity. A VM is arguably more complex, because you’re virtualizing a whole set of hardware and run a full OS that interacts with said virtual hardware. But from a software standpoint, that’s actually easier and less of a headache. In LXC, you are enabling nested containerization, which is somewhat of a security risk if your LXC container is untrusted. The LXC contaienr with nested option enabled has access to the host OS’ /proc and /sys file systems with read and write. The underlying OCI containers won’t have the same privilege, but this is already something to be wary of, from both security and stability standpoint. If the LXC container messes with these too much, your host OS can crash.
LXC with nesting is not inherently insecure, but it’s a less secure option than the default (non-nesting) option. Just make sure that you trust the workload in LXC (both the LXC and OCI containers). But I’d say it’s a trade-off for stability which everyone should decide on, when doing it.