Tools: Complete Guide to Stop Hand-Crafting Service Users: Practical `systemd-sysusers` for Declarative Linux Accounts

Tools: Complete Guide to Stop Hand-Crafting Service Users: Practical `systemd-sysusers` for Declarative Linux Accounts

What systemd-sysusers is for, and what it is not

Why this is better than useradd in random scripts

File locations and precedence

The format, one line at a time

Validate before touching the real system

Admin override example

Inline testing is handy for quick experiments

--replace is built for packaging workflows

Important limitation: this does not create your data directories

A few practical rules I would follow

When to use DynamicUser= instead

Final thought If you still create service accounts with ad hoc useradd commands in install scripts or README files, you are making something important harder to audit, harder to override, and easier to forget. systemd-sysusers gives you a declarative way to create system users and groups from simple config files. It is built for service identities, package installs, image builds, and first-boot provisioning, not for interactive human accounts. In this guide, I will show how to: All commands and behaviors below were checked against the current systemd-sysusers(8) and sysusers.d(5) documentation, plus test runs on a Linux host with systemd 257. systemd-sysusers creates system users and groups and adds users to groups based on declarative config. It is a good fit for: It is not the right tool for normal login users. The man page is explicit that it is for system users and groups, and it works directly with local account files like /etc/passwd and /etc/group rather than remote identity sources. A sysusers.d file is easier to reason about than an imperative shell snippet because it is: That makes it especially useful for service packaging and reproducible infrastructure. sysusers.d files are read from these locations: The important rule is: If you want to disable a vendor file entirely, the documented approach is to place a symlink to /dev/null in /etc/sysusers.d with the same filename. A sysusers.d file is line-oriented. The most common record types are: Here is a practical example for a daemon called demoapp: A few details matter here: My favorite part of this workflow is that you can test it safely. On my test run, this reported output like: That gives you a no-surprises review step for CI, image builds, or packaging checks. When you are done testing, clean up the temp root: Suppose a vendor ships this file: But you want a fixed UID and GID locally for a migration or NFS-mapped storage policy. Create an override in /etc/sysusers.d/demoapp.conf: You can test the effective result without writing anything: In my test run, the override won and systemd-sysusers planned to create _demoapp with UID 450 and demoapp-data with GID 451. That is exactly the kind of behavior you want in a packaging-friendly declarative system. If you just want to test a few rules without writing a file first, --inline is useful: One easy-to-miss feature is --replace=PATH. This matters when a package installation script needs to create accounts before its final sysusers.d file is present on disk. The man page includes this exact pattern: That tells systemd-sysusers to treat the supplied content as if it were replacing /usr/lib/sysusers.d/radvd.conf, while still respecting any higher-priority admin override that may already exist in /etc/sysusers.d. That is a subtle feature, but a very useful one for package maintainers. systemd-sysusers creates account entries. It does not create the service's state directories for you. The sysusers.d(5) docs explicitly recommend pairing it with the right directory mechanism. For modern systemd services, prefer unit-level directory management when possible: That is usually cleaner than a separate tmpfiles rule because the directory lifecycle stays attached to the service definition. If you really need a separate tmpfiles policy, use tmpfiles.d: Then apply or test it with: If your service does not need a persistent, named account in /etc/passwd, consider DynamicUser= in the service unit instead. Use systemd-sysusers when you want a stable, declarative real system identity. Use DynamicUser= when you want systemd to allocate a temporary runtime identity for a service. That distinction matters: If you care about reproducible Linux systems, service identities should live in configuration, not in half-remembered setup commands. systemd-sysusers is one of those small systemd tools that quietly removes a lot of operational mess. You get clearer intent, safer testing, and a much better override story than hand-written account creation scripts. For daemon users, that is a solid trade. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# /usr/lib/sysusers.d/demoapp.conf u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data # /usr/lib/sysusers.d/demoapp.conf u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data # /usr/lib/sysusers.d/demoapp.conf u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data tmpdir=$(mktemp -d) mkdir -p "$tmpdir"/{etc,usr/lib}/sysusers.d cat >"$tmpdir/usr/lib/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data EOF systemd-sysusers \ --root="$tmpdir" \ --dry-run \ "$tmpdir/usr/lib/sysusers.d/demoapp.conf" tmpdir=$(mktemp -d) mkdir -p "$tmpdir"/{etc,usr/lib}/sysusers.d cat >"$tmpdir/usr/lib/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data EOF systemd-sysusers \ --root="$tmpdir" \ --dry-run \ "$tmpdir/usr/lib/sysusers.d/demoapp.conf" tmpdir=$(mktemp -d) mkdir -p "$tmpdir"/{etc,usr/lib}/sysusers.d cat >"$tmpdir/usr/lib/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data EOF systemd-sysusers \ --root="$tmpdir" \ --dry-run \ "$tmpdir/usr/lib/sysusers.d/demoapp.conf" Creating group 'demoapp-data' with GID 999. Creating group '_demoapp' with GID 998. Creating user '_demoapp' (Demo app -weight: 500;">service user) with UID 998 and GID 998. Would write /etc/group… Would write /etc/gshadow… Would write /etc/passwd… Would write /etc/shadow… Creating group 'demoapp-data' with GID 999. Creating group '_demoapp' with GID 998. Creating user '_demoapp' (Demo app -weight: 500;">service user) with UID 998 and GID 998. Would write /etc/group… Would write /etc/gshadow… Would write /etc/passwd… Would write /etc/shadow… Creating group 'demoapp-data' with GID 999. Creating group '_demoapp' with GID 998. Creating user '_demoapp' (Demo app -weight: 500;">service user) with UID 998 and GID 998. Would write /etc/group… Would write /etc/gshadow… Would write /etc/passwd… Would write /etc/shadow… rm -rf "$tmpdir" rm -rf "$tmpdir" rm -rf "$tmpdir" # /usr/lib/sysusers.d/demoapp.conf u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data # /usr/lib/sysusers.d/demoapp.conf u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data # /usr/lib/sysusers.d/demoapp.conf u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data u! _demoapp 450 "Demo app -weight: 500;">service user" g demoapp-data 451 m _demoapp demoapp-data u! _demoapp 450 "Demo app -weight: 500;">service user" g demoapp-data 451 m _demoapp demoapp-data u! _demoapp 450 "Demo app -weight: 500;">service user" g demoapp-data 451 m _demoapp demoapp-data tmpdir=$(mktemp -d) mkdir -p "$tmpdir"/{etc,usr/lib}/sysusers.d cat >"$tmpdir/usr/lib/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data EOF cat >"$tmpdir/etc/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp 450 "Demo app -weight: 500;">service user" g demoapp-data 451 m _demoapp demoapp-data EOF systemd-sysusers --root="$tmpdir" --dry-run demoapp.conf tmpdir=$(mktemp -d) mkdir -p "$tmpdir"/{etc,usr/lib}/sysusers.d cat >"$tmpdir/usr/lib/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data EOF cat >"$tmpdir/etc/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp 450 "Demo app -weight: 500;">service user" g demoapp-data 451 m _demoapp demoapp-data EOF systemd-sysusers --root="$tmpdir" --dry-run demoapp.conf tmpdir=$(mktemp -d) mkdir -p "$tmpdir"/{etc,usr/lib}/sysusers.d cat >"$tmpdir/usr/lib/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp - "Demo app -weight: 500;">service user" g demoapp-data - m _demoapp demoapp-data EOF cat >"$tmpdir/etc/sysusers.d/demoapp.conf" <<'EOF' u! _demoapp 450 "Demo app -weight: 500;">service user" g demoapp-data 451 m _demoapp demoapp-data EOF systemd-sysusers --root="$tmpdir" --dry-run demoapp.conf tmpdir=$(mktemp -d) mkdir -p "$tmpdir/etc/sysusers.d" systemd-sysusers \ --root="$tmpdir" \ --dry-run \ --inline \ 'u! _cachebot - "Cache Bot" /var/lib/cachebot /usr/sbin/nologin' \ 'g cachebot-data -' \ 'm _cachebot cachebot-data' tmpdir=$(mktemp -d) mkdir -p "$tmpdir/etc/sysusers.d" systemd-sysusers \ --root="$tmpdir" \ --dry-run \ --inline \ 'u! _cachebot - "Cache Bot" /var/lib/cachebot /usr/sbin/nologin' \ 'g cachebot-data -' \ 'm _cachebot cachebot-data' tmpdir=$(mktemp -d) mkdir -p "$tmpdir/etc/sysusers.d" systemd-sysusers \ --root="$tmpdir" \ --dry-run \ --inline \ 'u! _cachebot - "Cache Bot" /var/lib/cachebot /usr/sbin/nologin' \ 'g cachebot-data -' \ 'm _cachebot cachebot-data' printf '%s\n' 'u! _radvd - "radvd daemon"' \ | systemd-sysusers --dry-run --replace=/usr/lib/sysusers.d/radvd.conf - printf '%s\n' 'u! _radvd - "radvd daemon"' \ | systemd-sysusers --dry-run --replace=/usr/lib/sysusers.d/radvd.conf - printf '%s\n' 'u! _radvd - "radvd daemon"' \ | systemd-sysusers --dry-run --replace=/usr/lib/sysusers.d/radvd.conf - # /etc/systemd/system/demoapp.-weight: 500;">service [Service] User=_demoapp Group=_demoapp StateDirectory=demoapp CacheDirectory=demoapp LogsDirectory=demoapp # /etc/systemd/system/demoapp.-weight: 500;">service [Service] User=_demoapp Group=_demoapp StateDirectory=demoapp CacheDirectory=demoapp LogsDirectory=demoapp # /etc/systemd/system/demoapp.-weight: 500;">service [Service] User=_demoapp Group=_demoapp StateDirectory=demoapp CacheDirectory=demoapp LogsDirectory=demoapp # /usr/lib/tmpfiles.d/demoapp.conf d /var/lib/demoapp 0750 _demoapp _demoapp - - d /var/log/demoapp 0750 _demoapp _demoapp - - # /usr/lib/tmpfiles.d/demoapp.conf d /var/lib/demoapp 0750 _demoapp _demoapp - - d /var/log/demoapp 0750 _demoapp _demoapp - - # /usr/lib/tmpfiles.d/demoapp.conf d /var/lib/demoapp 0750 _demoapp _demoapp - - d /var/log/demoapp 0750 _demoapp _demoapp - - systemd-tmpfiles --create --prefix=/var/lib/demoapp --prefix=/var/log/demoapp systemd-tmpfiles --create --prefix=/var/lib/demoapp --prefix=/var/log/demoapp systemd-tmpfiles --create --prefix=/var/lib/demoapp --prefix=/var/log/demoapp - define -weight: 500;">service users and groups with sysusers.d - validate changes safely with --dry-run - understand override precedence between /usr/lib and /etc - use --replace for packaging workflows - pair account creation with the right directory-management approach - daemon accounts like _demoapp or postgres - package -weight: 500;">install or image-build workflows - reproducible -weight: 500;">service identities across machines - first-boot or offline-root provisioning with --root or --image - declarative, the desired account state lives in a file - auditable, you can see exactly which identities a package or -weight: 500;">service expects - override-friendly, admins can replace vendor defaults in /etc/sysusers.d - testable, systemd-sysusers --dry-run shows what would happen before anything is written - /etc/sysusers.d/*.conf - /run/sysusers.d/*.conf - /usr/local/lib/sysusers.d/*.conf - /usr/lib/sysusers.d/*.conf - /etc/sysusers.d overrides /run/sysusers.d and /usr/lib/sysusers.d - /run/sysusers.d overrides /usr/lib/sysusers.d - vendor packages should -weight: 500;">install files in /usr/lib/sysusers.d - local admin overrides belong in /etc/sysusers.d - u to create a system user, and implicitly a same-named group - g to create a group - m to add a user to a group - r to define a UID/GID allocation range - creates a locked system user named _demoapp - lets systemd allocate a UID automatically - creates a supplemental group called demoapp-data - adds _demoapp to that group - u! is preferable for most daemon accounts because it creates a fully locked account - - in the ID field means automatic UID/GID allocation - prefixing -weight: 500;">service accounts with _ is strongly recommended by the docs to avoid clashes with human users - experimenting during packaging work - verifying syntax in CI - teaching teammates what each field does - Use automatic UID/GID allocation unless you truly need fixed numbers. The docs strongly recommend - for most cases. - Prefix daemon accounts with _. This reduces collision risk with human users. - Prefer u! for -weight: 500;">service identities. Locked accounts are safer for daemons. - Keep vendor config in /usr/lib/sysusers.d, local policy in /etc/sysusers.d. That is the intended split. - Pair accounts with -weight: 500;">service-level directories or tmpfiles. Do not assume the home or state path will magically appear. - Use --dry-run in CI and image builds. It is cheap and catches bad assumptions early. - systemd-sysusers is better for packages, shared file ownership, and predictable account names - DynamicUser= is better for tighter isolation when persistence is unnecessary - systemd-sysusers(8) man page: https://man7.org/linux/man-pages/man8/systemd-sysusers.8.html - sysusers.d(5) man page: https://man7.org/linux/man-pages/man5/sysusers.d.5.html - systemd upstream notes on UID/GID ranges: https://systemd.io/UIDS-GIDS/ - systemd upstream notes on user/group naming: https://systemd.io/USER_NAMES/ - tmpfiles.d(5) man page: https://man7.org/linux/man-pages/man5/tmpfiles.d.5.html - systemd.exec(5) for StateDirectory= and related directives: https://man7.org/linux/man-pages/man5/systemd.exec.5.html