Proxmox only grows disks. The restore dialog gets an optional "Root disk GiB" field for container backups; with a size, the backup is restored as a new guest with --rootfs <storage>:<N> (Proxmox's advanced restore mode). - A smaller disk is only allowed for a new VMID, checked in the dialog and again in cli.restore(): with force, Proxmox deletes the old disks before it unpacks, and a failed unpack then removes the guest. - The size must be a positive integer (0 gives an unlimited subvol on ZFS/dir storage) and above the used root disk space of a running guest. - The confirm screen is a separate DANGER screen with a red border: it says mount points are dropped and their backed-up files land in the root disk, raw lxc.* lines can come back under root@pam, root disk options are not carried over, and the used space is no fit check. - Never --unique or ignore-unpack-errors. Design: docs/plans/lxc-shrink.md (section A). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.7 KiB
Shrink a container's root disk
Problem
Proxmox only grows disks (resize_vm dies with "unable to shrink disk size"). A
container whose rootfs was sized too large cannot be made smaller from the web UI or
this TUI. Two routes exist, verified against pve-container 6.1.18 (master) source:
- API: restore a backup with an explicit
rootfs=<storage>:<GiB>("advanced restore mode", pct.adoc). Reachable through cv4pve-cli (do restore ct ... --rootfs S:Nforwards the key; checked with--dry-run). - Node shell: copy the files into a new, smaller volume and swap it in.
Both are dangerous; the TUI must say so plainly.
Approach
A. TUI: smaller rootfs on restore (container backups only)
RestoreScreengets an optional field "Root disk GiB" for lxc backups. Empty = today's behaviour, unchanged.- With a size set, only New guest is allowed. Overwrite + size is refused: with
force=1PVE destroys every old volume before it extracts, and a failed extraction then deletes the new disks and the config too; the container is gone. cli.restore()getsrootfs_gb: int | None; when set it checks it with_positive(a size of 0 makes PVE allocate an unlimited subvol on ZFS/dir) and adds--rootfs <storage>:<N>(same storage as the dialog's choice). Neverignore-unpack-errors.- The confirm screen for a shrink restore is a separate, red warning:
- every mpX of the backup is dropped from the new config; files of
backup=1mount points land inside the root disk and count against its size;backup=0mount points are not in the backup at all; - raw
lxc.*lines (includinglxc.mount.entrybind mounts) may come back when cv4pve-cli runs as root@pam: do not run both guests at once; - rootfs options (acl, quota, mountoptions, replicate) are not carried over;
- names the guest's current mount points, saying the backup may differ;
- shows rootfs used space when the guest is running (
status/currentdisk; 0 when stopped, shown as "unknown"), and says this is not a fit check: the backup may be older, mount point data and ext4 overhead (5 % reserve, inodes) come on top; - a too-small disk makes the restore task fail; the original is untouched because this is always a new VMID.
- every mpX of the backup is dropped from the new config; files of
- Hard refusal only where it is certain: size not a positive integer, or size <= used rootfs GiB of a running guest.
- Known limit, unchanged from today's restore:
run(timeout=3600)can give up while the PVE task still runs.
B. Node script scripts/pct-shrink-rootfs (bash, run as root on the node)
Usage: pct-shrink-rootfs [--yes] <vmid> <new-size-GiB>. Without --yes it only
measures and prints the plan; measuring runs pct mount/pct unmount, which sets
and clears lock: mounted (so even a dry run touches the config briefly).
Refuses (exit non-zero, nothing changed) when: not root; pct missing; CT not
stopped; config has any [...] section (snapshots, [pve:pending]); lock: set;
template: 1 or protection: 1; HA-managed (ha-manager status); a replication
job exists (pvesr list); rootfs is not a volume; any mpN or lxc.mount.entry
(rootfs-only CTs: no shadowed files, df measures rootfs alone, pct mount cannot
fail on another storage); new size >= current size; size not a positive integer.
Fit check after pct mount (df -B1 and df -i on the rootfs): used bytes * 1.1 <=
N GiB * 0.93 (5 % root reserve + ~2 % ext4 metadata), and used inodes * 1.1 <= the
new disk's inodes (N * 65536, mkfs.ext4 default ratio). On any error after
pct mount the trap runs pct unmount; if that fails it stops and prints the manual
cleanup (pct unmount, pct unlock), touching nothing else.
Steps (copy route, any storage):
pct set <id> --mp9 <storage>:<N>,mp=/.shrink-new- PVE allocates and formats; the script reads back mp9's volid andsize=.pct mount <id>;rsync -aHAX --numeric-ids --whole-file --sparse --one-file-system --exclude=/.shrink-newold rootfs -> new;pct unmount <id>. On rsync failure: unmount, then free the new volume explicitly -pct set --delete mp9only moves it tounusedN, so the script then deletes theunusedKwhose value is exactly the new volid (found by value, never by index).- Swap, under PVE's config flock (
flock /run/lock/lxc/pve-config-<id>.lock): copy the config to/root/pct-shrink-<id>-<timestamp>.conf; rewrite the main section with awk (exact string compare, no regex on volids) into<conf>.tmp.<pid>and rename it, as PVE'sfile_set_contentsdoes on pmxcfs; refuse if rootfs or mp9 changed since the copy:rootfs:gets the new volid with the old options and mp9'ssize=, themp9:line goes, andunusedK: <old volid>is added at the first free K. Nopct rescan: the script knows both volids and the size, so rescan's side effects (every orphan of the VMID added as unused,size=0on pve-container < 6.1.9) are avoided. - Print the exact
unusedKkey and value of the old rootfs: start the CT, check it, thenpct set <id> --delete unusedK. The script never deletes the old volume.
The in-place ZFS refquota shortcut is left out: one route for every storage is
less code to get wrong, and the copy keeps the original until the user deletes it.
chattr flags (+i, +a) are not copied by rsync; the script says so.
Tests: pytest runs the script with stub pct, df, rsync, flock,
ha-manager, pvesr on PATH and a temp config dir (PCT_CONF_DIR override,
default /etc/pve/lxc; the plan output prints the directory used). bats is not
installed and CI only runs pytest. ShellCheck via the edit hook.
Out of scope
- Shrinking mpX volumes, and containers that have any mount point (script).
- Overwrite-in-place restore with a smaller disk.
- Restoring back to the original VMID (second backup/restore cycle).
- Running the node script from the TUI (it needs a root shell on the node).
- Moving
backup=0mount points to the new guest.
Steps
cli.restore(rootfs_gb=...)adds--rootfs S:N; tests intest_cli.py.- RestoreScreen: size field for lxc, refuse with overwrite, refuse <= used of a
running guest; shrink confirm text with the warnings above; tests in
test_app.py. scripts/pct-shrink-rootfs+tests/test_pct_shrink.py: each refusal, dry run changes no config, happy path (exact new config,unusedKat first free K with existingunused0), dir-style volid with/and., rsync failure frees the new volume by value, unmount failure stops.- README: key table row and a warning paragraph; script usage.
- Full
uv run pytest, ruff, shellcheck green.
Open questions
- Should a shrink restore pass
--unique 1(new MACs)? Today's restore does not, it warns instead. Proposal: keep that, the warning already covers it.