Podman Quadlet Services
Kubernetes is great but sometimes you just want to run one container on one VM in the simplest way possible. docker --restart always
is one way to do this but its pretty clunky:
- Container managed by docker not systemd
- Requires installing docker
- Completely different to Kubernetes
Podman and Systemd - dream team
Turns out podman has a systemd enabled, kubernetes compatible, service capability called quadlet.
Long story short, if you install podman on RHEL 8+ or Debian Trixie (13)+, you can create a systemd service for a container by defining:
- Pod - Kubernetes compatible
- Systemd service -
.kube
file - Volume/Extra files
systemctl daemon reload
, then enable and start your service with systemd
Worked Example - Nexus
Nexus is a binary repository server for pretty much anything you can think of. I need one in my homelab to ship artifacts to servers and it needs to be outside of Kubernetes so that it can be used for bootstrapping things inside the cluster.
Find an image
First step is to find an image. There’s one from Sonatype at https://hub.docker.com/r/sonatype/nexus3
Test the image a bit with podman run
if you like.
Write a pod definition
podman generate kube
will generate a Kubernetes compatible pod definition customized for the pod your generating against. I find it easiest to copy paste an existing file when there’s a new service to run
Example:
/etc/containers/systemd/nexus.yml
apiVersion: v1
kind: Pod
metadata:
annotations:
io.kubernetes.cri-o.ContainerType/app: container
io.kubernetes.cri-o.TTY/app: "false"
io.podman.annotations.autoremove/app: "FALSE"
io.podman.annotations.init/app: "FALSE"
io.podman.annotations.privileged/app: "FALSE"
io.podman.annotations.publish-all/app: "FALSE"
labels:
app: nexus
name: nexus
spec:
automountServiceAccountToken: false
containers:
- image: docker.io/sonatype/nexus3:latest
name: app
ports:
- containerPort: 1234
hostPort: 1234
- containerPort: 8081
hostPort: 8081
- containerPort: 8443
hostPort: 8443
resources: {}
securityContext:
capabilities:
drop:
- CAP_MKNOD
- CAP_NET_RAW
- CAP_AUDIT_WRITE
volumeMounts:
- mountPath: /nexus-data
name: nexus-data-vol
enableServiceLinks: false
hostname: nexus
restartPolicy: Never
volumes:
- hostPath:
path: /data/containers/nexus
type: Directory
name: nexus-data-vol
status: {}
Write a systemd service (.kube)
No generate
for this one so just copy paste from somewhere:
/etc/containers/systemd/nexus-pod.kube
[Install]
WantedBy=default.target
[Unit]
[Kube]
Yaml=/etc/containers/systemd/nexus.yml
# same MAC address for DHCP
# https://stackoverflow.com/a/78139833
PodmanArgs=--mac-address 2a:6c:0d:5e:a2:8f
PublishPort=8081:8081
PublishPort=1234:1234
PublishPort=8443:8443
Network=podman-vlan-infrastructure
Podman supports macvlan networking. The arguments in the kube file essentially plugin the pod to the network with its own network card and MAC address. Perfect for running network services (podman-vlan-infrastructure
was created separately, already).
Volume/Extra files
Last thing to setup is to create the volume listed in the pod definition:
mkdir -p /data/containers/nexus
The directory needs to be accessible by the nexus process which runs as uid 200
, gid 200
. These are used on the host systems so need to chown 200:200 /data/containers/nexus
to directly assign ownership.
Config files will appear in this directory once the service starts and can be directly edited as regular files from the host.
Enable in systemd
systemct daemon-reload
will cause podman quadlet to re-evaluate all mappings and map them to systemd services which can then be enabled and started just like any normal service:
systemctl enable nexus-pod
systemctl start nexus-pod
Logs are available with journalctl -u nexus-pod
- same as any other service.
Next steps
If you want to run more then a couple of services, its a good idea to automate the above steps. I wrote some quick ansible to do this based on the contents of dict
:
role
- name: kube file
ansible.builtin.copy:
dest: "/etc/containers/systemd/{{ pod.value.kube_file }}"
src: "systemd/{{ pod.value.kube_file }}"
owner: root
group: root
mode: '0700'
notify: reboot podman
- name: pod definition file
ansible.builtin.copy:
dest: "/etc/containers/systemd/{{ pod.value.pod_definition_file }}"
src: "systemd/{{ pod.value.pod_definition_file }}"
owner: root
group: root
mode: '0700'
notify: reboot podman
- name: "{{ pod.key }} files"
ansible.builtin.file:
path: "{{ item.path }}"
state: "{{ item.state }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop: "{{ pod.value.files | default([]) }}"
notify: reboot podman
- name: "{{ pod.key }} cp"
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop: "{{ pod.value.cp | default([]) }}"
notify: reboot podman
- name: "{{ pod.key }} service"
ansible.builtin.systemd_service:
state: started
daemon_reload: true
name: "{{ pod.key }}"
enabled: true
Host variables
quadlet_services:
nexus-pod:
kube_file: nexus-pod.kube
pod_definition_file: nexus.yml
files:
- path: /data/containers/nexus
state: directory
owner: 200
group: 200
mode: '0700'
Summary
I can’t think of a better way to run containerized services outside of Kubernetes in 2025. Having a kubernetes compatible pod definition means you have a solid starting point to migrate a service to Kubernetes proper in the future too, if needed.
The long version
Take a look at Repeatable container deployments with Podman in Linux - Into the Terminal 132 from Nate Lager at Red Hat for the gory details.