Skip to content

Blog ·

VM or container? What to sell on Proxmox

A KVM guest and an LXC container share one hypervisor, one number space and one price list, and very little else. The differences that cost you money.

By Provibr

Proxmox VE runs two kinds of guest. A KVM virtual machine boots its own kernel behind emulated firmware, and an LXC container shares the kernel of the host it sits on. To a customer buying a VPS, both are "a server with root on it", which is why the comparison usually gets made in a marketing table with rows about density and overhead.

Those rows are true and they are not the ones that will cost you. What costs you is the set of places where the two look identical in the API and behave differently underneath, because that is where a panel, a billing system or an ordering flow quietly does the wrong thing and nobody notices for a month.

We recently added a second compute kind to our own Proxmox integration, which meant reading the API with the specific question "where do these two diverge". This is what came out of it, in the order it surprised us.

One cluster, one number space

Start with the identifier, because everything else hangs off it. A Proxmox guest is addressed by a number, and it is tempting to assume that virtual machines and containers each get their own range: 100 could be a VM and 100 could also be a container, distinguished by type.

They cannot. The number space is shared across the whole cluster, and three properties of the API say so. The endpoint that hands out the next free identifier takes no type parameter, so it cannot know which kind you are about to create. The cluster resource listing returns machines and containers together on the same numeric field. And the cluster's own guest list is keyed by that number.

This has a direct consequence for anything that stores Proxmox guests in its own database. Put the kind into your uniqueness constraint, so that "machine 5001" and "container 5001" are two different rows, and you have a schema that can hold two records for one guest and let them fight over it. The identifier is unique on its own, and the kind is an observation stored next to it. If you are picking ranges for a fleet, the arithmetic in choosing a VMID range on Proxmox does not change when containers arrive, and that is the point.

A container is unpacked, not cloned

The second divergence is the creation path, and it is bigger than it looks.

A virtual machine is normally produced by cloning a template: you name a source, you pick full or linked, and you get a copy of a disk image. A container is produced by unpacking an archive onto a fresh volume. There is no source guest, there is no full-versus-linked choice, and there is no cloud-init drive, because there is nothing to attach a drive to.

So the request body has almost no fields in common with the one that creates a machine. A container wants ostemplate pointing at an archive on a storage, an unprivileged flag, a hostname, a rootfs and a swap value. A machine wants scsi0, scsihw, bios, boot and an ide2 when it boots from an ISO. Reusing one vocabulary for the other is the mistake to avoid on purpose: the words are similar enough that a field called disk will mean two different things in the same codebase within a year.

Two details in that body are worth stealing regardless of what you build on.

rootfs=<storage>:<size> is an allocation, not a reference. It means "create a new volume of this many gigabytes on this storage". The form that names an existing volume looks almost identical and does something categorically different: it adopts a volume that may still hold somebody else's data.

Leaving a parameter out is the safe answer. Nesting, keyctl, FUSE, bind mounts and device passthrough are all off unless you send them, and there is no "empty" value you have to remember to write. The correct default posture for a hosting platform is therefore a short request rather than a long one. The architecture field is the same story: Proxmox derives it from the archive, and sending one that disagrees fails at unpack time.

Three things a container will never do

Some gaps are roadmap items. Three are structural, and confusing the two categories in your own product is how you end up promising a feature that cannot exist.

ActionKVM guestLXC container
Start, stop, shut down, deleteyesyes, same shape, different path segment
Pause or hibernateyesno: that is the QEMU monitor
Boot from the network with PXEyesnever
Install from an ISOyesnever
Choose a boot order, BIOS or UEFIyesnever
ConsoleVNC framebuffera terminal, over a different protocol
Swapinvisible to the hosta hypervisor setting with a number

Five of those rows need more than a cell, in three groups.

Netboot, ISO install and firmware selection are impossible for one reason, not three: a container has no firmware. It does not run a bootloader, it has no boot order, and starting it involves nothing that could be told to try the network first. If you sell a product where "reinstall from our image library" is a feature, note that the PXE path from DHCP answer to finished install has no container equivalent. Rebuilding a container is a different mechanism with different guarantees, and it deserves its own screen rather than a greyed-out button.

