Introduction
If you have a simple container service that doesn't justify an orchestrator (k8s / Docker Swarm), but docker compose up -d feels too non-production, Podman Quadlet is worth a check. Quadlet lets you manage your container resources as systemd unit files; you write a declarative .container file, and systemd runs it like any other service.
That's probably all you will find if you don't take a deep dive into the official docs. This post investigates what actually happens when you drop that file in place. We'll trace how Podman turns a .container into a real systemd .service, where that generated unit lives, why it's transient, and why those mechanics dictate how you start it and keep it running.
- Prerequisites
- Today's Scope
- How It Works
- Creating Our Quadlet File
- Running the Service
- The Big Gotcha
Prerequisites
- A Linux-based VM (this post uses Ubuntu)
- Podman >=v4.4.0 installed
- A container image to run
Today's Scope
There are many types of Quadlet files, but today we are only covering the rootless/user .container type.
The above image is taken from the official doc
How It Works
A .container file looks like a systemd unit, but, well, it's not. The gap between "the file you write" and "the unit systemd runs" is our focus today, so let's walk it before reaching for copy-and-paste.
Quadlet Files to systemd Unit Files
Quadlet files use the same format as systemd unit files but they aren't valid units on their own. Keys such as Image= and PublishPort= are Podman specific. Podman uses a systemd unit generator to convert the Quadlet files into standard systemd .service files.
User Manager (rootless)
When you use systemctl --user, the per-user manager runs /usr/lib/systemd/user-generators/podman-user-generator, which converts your Quadlet files into .service units and drops them in $XDG_RUNTIME_DIR/systemd/generator/. Because this manager runs as your UID, the containers it launches are rootless.
System Manager (rootful)
When you use systemctl (no --user), the system manager (PID 1) runs /usr/lib/systemd/system-generators/podman-system-generator, with output in /run/systemd/generator/. Because this manager runs as root, the containers it launches are rootful.
Note: Both generators are symlinks to /usr/libexec/podman/quadlet (path may vary by distro)
Read more about systemd generator output directories
Creating Our Quadlet File
With the mechanics out of the way, let's create a .container Quadlet:
[Container]
# the container name shown when running podman ps
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#containername
ContainerName=my-container
# the file that contains all the needed env vars
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#environmentfile
EnvironmentFile=my-env-file
# The image to run
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#image
Image=my-image:latest
# port mapping (hostPort:containerPort)
# note that rootless units can't bind to privilege ports
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#id30
PublishPort=3000:3000/tcp
[Service]
# restart policy
Restart=always
[Install]
# starts on boot
WantedBy=default.target
For the complete list of keys for .container unit files please refer to the official doc
For rootless/user units, the podman systemd generator reads from $XDG_CONFIG_HOME/containers/systemd/ or ~/.config/containers/systemd/ if $XDG_CONFIG_HOME is unset. We can save the above file as ~/.config/containers/systemd/my-service.container
Please refer to the official doc for the comprehensive list of rootless unit search paths.
Running the Service
Since it's a new unit, we need to tell our user systemd manager to reload. For a Quadlet unit, daemon-reload re-runs the generators (regenerating the .service from our .container) and reloads units from disk:
systemctl --user daemon-reload
The --user flag points systemctl at the per-user systemd manager which is running as your UID, not the system manager (PID 1). Note this picks which manager runs the unit; it's a separate thing from whether the container itself is rootless.
Start the service:
systemctl --user start my-service
Why systemctl start and not enable like normal systemd units? The systemd units Quadlet generates are transient; they live in $XDG_RUNTIME_DIR/systemd/generator/ (user) or /run/systemd/generator (system), and both are tmpfs. The generator regenerates them every time it runs (at boot and each daemon-reload). systemd won't let you systemctl enable a transient/generated unit, so you can't enable it the usual way. Instead, the generator reads the [Install] section from your .container and creates a symlink in $XDG_RUNTIME_DIR/systemd/generator/default.target.wants/ (user) or /run/systemd/generator/default.target.wants/ (system) as part of that same generation pass. That's why WantedBy=default.target is what makes it start on boot, not enable.
If you try systemctl --user enable my-service you will get
Failed to enable unit: Unit /run/user/<uid>/systemd/generator/my-service.service is transient or generated.
systemd captures the service's stdout/stderr to the journal (via journald). To follow the logs:
journalctl --user -u my-service -f
Now you can open $XDG_RUNTIME_DIR/systemd/generator/my-service.service to check the generated systemd unit file:
# Automatically generated by /usr/lib/systemd/user-generators/podman-user-generator
#
# my-service.container
[X-Container]
ContainerName=my-container
EnvironmentFile=my-env-file
Image=my-image:latest
PublishPort=3000:3000/tcp
[Service]
Restart=always
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/usr/bin/podman rm -v -f -i --cidfile=%t/%N.cid
ExecStopPost=-/usr/bin/podman rm -v -f -i --cidfile=%t/%N.cid
Delegate=yes
Type=notify
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/usr/bin/podman run --name=my-container --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d --publish 3000:3000/tcp --env-file /home/ubuntu/.config/containers/systemd/my-env-file my-image:latest
[Install]
WantedBy=default.target
[Unit]
SourcePath=/home/ubuntu/.config/containers/systemd/my-service.container
RequiresMountsFor=%t/containers
You can see that an X- is prefixed to the Podman-specific [Container] section. That's systemd's way of saying ignore it.
The Big Gotcha
Here's the part that bites people on a remote VM: your systemd --user manager only exists while you have an active session. It starts on your first login and stops when your last session ends. When it does, your user services go with it. So if you start the service over SSH and then close the SSH session, the service dies with it.
To keep the user manager alive at boot with no session attached, you must enable user lingering:
# enable lingering for the current user
loginctl enable-linger
# verify
loginctl show-user "$USER" --property=Linger
Lingering is recorded under /var/lib/systemd/linger/ and survives reboots. Note that lingering doesn't make you "logged in"; it only controls whether your per-user systemd --user manager runs without an active session. For the service to actually come up on boot you still need the [Install] WantedBy=default.target section in your Quadlet file as noted in Running the Service and the official doc
For details, see the loginctl docs.














