Everything below comes from running the feature against two real hosts, where
the assumptions that read fine in the source turned out not to hold.
The receiving side now creates the account instead of replacing one. That is
both simpler and safer: the name must be free, the account is built by the
same code `vps add` uses — same IP, port block, IPv6 assignment, firewall
rules and panel row — and the disk of the container that produced is then
replaced by the transferred one. Nothing that already exists is touched, so
every failure is undone by deleting what was created, and the host ends up
exactly as it was. It also drops the whole staging/rename dance the previous
shape needed.
What the real daemon taught us:
- A direct-stream export (Accept: application/octet-stream) refuses a backup
name: no backup object is created, so the field must not be sent at all.
- Incus validates device configuration while it CREATES the instance, so the
archive's own devices — the source host's IPv6 — are rejected before any
later fix-up can run ("not within network incusbr0 subnet"). This host's
devices therefore go in as part of the import, via X-Incus-devices. Without
them the source's disk quota comes along too: an 8 GiB account arrived on a
host that had allotted 5.
- A static IPv4 is handed out through a DHCP reservation held by the current
lease, and Incus counts a static address as taken by ANY instance that
declares it, stopped ones included. The container being replaced therefore
has to be gone before the replacement starts, or the replacement is given a
dynamic address from the pool — and then nothing can reach it, because the
panel's records and the host's DNAT rules all name the static one.
- The re-key step appends this host's IPv6 address, routed block and gateway
neighbour to the container's networkd file, checking only for its own
values. A container that arrived from another host kept that host's address
as a second global one, so the stanza is now cleared first.
- An archive carries the source's volatile.<nic>.hwaddr, so importing the same
archive twice on one host failed outright with "MAC address ... already
defined on another NIC". Per-instance volatile keys are now dropped, like
the machine-id and SSH host keys before them.
Also from the field: the re-keyed container is checked to actually hold its
assigned IPv4, the quota travels in the printed receive command (--cpu/--mem/
--disk/--bandwidth/--days), and the receiving command's <user> must be free —
receiving onto a taken name is refused before the download rather than after.
Verified on two hosts (Ubuntu 24.04 and 26.04, Incus 7.0.1, zfs): a 316 MiB
container moved and took its data (files, setuid bits, modes, a custom user,
a 200 MiB file), a fresh hostname/machine-id/SSH host key, cleared
authorized_keys, the transferred quota, its assigned static IPv4 and only this
host's IPv6; reachable over its published SSH port. The same archive imported
twice into two accounts; --optimized (zfs-native) likewise; a wrong
certificate pin refused in under a second with nothing created; a mid-import
failure rolled back to no account, no container and no files; idle exit, SIGHUP
and a killed CLI all left no archive behind.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Vpsmgr Lite
A lightweight Incus container hosting panel for small VPS instances and low-memory hosts.
Security warning: The “VMs” created by this project are LXC containers, not virtual machines. Container isolation is weaker than KVM/QEMU. A container escape or kernel vulnerability could affect the host and other tenants. Do not run untrusted or high-security workloads on a shared host.
Feature Details
- Admin panel: users, quotas, domains, IPv6 pool, SSH public keys, and audit logs; supports logging in as a user
- User panel: power control, self-service Debian 13/AlmaLinux 9/openSUSE Leap 16/Arch Linux reinstall, domain and SSH key management, encrypted sticky notes (exportable), and a time machine (checkpoint snapshots; rolling back discards the later checkpoints)
- Multi-container users: group accounts as
name+name-1,name-2, … to share one login and password across several containers, with a switcher and per-group accent color - The admin can assign each user a dedicated accent color (click the username in the user list): that user's "log in" button and user-panel background use it, so impersonating operators can tell users apart at a glance. A user group shares one color
- CPU, memory, and disk oversubscription; quota changes apply live without a container restart
- Strict container network isolation; prefix mode assigns each container a dedicated
/112IPv6 block that it can subdivide - Traffic is counted in both directions; over-quota containers are rate-limited to 1 Mbps both ways
- Compressed ZFS storage by default; the fixed instance limit is 200 containers
- Ports 80/443 forward to container ports 80/443; port 80 uses shared HTTP proxying and port 443 shared SNI proxying
Installation
Ubuntu 24.04 is recommended; Ubuntu 22+ and Debian 12+ also work. Debian requires a kernel-module build and takes longer to install.
Recommended minimum: 1 CPU core, 1 GB RAM, 15 GB of free disk space, and root access. Both amd64 and arm64 are supported; testing has focused mainly on amd64.
git clone https://github.com/bluebluesoda/vpsmgr.git
cd vpsmgr
sudo ./install.sh # download the latest prebuilt binary
# sudo ./install.sh --local-build # always build from this checkout
# sudo ./install.sh --update # update an existing install from the release
The installer first downloads the prebuilt binary from GitHub Releases. If that fails, it falls back to a local build. --local-build always rebuilds from source. If --update cannot download the release, the existing binary is left untouched.
If the host already runs other public services and you only need IPv6 inbound access, without IPv4 SSH/port forwarding, use:
sudo ./install.sh --disable-v4forward
This option asks for confirmation at the beginning. Once confirmed, it writes net.v4_forward=false, skips vpsmgr's reserved-port check, and only needs one randomly selected panel entry port. HAProxy is still installed but remains stopped. IPv4 inbound forwarding can later be restored with vps config set net.v4_forward true. The domain proxy can also be disabled on its own with vps config set net.haproxy false (renamed from net.traefik; the values are unchanged and a pre-rename config is adopted as-is).
ZFS is the default storage backend. btrfs is supported as an alternative
(on a btrfs host the pool becomes a native subvolume; elsewhere a loop file)
and dir for disposable test machines only (it provides no quotas, snapshots,
or cloning):
sudo VPSMGR_STORAGE=btrfs ./install.sh
sudo VPSMGR_STORAGE=dir ./install.sh
IPv6
IPv6 pass-through is disabled by default. Interactive installation offers three choices:
- Prefix mode: the provider routes an entire IPv6 prefix to the host
- Address-pool mode: the provider supplies separate IPv6 addresses, which an administrator adds in the admin panel
- Disabled: IPv4 only
To check IPv6 reachability before installation:
bash check-ipv6-support.sh
Usage
vps add <name> [--cpu N] [--mem NG] [--disk NG] [--bandwidth N]
vps quota <name> [--cpu N] [--mem NG] [--disk NG] [--bandwidth N]
vps list [name]
vps power <name> start|stop|restart
vps passwd <name>
vps del <name>
vps panel-url
vps admin-passwd
vps config list|set|help
The default allocation is 1 CPU core, 1 GB of RAM, and 10 GB of disk. CPU values may be whole cores or a fraction from 0.1 to 0.9; disk capacity can only be increased. Generated passwords are shown once.
Bandwidth quotas are monthly GiB totals for upload and download combined. Containers over quota are rate-limited to 1 Mbps in both directions without a restart.
Container swap is controlled by incus.swap_ratio (default 0.5 — a 1 GiB memory container may use up to 512 MiB of host swap). Setting the value applies it to all existing containers immediately (no restart); after upgrading to a version with swap support, run vps install to apply the swap allowance to containers created before it.
Users can define an init script in the panel. It runs as root inside the container after reinstall, with output written to /var/log/vpsmgr-init.log.
In the admin user list, clicking a username (a deliberately low-key entry point) opens a picker where the operator can assign that user a dedicated accent color, or clear it back to the default. The color themes that user's "log in" button and, during an operator impersonation session, their user-panel background (adapted for both light and dark mode) so the operator can tell users apart. Users logging in with their own password always see the default theme. Users cannot change the color themselves.
Additional Images
Debian 13 is the default image. To also offer AlmaLinux 9, openSUSE Leap 16, Arch Linux, or a Debian dev image during reinstall, build them once:
sudo bash scripts/60-rhel-image.sh # AlmaLinux 9
sudo bash scripts/70-opensuse-image.sh # openSUSE Leap 16
sudo bash scripts/80-debian-dev-image.sh # Debian 13 dev image (full dev toolchain)
sudo bash scripts/90-arch-image.sh # Arch Linux (rolling — rebuilds to the latest snapshot on every run)
The image builder is not run by install.sh, keeping the default installation small.
Configuration and Removal
The configuration file is /etc/vpsmgr/config.yaml. Prefer the validated CLI over editing it directly:
vps config list
vps config help
vps config set <key> <value>
sudo ./uninstall.sh # remove software, keep config, database, and containers
sudo ./uninstall.sh --purge # also remove config, database, containers, storage, and Incus
See the documentation index for technical details.

