Rootless Podman Quadlet with systemd User Services

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Linux hosts with Podman Quadlet support, systemd user sessions, and cgroup v2
Privilege Normal user for Quadlet work; sudo for loginctl linger and administrator paths
Scope Rootless Quadlet lifecycle — user search paths, systemctl --user, journalctl --user, linger for boot and logout persistence, admin-provisioned units under /etc/containers/systemd/users/, XDG_RUNTIME_DIR, supplementary-group traps, rootless storage, and boot troubleshooting. Assumes working rootless Podman. Does not cover subuid setup, rootless networking, full .container directive reference, privileged-port workarounds, or generic Quadlet generator errors.
IMPORTANT
This guide covers rootless Quadlet — units owned by a normal Linux user and managed with systemctl --user. It does not teach rootless Podman setup, rootless networking, or every [Container] directive. For those topics, see rootless Podman, Podman Quadlet container file, and Podman Quadlet with systemd for the shared generator model.

You already run Podman without root. Quadlet lets you declare containers as .container files and hand lifecycle to your systemd user manager instead of PID 1. The shift is not cosmetic: paths, systemctl flags, journals, and boot behavior all change when the unit runs rootlessly.

The lab user is podtest. Every systemctl --user and podman command below runs as that account unless noted.


How rootless Quadlet differs from rootful Quadlet

Rootful Quadlet drops files where the system manager reads them:

text
/etc/containers/systemd/web.container
system systemd manager (PID 1)
systemctl / journalctl
rootful Podman

Rootless Quadlet uses the user manager tied to one Linux account:

text
~/.config/containers/systemd/web.container
systemd --user (per-user manager)
systemctl --user / journalctl --user
rootless Podman

Do not try to run a rootful Quadlet as another user by adding [Service] overrides:

ini
[Service]
User=podtest

That still executes under PID 1's generator. Rootless Quadlet must be placed in a rootless search path and started by that user's systemd manager. The Quadlet [Container] syntax is largely the same in rootless and rootful mode, but some directives behave differently or are restricted by rootless Podman. The main operational differences covered here are ownership, search paths, user-manager scope, linger, storage, and session environment — see Podman Quadlet container file for directive detail.


Verify rootless Podman first

Quadlet only wraps Podman. Confirm the account is already rootless before you add unit files.

As podtest, ask Podman about security mode and cgroup version:

bash
podman info --format 'rootless={{.Host.Security.Rootless}} cgroups={{.Host.CgroupsVersion}}'

Sample output:

output
rootless=true cgroups=v2

A false rootless value means you are still on the rootful path — fix rootless Podman before continuing. Quadlet requires cgroup v2; if cgroups is not v2, resolve that before continuing.

Run a quick container smoke test:

bash
podman run --rm quay.io/podman/hello

The hello image prints its ASCII banner and exits. That proves image pull and rootless runtime work under this account.


Rootless Quadlet search paths

Quadlet generators scan several directories. For day-to-day work you usually touch only one.

Path Who manages it Typical use
~/.config/containers/systemd/ The logged-in user Personal services you edit yourself
$XDG_CONFIG_HOME/containers/systemd/ Same user when XDG_CONFIG_HOME is set Same as above when config lives outside ~/.config
$XDG_RUNTIME_DIR/containers/systemd/ User session Ephemeral or session-scoped units
/etc/containers/systemd/users/ Administrator Quadlets distributed to all users
/etc/containers/systemd/users/${UID}/ Administrator Quadlets for one UID

Think of the split this way:

  • ~/.config/... — you own and maintain the declaration.
  • /etc/.../users/ — an administrator ships the same unit to every user session that loads it.
  • /etc/.../users/${UID}/ — an administrator targets a single service account (for example UID 1014 for podtest).

Distribution packages may add vendor paths under /usr/share/containers/systemd/users/. Treat those like administrator content: useful to know, rarely the first place you create a personal app unit.


Create a rootless .container file

Create the user config directory:

bash
mkdir -p ~/.config/containers/systemd

Add ~/.config/containers/systemd/web.container:

