DEV Community

Cover image for Stop Hand-Crafting /run and /tmp Trees: Practical systemd-tmpfiles on Linux
Lyra
Lyra

Posted on

Stop Hand-Crafting /run and /tmp Trees: Practical systemd-tmpfiles on Linux

Stop Hand-Crafting /run and /tmp Trees: Practical systemd-tmpfiles on Linux

Most Linux hosts already run systemd-tmpfiles on every boot. Packages drop snippets under /usr/lib/tmpfiles.d/, the early boot units create volatile trees under /run and friends, and a timer later ages junk out of /tmp and /var/tmp.

Operators still keep reinventing the same work with ad-hoc shell:

mkdir -p /var/lib/myapp/cache
chown myapp:myapp /var/lib/myapp/cache
chmod 0750 /var/lib/myapp/cache
find /var/lib/myapp/cache -mtime +7 -delete
Enter fullscreen mode Exit fullscreen mode

That works until the next image rebuild, the next package update that races your script, or the next reboot where /run is empty again and nothing recreates the sockets directory.

tmpfiles.d is the declarative replacement: one line per path, applied the same way at boot, on package install hooks, and on a periodic clean timer.

What actually runs

Three system units do the work (names from systemd-tmpfiles(8)):

Unit Role
systemd-tmpfiles-setup-dev.service Early device-node /dev setup
systemd-tmpfiles-setup.service Create/adjust paths during boot (--create, with --boot lines)
systemd-tmpfiles-clean.service + .timer Age-based cleanup (--clean)

Typical boot invocation (documented in the same man page):

systemd-tmpfiles --remove --create
Enter fullscreen mode Exit fullscreen mode

Useful direct calls when you change config:

# Apply create/adjust rules now (safe runtime subset)
sudo systemd-tmpfiles --create

# Run age cleanup now
sudo systemd-tmpfiles --clean

# See which snippets would be merged
systemd-tmpfiles --cat-config | less

# Apply only one admin file
sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp.conf
Enter fullscreen mode Exit fullscreen mode

After editing drop-ins, restarting systemd-tmpfiles-clean.service re-applies settings that are safe at runtime. Lines marked with ! only run when --boot is passed — do not force those on a live multi-user system unless you understand the impact.

Config locations and precedence

