A fresh Omarchy installation can have hundreds of gigabytes free and still produce confusing storage reports after a few container-heavy sessions. du might report 250 GiB under /home while df says the filesystem uses nearly 400 GiB.
That does not necessarily mean Snapper has hidden another 150 GiB. Btrfs subvolumes, hibernation swap, Podman storage, snapshots, compression, and open deleted files all participate in different accounting systems.
The problem surfaced on my main workstation, where I create and destroy containers frequently as isolated sandboxes for AI coding agents. This guide records the storage boundaries, Snapper retention policy, and cleanup routines I want in place from the start of every fresh Omarchy installation.
Repeated emergency cleanup does not solve the underlying problem. A durable fix places each kind of data in a subvolume whose snapshot and cleanup policy matches how long that data should live.
Fresh-install checklist
Validation status: I have not yet run this checklist end to end on a clean Omarchy installation. Inspect your existing layout before making changes, and treat the paths and retention values as starting points rather than universal defaults.
Use this checklist only when the target directories do not exist. Some Podman commands, including podman info, may initialize rootless storage. Create the container subvolume before running them. If ~/.local/share/containers already exists, do not remove it; use the rootless migration procedure instead.
-
Inspect the installed Btrfs and Snapper layout without starting Podman:
Shell command findmnt -t btrfssudo btrfs subvolume list -t /sudo btrfs filesystem usage -T /sudo snapper list-configssudo snapper -c root get-config -
If you intend to use Podman's default rootless storage location, create its subvolume:
Shell command mkdir -p "$HOME/.local/share"sudo btrfs subvolume create "$HOME/.local/share/containers" &&sudo chown --reference="$HOME" "$HOME/.local/share/containers" -
If your workflow generates disposable AI sandboxes under
~/.local/share/ai-sandboxes, create that subvolume too:Shell command mkdir -p "$HOME/.local/share"sudo btrfs subvolume create "$HOME/.local/share/ai-sandboxes" &&sudo chown --reference="$HOME" "$HOME/.local/share/ai-sandboxes" -
Apply the bounded retention values from Give Snapper a bounded budget to
/etc/snapper/configs/root. Ifsnapper -c root get-configshowsQGROUPwithout a value, configure it:Shell command sudo snapper -c root setup-quota -
Enable snapshot creation and cleanup, then confirm that both timers are scheduled:
Shell command sudo systemctl enable --now \snapper-timeline.timer \snapper-cleanup.timersystemctl list-timers 'snapper-*' -
Verify the container boundary and current filesystem state:
Shell command sudo btrfs subvolume show "$HOME/.local/share/containers"sudo btrfs filesystem usage -T /sudo snapper -c root list
This checklist does not migrate existing Podman storage, prune Podman resources, or modify the hibernation swap file. The detailed sections cover rootless migration and cleanup; rootful migration remains outside the scope of this guide, and the swap section explains what not to change.
Inspect and plan
Inspect the installed layout first
Do not assume every Omarchy installation has the same storage layout. Inspect it:
findmnt -t btrfs
sudo btrfs subvolume list -t /
sudo btrfs filesystem usage -T /
sudo snapper list-configs
A typical layout might include:
@
@home
@log
@pkg
@/swap
@/.snapshots
These subvolumes share one Btrfs storage pool. Consequently, this command:
df -h /home
reports usage for the entire filesystem, not just @home.
This command answers a narrower question:
sudo du -xsh /home
It walks files visible inside the /home mount. The -x option also prevents du from crossing into other filesystems and may skip nested Btrfs subvolumes because they have distinct device IDs.
Use both tools, but do not expect their totals to answer the same question.
Classify data by lifecycle
A container-heavy workstation benefits from separating four lifecycle categories:
| Data | Examples | Desired lifecycle |
|---|---|---|
| Durable user data | Source code, documents, reusable configuration | Backed up and optionally snapshotted |
| Regenerable state | Container layers, build caches, generated sandboxes | Pruned or recreated |
| System state | Packages, /etc, service configuration |
Snapshotted for rollback |
| Hibernation state | Btrfs swap file | Persistent, but not backed up or snapshotted |
The important rule is simple:
Do not place high-churn, regenerable container data inside the same snapshot boundary as durable system or user data.
Btrfs snapshots are not recursive across nested subvolumes, as described in the Btrfs subvolume documentation . This allows subvolumes to isolate data with different lifecycles.
Separate high-churn storage
Separate rootless container storage
Rootless Podman normally stores images, writable layers, and container metadata beneath the user's home directory. Podman calls this location its graph root. Confirm the path and view a concise storage summary:
podman info --format 'graphRoot={{.Store.GraphRoot}}'
podman system df
Run podman system df -v only when you need per-image and per-container details. Its container table prints the stored command in full, so a container launched with a multiline shell script can produce a long report.
podman info may initialize the storage directory. If ~/.local/share/containers exists after inspection, even on a new machine, skip the creation step and use the migration procedure below. If the path does not exist, create it as a dedicated subvolume before running another Podman command:
mkdir -p "$HOME/.local/share"
sudo btrfs subvolume create "$HOME/.local/share/containers" &&
sudo chown --reference="$HOME" "$HOME/.local/share/containers"
Verify it:
sudo btrfs subvolume show "$HOME/.local/share/containers"
btrfs subvolume create requires the target path not to exist. If Podman has already created ~/.local/share/containers, do not delete it or create a subvolume over it. Stop the relevant containers and use the migration procedure below.
A dedicated subvolume does not make the data consume less space by itself. It prevents future snapshots of @home from retaining deleted container layers and gives the storage an explicit boundary for accounting and cleanup.
Migrate an initialized Podman store
If Podman has already created containers or images, do not delete or reset its storage. The storage path can be moved aside, replaced by a subvolume, and copied back while preserving the path Podman already knows.
First inspect the rootless store:
podman info --format \
'graphRoot={{.Store.GraphRoot}} graphDriver={{.Store.GraphDriverName}}'
podman system df
The following procedure assumes the usual rootless overlay graph driver and a graph root beneath:
~/.local/share/containers/
If the graph driver is btrfs, the store contains nested subvolumes, or GraphRoot points somewhere else, do not apply the commands blindly. Inspect that layout and adapt the migration.
Check whether the directory is already a subvolume or contains nested subvolumes:
sudo btrfs subvolume show "$HOME/.local/share/containers"
sudo btrfs subvolume list -o "$HOME/.local/share/containers"
Interpret the results before continuing:
- If
subvolume showsucceeds, the directory is already a subvolume and no migration is required. - If
subvolume showfails andsubvolume listreturns no output, the directory contains no nested subvolumes. The copy procedure below applies. - If
subvolume listreports any children, stop here. The procedure below would flatten those boundaries into ordinary directories, so each child needs a Btrfs-aware migration.
Before migrating:
- Stop user-level systemd services, including Quadlets, that can restart containers.
- Stop all rootless containers.
- Ensure enough free space exists for a temporary second directory.
- Keep the temporary migration backup until Podman has been validated.
systemctl --user list-units --type=service |
grep -E 'container-|podman' || true
podman stop --all
sudo du -sh "$HOME/.local/share/containers"
df -h "$HOME"
Create a timestamped backup path and move the existing store. Renaming within the same filesystem is immediate and does not duplicate its data:
containers_root="$HOME/.local/share/containers"
containers_backup="${containers_root}.before-subvolume-$(date +%Y%m%d-%H%M%S)"
mv -- "$containers_root" "$containers_backup"
Create the replacement subvolume at the original path:
sudo btrfs subvolume create "$containers_root" &&
sudo chown --reference="$containers_backup" "$containers_root" &&
sudo chmod --reference="$containers_backup" "$containers_root"
Copy the storage back while preserving ownership, permissions, hard links, extended attributes, and sparse files. The reflink option avoids duplicating compatible Btrfs extents, but enough free space should still be available in case some files require ordinary copies:
sudo cp -a --reflink=auto \
"$containers_backup/." \
"$containers_root/"
sync
Verify the new boundary:
sudo btrfs subvolume show "$containers_root"
sudo btrfs filesystem du -s "$containers_root"
Then inspect Podman without changing the graph-root path:
podman info --format \
'graphRoot={{.Store.GraphRoot}} graphDriver={{.Store.GraphDriverName}}'
podman ps --all
podman images
podman volume ls
podman system df
Start a known disposable container and verify that its volumes and bind mounts behave correctly. Keep the timestamped migration backup through several successful container sessions. Remove it only after verifying that no required images, volumes, or container metadata are missing.
Do not apply this procedure to a rootful store. Rootless and rootful Podman do not share the same storage database, and rootful migration has additional system-level requirements described below.
Separate generated sandbox data
Apply the same principle to custom generated state stored at:
~/.local/share/ai-sandboxes/
On a new installation, create the subvolume before generating any sandboxes:
mkdir -p "$HOME/.local/share"
sudo btrfs subvolume create "$HOME/.local/share/ai-sandboxes" &&
sudo chown --reference="$HOME" "$HOME/.local/share/ai-sandboxes"
Confirm the boundary and measure it directly:
sudo btrfs subvolume show "$HOME/.local/share/ai-sandboxes"
sudo btrfs filesystem du -s "$HOME/.local/share/ai-sandboxes"
Keep source code, prompts, reusable configuration, and other durable inputs outside this subvolume. It should contain only sandbox data that can be reproduced or intentionally discarded.
If the directory already contains generated data, stop every process and container using it before migrating:
sandbox_root="$HOME/.local/share/ai-sandboxes"
sandbox_backup="${sandbox_root}.before-subvolume-$(date +%Y%m%d-%H%M%S)"
sudo du -sh "$sandbox_root"
mv -- "$sandbox_root" "$sandbox_backup"
sudo btrfs subvolume create "$sandbox_root" &&
sudo chown --reference="$sandbox_backup" "$sandbox_root" &&
sudo chmod --reference="$sandbox_backup" "$sandbox_root"
sudo cp -a --reflink=auto \
"$sandbox_backup/." \
"$sandbox_root/"
sync
Verify the subvolume before restarting the sandbox generator:
sudo btrfs subvolume show "$sandbox_root"
sudo btrfs filesystem du -s "$sandbox_root"
sudo du -sh "$sandbox_backup" "$sandbox_root"
Keep the backup until several generated sandboxes have been created, started, stopped, and removed successfully.
Treat rootful Podman separately
This section provides layout guidance for a fresh installation, not a complete rootful Podman setup procedure. The exact subvolume name, device, mount options, and /etc/fstab entry depend on the installed Btrfs layout.
Rootless Podman runs under your user account, while rootful Podman runs as root, commonly through sudo. Their stores are separate. If you use rootful containers, inspect the rootful store independently:
sudo podman info --format 'graphRoot={{.Store.GraphRoot}}'
sudo podman system df
Rootful Podman's data commonly lives under /var/lib/containers. If that directory lives directly inside the root @ subvolume, every root snapshot can retain old container layers after Podman deletes them.
For reliable rollback behavior, prefer a top-level sibling subvolume such as @containers, mounted at /var/lib/containers through /etc/fstab. This keeps rootful container data outside root snapshots.
Use the existing root entry as the source for the Btrfs device, UUID, and mount options. Change the subvolume and mount point for the new entry rather than inventing storage options:
findmnt -no SOURCE,OPTIONS /
grep -E '[[:space:]]/[[:space:]]' /etc/fstab
The resulting mount should be verified explicitly:
findmnt -T /var/lib/containers
sudo btrfs subvolume show /var/lib/containers
Create and mount this subvolume before rootful Podman writes data.
Migrating an existing rootful Podman store is outside the scope of this guide. Do not reuse the rootless migration procedure unchanged. A rootful migration must also account for system services, the /etc/fstab mount, and root-owned storage. If the rootful graph root already contains data, stop here and plan that migration before changing the mount.
Set retention and cleanup
Give Snapper a bounded budget
Inspect the active configuration:
sudo snapper list-configs
sudo snapper -c root get-config
sudo snapper -c root list
Set retention according to the disk size and the rollback history you need. For a container-heavy workstation with a terabyte-scale filesystem, the following /etc/snapper/configs/root values are a practical starting point:
SPACE_LIMIT="0.10"
FREE_LIMIT="0.20"
NUMBER_CLEANUP="yes"
NUMBER_MIN_AGE="3600"
NUMBER_LIMIT="2-5"
NUMBER_LIMIT_IMPORTANT="1-2"
TIMELINE_CREATE="yes"
TIMELINE_CLEANUP="yes"
TIMELINE_MIN_AGE="3600"
TIMELINE_LIMIT_HOURLY="6"
TIMELINE_LIMIT_DAILY="7"
TIMELINE_LIMIT_WEEKLY="4"
TIMELINE_LIMIT_MONTHLY="3"
TIMELINE_LIMIT_QUARTERLY="0"
TIMELINE_LIMIT_YEARLY="0"
EMPTY_PRE_POST_CLEANUP="yes"
EMPTY_PRE_POST_MIN_AGE="3600"
These values are a starting point, not universal defaults. They provide six hourly recovery points plus a short daily, weekly, and monthly history. This is a rollback policy, not an archival backup policy.
The snapper-configs(5) documentation explains the cleanup limits and their interaction with Btrfs quotas.
If snapper -c root get-config shows QGROUP without a value, ask Snapper to create and configure its aggregate quota group:
sudo snapper -c root setup-quota
Then verify it:
sudo snapper -c root get-config
sudo btrfs qgroup show -reF /
sudo btrfs quota rescan -s /
Do not manually invent a qgroup identifier. If quota setup fails, diagnose the current quota state instead of placing an arbitrary value in the configuration.
Finally, ensure automatic creation and cleanup are enabled:
sudo systemctl enable --now snapper-timeline.timer
sudo systemctl enable --now snapper-cleanup.timer
systemctl list-timers 'snapper-*'
Space limits are a safety boundary. Strict snapshot counts and isolation of high-churn data remain the primary controls.
Let Podman own container cleanup
Snapper should not be the lifecycle manager for containers. Podman already knows which containers, images, and volumes are in use.
Start with the storage summary and a list of containers, including their writable-layer sizes:
podman system df
podman ps --all --size
Prefer transient containers when their state is disposable:
podman run --rm ...
List all containers, then let Podman prompt before removing stopped containers:
podman ps --all
podman container prune
Review and remove dangling images that no container uses:
podman image prune
Be more cautious with:
podman volume prune
podman system prune --all --volumes
An unused volume can still contain valuable database or application data. Do not automate volume pruning unless every affected volume is explicitly disposable.
The Podman documentation describes the storage report from podman system df and the resources affected by podman system prune .
Diagnose and maintain Btrfs
Account for the hibernation swap file
A high-memory Omarchy workstation may contain a large Btrfs swap file. For example, a workstation with 128 GiB of installed RAM may report about 125 GiB of usable memory and have a 126 GiB swap file:
free -h
sudo du -sh /swap
swapon --show --bytes
cat /proc/cmdline
Kernel arguments such as these indicate hibernation support:
resume=/dev/mapper/root
resume_offset=3730382
The swap file can explain a large part of the difference between du -x / and df, because /swap may be a nested subvolume skipped by du -x.
Verify the Btrfs resume offset:
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile
It should match the resume_offset in the kernel command line.
Do not delete, move, defragment, or recreate the swap file merely because its current usage is zero. An idle swap file may still be required for hibernation. Recreating it changes its physical mapping and requires updating the resume offset and boot configuration. The ArchWiki Btrfs swap-file guidance covers the relevant restrictions.
Use the right commands when space looks wrong
Start with the whole filesystem:
sudo btrfs filesystem usage -T /
Measure known subvolumes separately:
sudo btrfs filesystem du -s \
/home \
/swap \
/var/cache/pacman/pkg \
/var/log
Inspect visible root directories without crossing subvolume boundaries:
sudo du -xhd1 / | sort -h
sudo du -xhd1 /home | sort -h
Inspect snapshots:
sudo snapper -c root list
sudo btrfs subvolume list -s /
Check deleted files still held open by processes:
sudo lsof +L1
Memory-backed entries such as memfd:mozilla-ipc (deleted) are not Btrfs files and usually do not explain filesystem usage. Look for large deleted regular files on the Btrfs device instead.
After deleting snapshots, Btrfs may reclaim extents asynchronously. Allow it time to finish before assuming cleanup failed.
Avoid common Btrfs cleanup mistakes
Do not delete Snapper directories manually. Delete snapshots through Snapper:
sudo snapper -c root delete SNAPSHOT_ID
Or run the configured cleanup algorithms:
sudo snapper -c root cleanup timeline
sudo snapper -c root cleanup number
Do not run a full balance as routine cleanup. A balance relocates extents; it does not decide which container images or snapshots are valuable. Unnecessary full balances generate substantial I/O and can amplify churn.
Do not disable copy-on-write across broad directory trees as a substitute for lifecycle design. Separate subvolumes, bounded snapshots, and application-aware pruning solve the underlying problem more clearly.
Keep at least 15 to 20 percent of the filesystem free. Btrfs needs working space for metadata, extent relocation, snapshots, and copy-on-write updates. The Btrfs quota-group documentation also explains why referenced and exclusive space can differ from simple directory totals.
Follow a practical operating routine
After a container-heavy session:
podman system df
sudo btrfs filesystem usage -T /
sudo snapper -c root list
Periodically:
podman ps --all --size
sudo btrfs device stats /
sudo btrfs scrub start -Bd /
A scrub verifies checksummed data; it is an integrity task, not a space-reclamation task.
The resulting model is straightforward:
- Snapper protects the operating system.
- External backups protect source code, documents, and other irreplaceable user data.
- Podman manages container resources.
- Dedicated subvolumes isolate high-churn generated data.
- The hibernation swap file remains intact.
- Btrfs keeps enough free space to operate safely.
Once those responsibilities are separated, the reports from df, du, Snapper, and Podman become easier to reconcile. Each describes a different layer of the same storage system.