ini
[Container]
Image=quay.io/podman/hello

[Install]
WantedBy=default.target

Do not run systemctl --user enable web.service. Quadlet processes the [Install] section while generating the service, so WantedBy=default.target is declared in the .container file and takes effect after systemctl --user daemon-reload.

web.container becomes web.service in the user manager. The hello image exits after printing, so the unit may show inactive (dead) soon after start — that is expected for this smoke test.

Tell the user manager to pick up the new file:

bash
systemctl --user daemon-reload

daemon-reload succeeds silently when the generator accepts the unit.

Start the generated service — no sudo:

bash
systemctl --user start web.service

Check status under the same user manager:

bash
systemctl --user status web.service --no-pager

Sample output:

output
● web.service
     Loaded: loaded (/home/podtest/.config/containers/systemd/web.container; generated)
     Active: active (running) since Sun 2026-08-23 03:09:00 IST; 33ms ago
   Main PID: 248423 (conmon)

The Loaded: line must point at your home-directory .container file. Active: may flip to inactive (dead) once the hello container finishes — that only means the one-shot workload exited cleanly.


Why sudo systemctl is wrong here

sudo systemctl talks to PID 1. systemctl --user talks to your user manager. They are separate unit namespaces.

Query the system manager as root:

bash
sudo systemctl status web.service --no-pager

Sample output:

output
× web.service
     Loaded: not-found (Reason: Unit web.service not found.)
     Active: failed (Result: exit-code) since Sun 2026-08-23 03:04:38 IST; 4min ago

Even when a name collides, the system unit is not your rootless Quadlet. The Loaded: path will not show /home/podtest/.config/containers/systemd/web.container.

Query the user manager instead:

bash
systemctl --user status web.service --no-pager

Sample output:

output
● web.service
     Loaded: loaded (/home/podtest/.config/containers/systemd/web.container; generated)
     Active: inactive (dead) since Sun 2026-08-23 03:09:00 IST; 13s ago

The home-directory path in Loaded: confirms you are inspecting the right manager.

The same split applies to Podman. As root:

bash
sudo podman ps --format 'table {{.Names}}\t{{.Status}}'

Root's container list is empty when only rootless workloads run.

As podtest:

bash
podman ps --format 'table {{.Names}}\t{{.Status}}'

You see containers started by that user's manager. Always match the Linux account: systemctl --user plus podman as the same user, never sudo for routine rootless checks.


Rootless Quadlet logs

Container stdout and Quadlet generator messages for user units land in the user journal.

Recent lines for the service:

bash
journalctl --user -u web.service --no-pager -n 8

Sample output:

output
Aug 23 03:09:00 vm1.lab.example systemd-web[248423]: Project:   https://github.com/containers/podman
Aug 23 03:09:00 vm1.lab.example systemd-web[248423]: Website:   https://podman.io
Aug 23 03:09:00 vm1.lab.example web[248401]: efbc7bb528af8ea6f1b746109408a95d95287472610a75f07d066b08ab539dc4
Aug 23 03:09:00 vm1.lab.example podman[248427]: container remove efbc7bb528af8ea6f1b746109408a95d95287472610a75f07d066b08ab539dc4

Follow live output while you reproduce an issue:

bash
journalctl --user -u web.service -f

Use Ctrl+C to stop following. You do not need sudo to read your own user journal. Reach for sudo journalctl only when you are debugging someone else's session as an administrator.


Linger is not required just to use rootless Quadlet

A common oversimplification says you must run loginctl enable-linger before any rootless Quadlet works. That is false for interactive use.

While you have an active login and a running user systemd manager:

bash
systemctl --user start web.service

works without linger. I run Quadlet units during an SSH or desktop session without enabling linger first.

Linger matters when the service must:

  • start at host boot without anyone logging in
  • keep running after the user's final logout
  • keep the user systemd manager alive when no graphical or SSH session exists

For a container you start manually during the workday, skip linger until you need boot or logout persistence.


Enable linger for boot and logout persistence

When boot-time or post-logout behavior is required, an administrator enables linger for that account:

bash
sudo loginctl enable-linger podtest

Verify the setting:

bash
loginctl show-user podtest -p Linger

Sample output:

output
Linger=yes

With linger and [Install] WantedBy=default.target in the Quadlet file, reload the user manager after edits — Quadlet applies the install relationship during generation; do not systemctl --user enable the generated service:

bash
systemctl --user daemon-reload

On the next boot, systemd starts the user manager for podtest even when nobody logs in, and units wanted by default.target can start automatically. Test on a lab host by rebooting and checking systemctl --user status from a lingering session or loginctl state — do not assume boot works until you verify on your distribution.


Disable linger

To stop keeping the user manager alive without a session:

bash
sudo loginctl disable-linger podtest

After disable, user services may stop when the last session ends, and the user manager might not exist at boot until someone logs in again. Disabling linger does not delete Quadlet files under ~/.config/containers/systemd/ — it only changes session policy.


Administrator-provisioned rootless Quadlet

Teams sometimes centralize unit files while still running containers rootlessly. Place the declaration where the administrator owns the file but the target user's manager executes it.

Create a UID-specific directory (1014 is podtest on the lab host):

bash
sudo mkdir -p /etc/containers/systemd/users/1014

Install web.container (or another name) there:

ini
[Container]
Image=quay.io/podman/hello
ContainerName=admin-hello

[Install]
WantedBy=default.target

As podtest, reload the user manager:

bash
systemctl --user daemon-reload

Confirm the generator picked up the admin file:

bash
systemctl --user list-unit-files 'admin-web*'

Sample output:

output
UNIT FILE         STATE     PRESET
admin-web.service generated -

The administrator owns /etc/containers/systemd/users/1014/admin-web.container, but admin-web.service still runs under podtest's rootless Podman. That pattern suits service accounts where config management drops units without giving users write access to /etc.


/etc/containers/systemd/users/ for all users

A Quadlet file directly under:

text
/etc/containers/systemd/users/

can be processed by user sessions broadly — not only one UID. Use that path when every user (or every user who receives the drop-in) should get the same generated unit.

Prefer /etc/containers/systemd/users/${UID}/ when the workload belongs to one account. Dropping an application-specific unit in the all-users directory accidentally exposes it to every user manager that loads system-wide user Quadlets.


Rootless environment and XDG_RUNTIME_DIR

systemctl --user and rootless Podman expect a valid session runtime directory.

Print the variable inside a normal login shell:

bash
echo "$XDG_RUNTIME_DIR"

Sample output:

output
/run/user/1014

Typical layout is /run/user/$UID. Without it, user systemd cannot connect. A bare su into the account often reproduces the failure:

bash
systemctl --user status web.service

Sample output:

output
Failed to connect to user scope bus via local transport: $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined

That often appears when you use plain su instead of a login shell (su -, machinectl shell, or SSH). Prefer a real user session for Quadlet work. Do not paper over a broken session by exporting another user's XDG_RUNTIME_DIR — that connects you to the wrong bus.

Administrators automating checks from cron or Ansible can use machinectl shell podtest@ or systemctl --user -M podtest@ patterns on supported setups; interactive tutorials should stick to a normal login.


Supplementary groups and GroupAdd=keep-groups

Rootless Quadlet inherits the user manager's view of groups from when that manager started. This bites when a unit uses:

ini
GroupAdd=keep-groups

and an administrator later adds the user to a new group:

bash
sudo usermod -aG docker podtest

The running user manager may not see the new membership until you refresh the session. Terminate the user's systemd user manager:

bash
sudo loginctl terminate-user podtest

Then log in again (or reboot). Until you do, device or volume permission errors can look like Podman bugs when they are stale group data on the user manager.


Rootless port restrictions

A rootless Quadlet that publishes a privileged host port:

ini
PublishPort=80:80

hits the same low-port restriction as ordinary rootless podman run. The generator does not grant extra capability.

Use a high host port in the unit, or follow rootless Podman privileged ports for supported workarounds. This article does not duplicate those recipes.