Search paths (system instance):

  1. /etc/tmpfiles.d/*.conf — admin overrides
  2. /run/tmpfiles.d/*.conf — runtime
  3. /usr/lib/tmpfiles.d/*.conf — vendor/package (plus /usr/local/lib/tmpfiles.d on newer releases)

Same basename: /etc wins over /run wins over /usr/lib. Files are then sorted lexicographically by filename. If two different files claim the same path, the lexicographically earliest filename wins; later conflicts are logged as errors.

Disable a vendor file without deleting package content:

sudo ln -s /dev/null /etc/tmpfiles.d/some-vendor.conf
Enter fullscreen mode Exit fullscreen mode

User instance (separate from system cleanup) reads ~/.config/user-tmpfiles.d/, $XDG_RUNTIME_DIR/user-tmpfiles.d/, and share paths under ~/.local/share/user-tmpfiles.d/ and /usr/share/user-tmpfiles.d/. Important nuance from the man page: system age rules for /tmp still apply to user files placed there, even if the user instance turns cleanup off.

Line format

#Type  Path                 Mode  User  Group  Age  Argument
d      /run/myapp           0755  myapp myapp  -    -
f      /run/myapp/ready     0644  myapp myapp  -    -
L      /run/myapp/log       -     -     -      -    /var/log/myapp
Enter fullscreen mode Exit fullscreen mode

Fields:

  • Type — operation letter, optional modifiers (+, !, -, =, ~, ^)
  • Path — absolute path (specifiers like %h, %C allowed)
  • Mode — e.g. 0755; - means default (0755 dirs, 0644 files) or “don’t change” for z/Z
  • User / Group — name or numeric ID; use system accounts resolvable in early boot
  • Age — cleanup threshold (10d, 1h, 0, or -)
  • Argument — content, symlink target, device major:minor, ACL text, etc.

Types you will actually use

Create and own paths

# Directory; contents eligible for age cleanup if Age is set
d /run/myapp 0750 myapp myapp - -

# Like d, but --remove empties it (boot wipe of leftovers)
D /run/myapp/tmp 0750 myapp myapp 1d -

# Create file only if missing; optional content in Argument
f /var/lib/myapp/state.env 0640 myapp myapp - -

# Always truncate/write content
f+ /run/myapp/banner 0644 root root - Hello from tmpfiles

# Write into an existing file (globs OK); w+ appends
w /sys/module/dummy/parameters/foo - - - - 1
Enter fullscreen mode Exit fullscreen mode

Leading parents for create types are auto-created as root:root mode 0755. If you need different ownership on parents, add explicit d lines first.

Clean without owning create

# Adjust mode/owner of existing dirs + age their contents (globs OK)
e /var/tmp/myapp 0750 myapp myapp 7d -
Enter fullscreen mode Exit fullscreen mode

e does nothing useful unless at least one of mode, user, group, or age is set.

Symlinks, pipes, copy-from-factory

L /etc/os-release - - - - ../usr/lib/os-release
L+ /run/service/current - - - - /opt/service/releases/1.2.3

p /run/myapp/cmd.fifo 0600 myapp myapp - -

# Copy from source if target missing/empty
C /var/lib/myapp/defaults - - - - /usr/share/myapp/factory
Enter fullscreen mode Exit fullscreen mode

L/C with omitted argument pull same-named objects from /usr/share/factory/.

Cleanup exclusions and forced removal

# Keep this tree during age clean of a parent
x /var/tmp/myapp/keep - - - - -

# Remove empty path / recursive tree (usually with ! for boot-only)
r /run/myapp/stale-lock - - - - -
R! /run/myapp/scratch - - - - -
Enter fullscreen mode Exit fullscreen mode

x excludes a path (and below) from age cleaning; it does not block r/R.

Fix permissions after the fact

z /var/lib/myapp/data 0750 myapp myapp - -
Z /var/lib/myapp 0750 myapp myapp - -
Enter fullscreen mode Exit fullscreen mode

z/Z adjust mode, owner, and restore SELinux context when policy is active. They accept globs.

ACLs and attributes (when you need them)

a /var/lib/shared 0750 root shared - u:alice:rwx,u:bob:r-x
h /var/log/immutable.log - - - - +i
Enter fullscreen mode Exit fullscreen mode

Type modifiers that prevent outages

Modifier Meaning
! Only with --boot (unsafe on a live system — e.g. wiping X11 locks)
- Create failure is non-fatal (great for optional sysfs writes in containers)
+ Recreate/truncate/replace semantics for f/w/L/p/c/b (and force-copy for C+ on newer systemd)
= If an existing object has the wrong type, remove it first
~ on type Base64-decode Argument before write (f/w family)
^ on type Read Argument from a service credential name (tmpfiles.extra / credentials docs)

Example from the manual — boot-only lock cleanup paired with a normal directory line:

d /tmp/.X11-unix 1777 root root 10d
r! /tmp/.X[0-9]*-lock
Enter fullscreen mode Exit fullscreen mode

Container-safe sysctl-style write:

w- /proc/sys/vm/swappiness - - - - 10
Enter fullscreen mode Exit fullscreen mode

Age cleanup without foot-guns

Age applies to d, D, e, v, q, Q, C, x, and X.

Formats: 10d, 12h, 30m, 1w, or combinations that sum. 0 means delete matching contents every --clean run.

Default “is this still young?” check uses mtime, atime, and (for non-directories) ctime. Directory ctime is ignored by default so the cleaner does not keep resurrecting directories it just touched.

Narrow the clocks with age-by:

# Files by mtime/birth; directories by atime; threshold 1 hour
d /tmp/foo/bar - - - bmA:1h -
Enter fullscreen mode Exit fullscreen mode

Prefix Age with ~ to clean only one level down, not the directory’s immediate children naming semantics described in tmpfiles.d(5) — useful when the top-level entries must stay.

Applications can take a shared BSD flock on a directory to pause aging for that subtree while a job runs.

Lab: app runtime dir + aged cache

Assume a system user myapp already exists (create with useradd --system / systemd-sysusers as you prefer).

sudo tee /etc/tmpfiles.d/myapp.conf >/dev/null <<'EOF'
# Runtime (tmpfs /run) — recreated every boot
d /run/myapp           0750 myapp myapp - -
d /run/myapp/sockets   0750 myapp myapp - -
f /run/myapp/README    0640 myapp myapp - managed-by-tmpfiles

# On-disk cache with 7-day age cleanup of contents
d /var/cache/myapp     0750 myapp myapp 7d -

# Keep one subdirectory forever while parent ages
x /var/cache/myapp/pinned - - - - -

# Permission repair if something else chowns the tree
Z /var/cache/myapp 0750 myapp myapp - -
EOF

sudo systemd-tmpfiles --create /etc/tmpfiles.d/myapp.conf
Enter fullscreen mode Exit fullscreen mode

Verify:

namei -l /run/myapp/sockets
ls -la /run/myapp /var/cache/myapp
Enter fullscreen mode Exit fullscreen mode

Simulate cache files and clean:

sudo -u myapp touch /var/cache/myapp/old.bin /var/cache/myapp/pinned/keep.bin
# After aging window, or with Age 0 in a test copy of the snippet:
sudo systemd-tmpfiles --clean /etc/tmpfiles.d/myapp.conf
Enter fullscreen mode Exit fullscreen mode

For a force-clean lab without waiting, use a separate test snippet with e /tmp/myapp-cache-test 0755 myapp myapp 0 -, create files, then --clean.

Prefer unit directories when the lifetime matches the service

tmpfiles.d is for paths whose lifetime is independent of one unit, or that need richer rules (globs, ACLs, factory copy, boot-only removes).

For a single service, prefer systemd.exec(5) directives so the directory dies with the stop:

# /etc/systemd/system/myapp.service
[Service]
User=myapp
RuntimeDirectory=myapp
RuntimeDirectoryMode=0750
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp
ExecStart=/usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

That yields /run/myapp, /var/lib/myapp, /var/cache/myapp, /var/log/myapp with matching ownership, without a parallel tmpfiles snippet.

Rule of thumb:

  • Service-private runtime/state/cache/log → RuntimeDirectory= / StateDirectory= / …
  • Shared, multi-service, boot-seeded, aged, or factory-copied paths → tmpfiles.d
  • Secrets → LoadCredential= / systemd-creds, not world-readable tmpfiles content

Image builds and credentials

Offline OS trees:

sudo systemd-tmpfiles --root=/mnt/rootfs --create -E
# -E skips /dev /proc /run /sys so you do not populate overmount points
Enter fullscreen mode Exit fullscreen mode

Disk images: --image=PATH (implies -E).

First-boot extras can arrive via the tmpfiles.extra credential: extra tmpfiles lines processed after on-disk drop-ins (cannot override earlier same-path winners). Combine with ^ type modifier to pull file bodies from named credentials.

Operational checklist

  1. Put admin rules in /etc/tmpfiles.d/name.conf — never edit vendor files in /usr/lib.
  2. Run systemd-tmpfiles --create after edits; use --boot only in early-boot context.
  3. Confirm the clean timer: systemctl status systemd-tmpfiles-clean.timer.
  4. Use x/X before aggressive parent Age= values.
  5. Mark destructive lines with !.
  6. Keep User=/Group= resolvable without LDAP at early boot.
  7. Mask bad vendor snippets with /etc/tmpfiles.d/foo.conf -> /dev/null.
  8. Do not store secrets in f/w arguments; use credentials.

Boundaries (what this article is not)

  • Not systemd-sysusers (account creation) or systemd-firstboot (machine identity seeding)
  • Not RuntimeDirectory= deep-dive beyond the boundary above
  • Not systemd-creds secret injection (covered separately)
  • Not Btrfs quota design beyond noting v/q/Q exist for subvolumes
  • Not a replacement for backups — age cleanup deletes data on purpose

References


Ship the snippet next to the unit, not a shell mkdir in ExecStartPre=. Your boot path stays boring — which is exactly what you want from /run and /tmp.

Top comments (0)