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/*.confpartition 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=, orVerity=.
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=rootis an alias for the local architecture root type (DPS). -
Weight=shares leftover free space elastically (default 1000). - Equal
SizeMinBytes=andSizeMaxBytes=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 suitableFormat=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-generatormount 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=guesssizes the partition from content (may populate twice on writable FS). -
Minimize=bestis 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 cmdlinesystemd.factory_reset=yes, or the EFIFactoryResetRequestvariable documented insystemd-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 freeshows little or no unexpected trailing free space you intended to claim. - New partitions have correct PARTLABEL / type GUIDs.
-
dfsize tracks the grown partition after growfs. - Second
systemd-repart --dry-run=yesreports 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=createuntil defs look right. -
Spare USB/VM disk with
--dry-run=yesthen--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.servicebefore filesystems that depend on new partitions are mounted (image-based distros usually wire this; classic installer roots may need explicit enablement). - Pair
GrowFileSystem=onwith growfs on first mount. - Prefer DPS types so
systemd-gpt-auto-generatorand 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.
Top comments (0)