On Fedora the core Linux steps are the same, with three differences. SELinux enforces file contexts, so an agent under /srv may be denied until its files carry the right label; firewalld is on by default, so nothing is reachable until you allow it; and Podman quadlets run the agent as a systemd-managed container from one unit file. Expect a new release every six months.
Part of the Always-on agents series, post 18 of 33.
Third distribution guide, same rule as the others: I am not repeating the Linux core page. The user, the virtual environment, the unit with a restart policy, the timer, the secrets file, the watchdog are all identical on Fedora. What follows is only what is different, and on Fedora the differences are not small. One of them stopped my service dead in a way the other distributions never would.
The release cycle, and why it matters for a box you forget about
Fedora ships a release roughly every six months and supports each one until about four weeks after the release two versions later, which works out to about thirteen months. Fedora 43 came out on 28 October 2025 and reaches end of life around 9 December 2026; Fedora 44 arrived on 28 April 2026. For a laptop that is fine. For an agent host it means a major upgrade every year, on a machine you were trying not to touch. I would still run Fedora for an agent, because of what follows, but I would put the upgrade in the calendar the day I set the box up, and I would not pick it for a box I intend to ignore for three years; that is what Debian is for.
Fedora 43 container$ . /etc/os-release; echo "$PRETTY_NAME"
Fedora Linux 43 (Container Image)Packages: dnf, and versions that are current
Where Debian's problem is old packages, Fedora's is the opposite: everything is recent, which an agent framework likes. I installed the runtime, Podman, firewalld and the SELinux policy tools in one go:
Terminal$ dnf -y install python3 nodejs podman firewalld policycoreutils-python-utils
$ python3 --version; node --version; podman --version
Python 3.14.8
v22.22.2
podman version 5.8.8Compare that with Debian 13's Python 3.13 and Node 20. Node 22 still falls short of OpenClaw's stated minimum of 24.16, so Fedora does not get you out of the Node question either; Python 3.14 is exactly what Hermes Agent's first-party builds run on. The dnf step itself took a long time in my container, because Podman and firewalld pull in a good deal, and I would expect the same on a small VM.
The policycoreutils-python-utils package is the one most people skip and then need at two in the morning: it provides semanage and audit2allow, the tools for the section below.
SELinux: the thing that stops your service and tells you nothing
Here is the failure, as it happens on a real Fedora install. You follow the core page, put the agent under /srv/agent, write the unit, enable it, and it fails to start or fails on its first file write. The unit is correct. journalctl shows a permission error that makes no sense because the file permissions are right. That is SELinux: every file has a security context, systemd runs your service in a confined domain, and a file created by copying or by a user with the wrong context is not allowed. The three commands that sort it out:
Terminal# 1. is it SELinux? look for AVC denials since the last boot
ausearch -m avc -ts recent
# 2. see what contexts the agent's files actually carry
ls -Z /srv/agent
# 3. fix the common case: files copied in with the wrong label
restorecon -Rv /srv/agent
# if the agent needs a non-standard port, tell SELinux about it (example: 18789)
semanage port -a -t http_port_t -p tcp 18789
# last resort, and read what it generates first: a local policy module from the denials
ausearch -m avc -ts recent | audit2allow -M agent-local
semodule -i agent-local.ppIn my container SELinux was not merely permissive, it was disabled, which is true of containers in general: the policy belongs to the host kernel, not the container. So I could install the tools and run them, but I could not reproduce the denial, and the file listing shows why, with no label on anything:
Terminal$ getenforce
Disabled
$ sestatus | head -1
SELinux status: disabled
$ ls -Z /srv/agent
? venvTwo habits that avoid most of this. Keep the agent under a path SELinux already understands for services, or run it as a container through a quadlet, where the policy is already written. And never answer a denial by switching SELinux to permissive on an always-on box. The agent holds credentials and runs unattended; the thing saying no is the thing you want.
firewalld: on by default, which is what you want
On Ubuntu you install ufw and switch it on. On Fedora firewalld is already running, with a default zone that allows little inbound beyond SSH. For an agent that only talks outward, you have nothing to do, and that is the right answer. If the agent has a dashboard, as OpenClaw does on 18789, I would still not open it; I would reach it over a private network. But if you must:
Terminal$ firewall-cmd --state
not running # in my container; on a real install this says running
$ firewall-cmd --permanent --add-port=18789/tcp --zone=FedoraServer # only if you truly must
$ firewall-cmd --reloadThat first line is a container artefact: firewalld was installed but not started, because nothing enables services inside an image. On a Fedora Server or Workstation install it is running from first boot, which is the state the rest of this section assumes.
Podman quadlets: the Fedora-native way to run an agent
This is the part I think Fedora does best. A quadlet is a small systemd unit file with a [Container] section; Podman's systemd generator turns it into a real service at boot or on daemon-reload. You get the container's isolation and the agent's whole runtime pinned to an image, with systemd's restart policy and journal on top, and no Docker daemon running as root. For rootful units the file lives in /etc/containers/systemd/; for a user's own agent, ~/.config/containers/systemd/ with lingering on, exactly as on Debian.
/etc/containers/systemd/agent.container[Unit]
Description=Always-on agent in a container
[Container]
Image=docker.io/library/python:3.12-slim
Exec=python -c "import time;print('heartbeat',flush=True);time.sleep(3600)"
EnvironmentFile=/etc/agent.env
[Service]
Restart=always
[Install]
WantedBy=multi-user.targetThe file name becomes the unit name, so agent.container produces agent.service and a container called systemd-agent. Image is the only required key. Before trusting it, ask the generator what it would produce, which also shows any error that would otherwise appear as a confusing "unit not found":
Terminal$ /usr/lib/systemd/system-generators/podman-system-generator --dryrun
quadlet-generator[309]: Loading source unit file /etc/containers/systemd/agent.container
---agent.service---
[Unit]
Wants=network-online.target
After=network-online.target
Description=Always-on agent in a container
SourcePath=/etc/containers/systemd/agent.container
RequiresMountsFor=%t/containers
[X-Container]
Image=docker.io/library/python:3.12-slim
Exec=python -c "import time;print('heartbeat',flush=True);time.sleep(3600)"
EnvironmentFile=/etc/agent.env
[Service]
Restart=always
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/usr/bin/podman rm -v -f -i systemd-%N
ExecStopPost=-/usr/bin/podman rm -v -f -i systemd-%N
Delegate=yes
Type=notify
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/usr/bin/podman run --name systemd-%N --replace --rm --cgroups=split --sdnotify=conmon -d --env-file /etc/agent.env docker.io/library/python:3.12-slim python -c "import time;print('heartbeat',flush=True);time.sleep(3600)"
[Install]
WantedBy=multi-user.target
$ systemctl daemon-reload && systemctl list-unit-files agent.service
UNIT FILE STATE PRESET
agent.service generated -That is the whole value of a quadlet in one screen. Eleven lines of mine became a complete service: systemd waits for the network, Podman runs the container with notify support so systemd knows when it is really up, the container is removed on stop and replaced on start, and the secrets file is passed through as an environment file. The unit shows as generated, which is the state you want to see; if it is missing, the generator rejected the file and the dry run above says why.
Then systemctl enable --now agent.service as for any unit, and journalctl -u agent.service for the heartbeat. The agent's secrets go in the EnvironmentFile, owned by root and readable by nobody else, exactly as the core page does for a plain service.
What I would do in order
This is the order I ran it in, with the SELinux step placed where it bites on a real install.
- Install the runtime, Podman, firewalld and the SELinux tools together. One dnf command. Do not skip policycoreutils-python-utils.
- Decide: plain service or quadlet. Plain service if the agent is a Python script you own. Quadlet if it is a published image or you want the runtime pinned.
- Create the user and the environment as on the core page. Then run restorecon on the agent's directory before the first start, so the contexts are right from the beginning.
- Write the unit or the quadlet, dry-run the generator, verify, enable. systemd-analyze verify for a plain unit; the generator's --dryrun for a quadlet.
- When it fails for no reason, read ausearch before anything else. If it is an AVC denial, restorecon, then semanage for ports, then audit2allow as the last resort. Never permissive.
- Put the next Fedora upgrade in your calendar now. Thirteen months of support means the box will need you once a year.
Sources
- endoflife.date, Fedora Linux release and support dates (accessed October 20, 2026)
- Podman docs, podman-systemd.unit: systemd units using Podman Quadlet (accessed October 20, 2026)
- Linuxiac, Fedora 43 final build approved, official release set for October 28 (accessed October 20, 2026)
Written by Lera Leonteva. Commands and outputs are from a fresh Fedora 43 container with systemd on the access date; SELinux was not enforcing there.