The console is the same kind of difference wearing a familiar name. Proxmox will give you a console for a container, but it is a terminal over a websocket, not the framebuffer of a graphics adapter, and a VNC client will not render it. One console tab now has two implementations behind it, and the honest interim answer is to say the tab is not available yet rather than open a viewer that shows a black rectangle.

Pause and hibernate deserve a footnote because they trap enforcement logic. If your platform suspends a machine when a customer goes over a limit, and "suspend" means pause, then containers fall out of enforcement entirely: the command is refused, no worker reports anything, and the customer keeps running. The boundary belongs on the mode, not on the command. Stopping a container is perfectly possible.

Swap is a product decision, not a default

Here is the one that will show up in support tickets.

Proxmox gives a new container 512 MiB of swap unless you say otherwise. Inherit that quietly and a plan sold as "2 GiB of memory" delivers 2.5 GiB of usable memory, and the difference does not appear as a bigger number anywhere: it appears as a container that gets slow instead of hitting an honest out-of-memory condition, months later, on a host that has started swapping.

We send the value explicitly on every create, with zero as our default. Zero is a choice and not an absence. Whatever you pick, pick it deliberately, write it on the plan, and make sure the number a customer sees is the number the hypervisor receives.

Measuring swap has its own price. The bulk listing that gives you CPU, memory, disk and network counters for every guest in a cluster carries no swap field at all, for either kind. The only place Proxmox reports it is the per-guest status endpoint, so it is one extra HTTP call per running container, forever. We read it once a minute: three times a minute triples the load on somebody else's hypervisor for a number that moves in minutes, and once every five minutes puts exactly one sample in a five minute rollup bucket, which makes the maximum column equal to the average and therefore decoration.

Two bugs we shipped by assuming they were the same thing

Both of these had been live for months before the round that went looking.

The first: our import screen listed containers as virtual machines. The cluster resource listing with type=vm is not a QEMU filter, it returns both kinds, and the field saying which one was simply not read. Nothing failed. The list was wrong.

The second was quieter. Every running container was getting two guest agent calls per polling round, plus one after every command, against a path that only exists for QEMU. Each was a guaranteed failure, and nothing surfaced, because a failed optional enrichment is supposed to be survivable and it was. It was found by a test that counts which paths get called rather than asserting on outcomes, which is a habit worth copying: assert only on results and an entire category of pointless work stays invisible.

What we would write down before selling either

If you are deciding what a hosting product should offer on Proxmox, four questions get you most of the way.

  1. Is the kind a property of the plan or a choice at checkout? Both are defensible. A default underneath the choice is not: a customer who ordered a container and received a machine got a different security model, a different kernel and a different set of capabilities than the one they picked.
  2. Which refusals are permanent and which are temporary? Say so in the interface, in those words. "Not possible for a container" and "not built yet" are different promises.
  3. Where does the anti-spoofing come from? The honest gap in our own work: a cloned machine gets IP filtering at delivery and a container does not yet. Proxmox has the same firewall endpoints for both, so it is a build step rather than an obstacle, but a container handed over without it can send traffic claiming to be somebody else.
  4. What does monitoring show for a container? Anything derived from a guest agent, a framebuffer or firmware is empty. Empty is fine. Empty presented as a healthy zero is not.

We have the plan field, the ordering path and the create request built and measured against the raw HTTP shape of the API. What we have not done is create a container on somebody's production hypervisor, so the honest description of our container support is "the shape is right and the round trip is untested", which is a useful thing to be able to say out loud about your own platform.

This came out of building the Proxmox integration for Provibr, where the agent runs inside the customer's own network and only ever connects outbound. The related habit of not trusting a hypervisor's success response is in why a Proxmox 200 is not a result.

Start with one host and one agent

Create an account, install the agent with a single command and connect your first Proxmox host. The trial runs for 14 days and asks for no payment details.