Rootless Quadlet storage

Rootless images and container layers live in the user's store, not /var/lib/containers/storage.

Confirm paths:

bash
podman info --format 'rootless={{.Host.Security.Rootless}} graphRoot={{.Store.GraphRoot}} runRoot={{.Store.RunRoot}}'

Sample output:

output
rootless=true graphRoot=/home/podtest/.local/share/containers/storage runRoot=/run/user/1014/containers

Administrators hunting disk usage for a rootless Quadlet service must inspect that user's home tree and /run/user/UID, not only the rootful graph root.


Troubleshooting when the rootless service does not start at boot

Work through these checks in order when a unit runs interactively but fails after reboot or logout:

Symptom Likely cause Fix
Unit not found in user manager File not in a rootless search path, or typo in filename Place name.container under ~/.config/containers/systemd/ or admin path; filename stem becomes service name
Generator never sees new file No reload after edit Run systemctl --user daemon-reload as the target user
Nothing starts until login Linger disabled sudo loginctl enable-linger USER and confirm Linger=yes
Service does not join default boot target Missing install section Add [Install] / WantedBy=default.target to the Quadlet source
systemctl --user cannot connect No session / missing XDG_RUNTIME_DIR Log in with a real session; avoid bare su
Permission errors after group change Stale supplementary groups on user manager loginctl terminate-user and log in again

Pull user-unit logs last — they usually name the real fault:

bash
journalctl --user -u web.service --no-pager -n 20

Generator syntax errors and missing images often appear here. For cross-cutting Quadlet failures (wrong section headers, unsupported keys), see Podman Quadlet troubleshooting.


References


Summary

Rootless Podman Quadlet moves unit files into your home config tree (or administrator paths under /etc/containers/systemd/users/) and hands lifecycle to systemctl --user instead of PID 1. The lab web.container example shows the full loop: create the file, daemon-reload, start the service, and read logs with journalctl --user — all without sudo.

The mistake I see most often is reaching for sudo systemctl or sudo podman after a rootless unit misbehaves. Those tools query the system manager and rootful store; your Quadlet lives in the user manager tied to one account. Match the user for both systemd and Podman commands.

Linger is the other misunderstood knob. You do not need it to start a container during an active session. Enable linger only when the workload must survive logout or start at boot without a login. Add [Install] WantedBy=default.target in the Quadlet source so the generator wires the unit to default.target, reload the user manager, and verify with loginctl show-user.

For production hardening, remember rootless storage paths, low-port publishing limits, and supplementary groups frozen at user-manager start. When boot persistence still fails after linger and install sections look correct, user journals usually spell out the next fix — and generic generator issues belong in the dedicated troubleshooting guide.


Frequently Asked Questions

1. Do I need loginctl enable-linger to use rootless Podman Quadlet?

No. While you have an active login session and a running user systemd manager, systemctl --user start works without linger. Enable linger only when the service must start at host boot without interactive login or keep running after your final logout.

2. Why does sudo systemctl status not find my Quadlet service?

Rootless Quadlet units live in the per-user systemd manager, not PID 1. Use systemctl --user and journalctl --user under the same Linux account that owns the .container file. sudo systemctl queries the system manager and sudo podman lists rootful containers only.

3. Where should I put a rootless Quadlet .container file?

The usual path is ~/.config/containers/systemd/. Administrators can also drop units in /etc/containers/systemd/users/ for all users or /etc/containers/systemd/users/UID/ for one account. After any change, run systemctl --user daemon-reload as that user.

4. Why does my rootless Quadlet service not start at boot?

Confirm the file has an [Install] WantedBy= default.target section, run systemctl --user daemon-reload, enable linger for that user with loginctl enable-linger, and inspect journalctl --user -u UNIT. Without linger, the user manager may not exist until someone logs in.

5. Can I make a rootful Quadlet rootless with User= in the Service section?

No. User=, Group=, and DynamicUser= in [Service] do not move a rootful unit into another account user manager. Place the Quadlet under a rootless search path and manage it with systemctl --user as that Linux user.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)