Stop Hand-Partitioning Disks: Practical systemd-repart on Linux
DEV Community

Stop Hand-Partitioning Disks: Practical systemd-repart on Linux

What systemd-repart actually does

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.

From systemd-repart(8):

  • Reads repart.d/*.conf partition definitions.
  • Operates on a block device or image file (or the disk backing / or /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. The 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 "

Preview:

systemd-repart \
 --definitions = /tmp/repart-lab/defs \
 --dry-run = yes \
 --pretty = yes \
 " $IMG "

Apply:

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

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

  1. Lab file image with --empty=create until defs look right.
  2. Spare USB/VM disk with --dry-run=yes then --dry-run=no.
  3. Install defs into the image under /usr/lib/repart.d/ (vendor) or /etc/repart.d/ (local policy).
  4. 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).
  5. Pair GrowFileSystem=on with growfs on first mount.
  6. Prefer DPS types so systemd-gpt-auto-generator and nspawn image mounts stay consistent.
  7. 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/re
Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.