Stop Hand-Partitioning Disks: Practical systemd-repart on Linux You ship a minimal OS image. The target disk is 64 GB, 256 GB, or 2 TB. Root is still 8 GB. Swap does not exist. /home is not a partition yet. The usual fix is a one-off parted / gdisk script, a fragile installer hook, or “remember to resize after first boot.” That does not scale across VMs, bare metal, and image-based fleets. systemd-repart turns partition layout into declarative config: GPT definitions under repart.d/*.conf, incremental grow/add on every boot, optional format/encrypt/populate while building disk images (DDIs), and no shrink/move/delete of existing data by default. This post is a practical operator guide: lab image build, first-boot grow, swap/home/srv addition, filesystem growth, verification, and rollback boundaries. What systemd-repart actually does From systemd-repart(8): Reads repart.d/*.conf partition definitions. Operates on a block device or image file (or the disk backing / / /sysroot). Adds missing partitions and grows existing ones to satisfy size/weight constraints. Is incremental and idempotent: if the table already matches config, it is a no-op. Does not shrink, delete, or reorder partitions in normal mode. By default only changes the partition table, unless you set Format=, CopyFiles=, CopyBlocks=, Encrypt=, or Verity=. Matching is by GPT type UUID (friendly names like root, home, swap, esp), not by partition number. Filenames sort the definition order. First existing partition of type T binds to the first conf of type T, and so on. GPT only. MBR is out of scope. Mental model: three jobs, one tool Job Typical invocation Notes First-boot disk takeover systemd-repart.service in initrd Grow root; create swap/home/srv on free space Offline image build systemd-repart --image=... / --empty=create Build a DDI from scratch with Format/CopyFiles Safe preview default --dry-run=yes Always dry-run before --dry-run=no Filesystem growth is a sibling concern: GrowFileSystem= GPT flag + systemd-growfs / x-systemd.growfs, not “repart magically resizes ext4 by default.” Prerequisites GPT disk (or you are creating a new GPT image). Package providing the tool (Debian/Ubuntu: systemd-repart; many images already ship it with systemd). Root for real devices; unprivileged builds often use loop files + userns. Enough free space after the last partition you care about (repart appends; it does not defragment the table). # Presence check (paths vary by distro) command -v systemd-repart man 8 systemd-repart man 5 repart.d Lab 1: Build a minimal GPT image without touching real disks This is the safest way to learn. Create a sparse file, declare ESP + root + swap, format them, and inspect the result. mkdir -p /tmp/repart-lab/{defs,out,rootfs} # Tiny fake root tree to copy into the root partition mkdir -p /tmp/repart-lab/rootfs/{etc,usr/bin,var} echo "repart-lab" > /tmp/repart-lab/rootfs/etc/hostname printf '#!/bin/sh\necho ok\n' > /tmp/repart-lab/rootfs/usr/bin/hello chmod +x /tmp/repart-lab/rootfs/usr/bin/hello Partition definitions Drop files under a dedicated definitions directory (do not write these to a production host’s /etc/repart.d yet): cat >/tmp/repart-lab/defs/00-esp.conf <<'EOF' [Partition] Type=esp Format=vfat SizeMinBytes=512M SizeMaxBytes=512M Label=ESP EOF cat >/tmp/repart-lab/defs/10-root.conf <<'EOF' [Partition] Type=root Format=ext4 # Architecture-aware: "root" means root-x86-64 on amd64, root-arm64 on aarch64, etc. CopyFiles=/tmp/repart-lab/rootfs:/ SizeMinBytes=2G Weight=1000 Label=root-a GrowFileSystem=on EOF cat >/tmp/repart-lab/defs/20-swap.conf <<'EOF' [Partition] Type=swap Format=swap SizeMinBytes=1G SizeMaxBytes=1G Label=swap Priority=100 EOF Notes from repart.d(5): Type=root is an alias for the local architecture root type (DPS). Weight= shares leftover free space elastically (default 1000). Equal SizeMinBytes= and SizeMaxBytes= fix the size (weight ignored). Priority= drops optional new partitions when the disk is too small (0 or lower never dropped; higher number = lower priority). Format= runs before the partition is registered, so you never see a half-initialized slot. CopyFiles= implies a suitable Format= if omitted (ext4 by default for non-ESP). Dry-run, then create IMG=/tmp/repart-lab/out/lab.raw # Compute minimum size and create the image file systemd-repart \ --definitions=/tmp/repart-lab/defs \ --empty=create \ --size=4G \ --dry-run=yes \ --pretty=yes \ "$IMG" # Apply for real systemd-repart \ --definitions=/tmp/repart-lab/defs \ --empty=create \ --size=4G \ --dry-run=no \ --pretty=yes \ "$IMG" Inspect: sfdisk -d "$IMG" # or parted -s "$IMG" unit MiB print free # Loop-attach and probe filesystems sudo losetup -fP --show "$IMG" # Suppose it printed /dev/loop0 lsblk -o NAME,SIZE,FSTYPE,LABEL,PARTTYPE,PARTLABEL /dev/loop0 sudo mkdir -p /mnt/repart-root sudo mount /dev/loop0p2 /mnt/repart-root # partition numbers depend on layout find /mnt/repart-root -maxdepth 3 -type f sudo umount /mnt/repart-root sudo losetup -d /dev/loop0 --empty= modes matter: Value Behavior refuse (default) Require an existing partition table allow Extend existing or create if missing require Create only if empty; refuse if a table exists force Wipe and create a fresh table (data loss) create Create a new regular file image at the path Default CLI mode is --dry-run=yes. Nothing is written until you pass --dry-run=no. Lab 2: Grow into a larger disk (the VM resize story) You deploy the same 4 G image onto a 16 G virtual disk. Goal: root grows; swap stays fixed. # Simulate "hypervisor gave us a bigger disk" truncate -s 16G "$IMG" systemd-repart \ --definitions=/tmp/repart-lab/defs \ --dry-run=yes \ --pretty=yes \ "$IMG" systemd-repart \ --definitions=/tmp/repart-lab/defs \ --dry-run=no \ --pretty=yes \ "$IMG" What should happen: Existing partitions keep their identities (type UUID match). Root grows according to Weight= / max constraints into free space. Swap stays 1 G because min=max. No partition is moved or renumbered. Partition table growth ≠ filesystem growth. After the partition is larger, grow the filesystem: sudo losetup -fP --show "$IMG" # mount the root partition, then: sudo mount /dev/loop0p2 /mnt/repart-root sudo systemd-growfs /mnt/repart-root # equivalent idea: resize2fs on ext4 while mounted (ext4 supports online grow) df -h /mnt/repart-root sudo umount /mnt/repart-root sudo losetup -d /dev/loop0 On real systems, prefer the GPT Grow-File-System flag (GrowFileSystem=on in repart.d) plus automatic systemd-growfs@.service via x-systemd.growfs in fstab, or discovery helpers that honor the DPS flags. systemd-growfs supports ext4, btrfs, xfs, and dm-crypt mappings of those. Production pattern: first-boot additions on a live host Common homelab/server goal: Keep vendor root as-is (or grow it). Add encrypted-capable state partitions on free space: swap, home, srv. Let systemd-gpt-auto-generator mount them by DPS type without hand-written fstab. Example definitions for /etc/repart.d/ (review carefully; test on a spare disk first): # /etc/repart.d/50-root.conf [Partition] Type=root # Grow existing root partition into free space; do not format existing data Weight=2000 GrowFileSystem=on # /etc/repart.d/60-swap.conf [Partition] Type=swap Format=swap SizeMinBytes=4G SizeMaxBytes=8G Weight=100 Priority=50 Label=swap # /etc/repart.d/70-home.conf [Partition] Type=home Format=ext4 SizeMinBytes=20G Weight=1000 Label=home GrowFileSystem=on # /etc/repart.d/80-srv.conf [Partition] Type=srv Format=ext4 SizeMinBytes=10G Weight=1000 Label=srv GrowFileSystem=on FactoryReset=yes Preview against the disk that backs root: sudo systemd-repart --dry-run=yes --pretty=yes # When correct: sudo systemd-repart --dry-run=no --pretty=yes With no device argument, repart targets the block device backing the root filesystem (or /sysroot in the initrd). Why Type=home / Type=srv matter The UAPI.2 Discoverable Partitions Specification assigns stable GPT type UUIDs for root, /usr, home, srv, var, tmp, swap, ESP, XBOOTLDR, and verity siblings. systemd-gpt-auto-generator(8) can then: Discover and mount home/srv/var/tmp and enable swap from GPT types. Skip mount points that already have fstab entries or non-empty directories (important operational caveat). Pair with image tools (systemd-nspawn --image=, dissect helpers) so the same GPT image boots on bare metal and as a container root. If you format a “data” partition as generic Linux without the right type UUID, auto-discovery will not place it on /home. Image factory: CopyFiles, Encrypt, Verity For offline DDI builds, repart becomes an image factory—not only a grower. Populate root from a directory tree [Partition] Type=root Format=ext4 CopyFiles=/var/tmp/my-os-tree:/ Minimize=guess Minimize=guess sizes the partition from content (may populate twice on writable FS). Minimize=best is for read-only filesystems. CopyFiles= cannot modify an existing partition; it only populates newly created+formatted ones. Symlinks/devices may be skipped on vfat ESP copies (logged). LUKS2 at creation time [Partition] Type=var Format=ext4 Encrypt=key-file SizeMinBytes=5G Label=var systemd-repart \ --definitions=... \ --key-file=/path/to/key \ --dry-run=no \ --empty=create --size=auto disk.raw Encrypt= values from the man page: off, key-file, tpm2, key-file+tpm2. Encryption is applied when the partition is created; existing partitions are left alone. dm-verity siblings (image build) # 10-root.conf [Partition] Type=root CopyFiles=/path/to/root:/ Verity=data VerityMatchKey=root # 11-root-verity.conf [Partition] Type=root-verity Verity=hash VerityMatchKey=root Verity=signature can add a signature partition when you pass --private-key= / --certificate=. This is the image-build side of the verity story; runtime open still needs your verity/UKI/cmdline story (separate from day-2 veritysetup on arbitrary devices). Factory reset (explicit, destructive, opt-in) Normal mode never deletes partitions. Factory reset is the exception: Mark partitions with FactoryReset=yes. Trigger via systemd-repart --factory-reset=yes, kernel cmdline systemd.factory_reset=yes, or the EFI FactoryResetRequest variable documented in systemd-repart(8). Marked partitions are deleted and recreated empty per definitions. systemd-repart --can-factory-reset # exit 0 if any partition is marked # Only when you truly mean it: # systemd-repart --factory-reset=yes --dry-run=no Use for lab appliances and kiosk images—not as a casual “cleanup” tool. Idempotency, seeds, and reproducibility Re-running with the same defs on an already-compliant disk does nothing. New partition UUIDs (and the disk UUID when zero) are hashed from a seed (machine-id by default, or --seed=UUID / --seed=random). Fixed seeds make image builds reproducible; random seeds make unique instances. systemd-repart --seed=00000000-0000-0000-0000-000000000001 \ --definitions=/tmp/repart-lab/defs \ --empty=create --size=4G --dry-run=no /tmp/repart-lab/out/repro.raw Verification checklist # Layout + free space sudo fdisk -l /dev/DISK sudo parted -s /dev/DISK unit MiB print free # DPS types and labels lsblk -o NAME,SIZE,FSTYPE,LABEL,PARTLABEL,PARTTYPE,UUID /dev/DISK # What gpt-auto would consider (after reboot / generator run) systemctl cat systemd-gpt-auto-generator 2>/dev/null || true find /run/systemd/generator* -iname '*home*' -o -iname '*srv*' -o -iname '*swap*' 2>/dev/null | head # Filesystem actually filled the partition findmnt -no SOURCE,FSTYPE,SIZE,AVAIL / sudo blockdev --getsize64 /dev/disk/by-partlabel/root-a Expected signals of success: parted print free shows little or no unexpected trailing free space you intended to claim. New partitions have correct PARTLABEL / type GUIDs. df size tracks the grown partition after growfs. Second systemd-repart --dry-run=yes reports no changes. Boundaries (what not to expect) Need Use instead / note Shrink or delete partitions in place Manual partitioning; factory-reset only for marked partitions Reorder partition numbers Not supported; numbers stay stable MBR disks GPT only Grow FS without partition grow resize2fs / xfs_growfs / btrfs filesystem resize alone Writable block integrity dm-integrity + integritysetup (different layer) Read-only image authentication at runtime dm-verity + veritysetup / UKI measurements Per-file authenticity on mutable FS fs-verity LVM thin provisioning / snapshots LVM thin pools Btrfs send/receive backups btrfs tooling Multipath LUNs multipathd systemd-repart owns GPT shape and optional first-time population. Integrity, confidentiality, and backup remain separate layers. Suggested roll-out Lab file image with --empty=create until defs look right. Spare USB/VM disk with --dry-run=yes then --dry-run=no. Install defs into the image under /usr/lib/repart.d/ (vendor) or /etc/repart.d/ (local policy). Ensure initrd runs systemd-repart.service before filesystems that depend on new partitions are mounted (image-based distros usually wire this; classic installer roots may need explicit enablement). Pair GrowFileSystem=on with growfs on first mount. Prefer DPS types so systemd-gpt-auto-generator and nspawn image mounts stay consistent. Document that operators must not create conflicting fstab lines for the same mount points if they want auto-discovery. Quick reference # Preview host root disk sudo systemd-repart --pretty=yes # Apply host root disk sudo systemd-repart --dry-run=no --pretty=yes # Build new image systemd-repart --definitions=./defs --empty=create --size=auto --dry-run=no ./disk.raw # Force fresh partition table on a file/device (DESTRUCTIVE) # systemd-repart --empty=force --dry-run=no /dev/DISK # Definitions search path (highest precedence first among drop-ins; see man) # /etc/repart.d/*.conf # /run/repart.d/*.conf # /usr/local/lib/repart.d/*.conf # /usr/lib/repart.d/*.conf References systemd-repart(8) — Debian manpages: systemd-repart.8 repart.d(5) — repart.d.5 systemd-growfs(8) / systemd-makefs@.service — systemd-makefs@.service.8 systemd-gpt-auto-generator(8) — generator man page UAPI.2 Discoverable Partitions Specification — uapi-group.org Related reading on this blog: dm-verity with veritysetup, dm-integrity with integritysetup, fs-verity with fsverity, LVM thin pools, systemd-nspawn images Ship a small GPT image. Let the target disk tell you how big it is. Declare the partitions you want in repart.d, dry-run until the pretty table looks boring, then apply once. That is the whole habit: layout as config, not as a remembered parted session.