Podman
Run Prelude Collector under Podman with Quadlet/systemd units or a Kubernetes manifest via podman kube play, including SELinux and rootless notes.
Prelude Collector runs unchanged on Podman. Unlike a single-container app, the collector is a three-service stack — PostgreSQL, NATS, and the collector itself — so the examples below bring up all three. There are two supported deployment options:
- Quadlet + systemd
(recommended) - declarative
.containerunits that systemd manages directly: start at boot, ordering, restart policy, journal logging. podman kube play- a Kubernetes Pod manifest played by Podman. The same manifest is also the starting point for a real Kubernetes cluster, so one file covers both.
What about Compose? The Docker Compose template does run under
podman compose, but the Podman project does not recommend Compose for deployments -podman composeis only a thin wrapper that delegates to an external provider (docker-composeor the communitypodman-compose). Use one of the two options below; if your team standardizes on Compose, run it under Docker as described in Docker.
Prerequisites
| Requirement | Version | Notes |
|---|---|---|
| Podman | 4.4 or later | First version with Quadlet and the podman kube play name. |
| CPU / RAM | 2 vCPU / 2 GB | Same sizing as Docker; scale up with subscription count. |
| Disk | ~20 GB free | Storage volume, YANG cache, and the PostgreSQL data directory. |
The collector image is published to
registry.arolo-solutions.com/prelude/prelude-collector and is
publicly pullable — no registry login required. The examples below use
the :v1.0.0 tag, the current stable release:
podman pull registry.arolo-solutions.com/prelude/prelude-collector:v1.0.0
A standalone deployment publishes the web UI and REST API on 443
(HTTPS, mapped to 4030 inside the container). 443 is a privileged
port, so the examples below run rootful — see
Rootless for the alternative.
Option 1 - Quadlet + systemd (recommended)
Quadlet turns declarative unit files into systemd services at
daemon-reload. The stack is four units — a shared network, PostgreSQL,
NATS, and the collector — all placed under
/etc/containers/systemd/. Create the data directories first:
sudo mkdir -p /var/lib/prelude-collector/storage
sudo mkdir -p /var/lib/prelude-collector/pgdata
Network
Create /etc/containers/systemd/prelude.network:
[Network]
NetworkName=prelude
PostgreSQL
Then /etc/containers/systemd/prelude-postgres.container:
[Unit]
Description=Prelude Collector - PostgreSQL
After=network-online.target
Wants=network-online.target
[Container]
ContainerName=prelude-postgres
Image=docker.io/library/postgres:16-alpine
Environment=POSTGRES_USER=prelude
Environment=POSTGRES_PASSWORD=change-me-sE43kapqD8df5fds
Environment=POSTGRES_DB=collector
Volume=/var/lib/prelude-collector/pgdata:/var/lib/postgresql/data:Z
Network=prelude.network
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
NATS
Then /etc/containers/systemd/prelude-nats.container:
[Unit]
Description=Prelude Collector - NATS
After=network-online.target
Wants=network-online.target
[Container]
ContainerName=prelude-nats
Image=docker.io/library/nats:2
Network=prelude.network
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
Collector
Finally /etc/containers/systemd/prelude-collector.container:
[Unit]
Description=Prelude Collector
After=prelude-postgres.service prelude-nats.service
Wants=prelude-postgres.service prelude-nats.service
[Container]
ContainerName=prelude-collector
Image=registry.arolo-solutions.com/prelude/prelude-collector:v1.0.0
PublishPort=443:4030
Volume=/var/lib/prelude-collector/storage:/app/storage:Z
Environment=COLLECTOR_DB_HOST=prelude-postgres
Environment=COLLECTOR_DB_PORT=5432
Environment=COLLECTOR_DB_USER=prelude
Environment=COLLECTOR_DB_PASSWORD=change-me-sE43kapqD8df5fds
Environment=COLLECTOR_DB_NAME=collector
Environment=COLLECTOR_APP_URL=https://collector.example.com
Environment=COLLECTOR_TIMEZONE=Europe/Paris
Environment=COLLECTOR_LOG_LEVEL=INFO
Network=prelude.network
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
The :Z suffix relabels the bind mounts on SELinux-enforcing hosts —
see SELinux and volume mounts. The
COLLECTOR_DB_PASSWORD must match the PostgreSQL unit's
POSTGRES_PASSWORD; change both for any non-evaluation deployment. To
serve with your own certificate instead of the auto-generated
self-signed one, place the files under
/var/lib/prelude-collector/storage/certs/ and add:
Environment=COLLECTOR_TLS_CERT=/app/storage/certs/collector.crt
Environment=COLLECTOR_TLS_KEY=/app/storage/certs/collector.key
If you front the collector with a reverse proxy, publish 4030:4030
instead of 443:4030 and terminate TLS at the proxy.
Manage with systemd
# Pick up new unit files — Quadlet generates a .service per .container
sudo systemctl daemon-reload
# Start the stack
sudo systemctl start prelude-postgres prelude-nats prelude-collector
# Status and logs
sudo systemctl status prelude-collector
sudo journalctl -u prelude-collector -f
Quadlet-generated units are enabled through their [Install]
section — there is no separate systemctl enable step; they start at
boot as soon as the files exist. The collector's After=/Wants= lines
order it behind PostgreSQL and NATS, and Restart=always lets it retry
until PostgreSQL is accepting connections on the first boot.
First-run setup
On first boot the schema migrates automatically and a default admin
(admin / @rolo!Pass246) is seeded. Open the web UI on the published
HTTPS port, sign in, and change the password when prompted.
NATS is not configured through an environment variable. Once the stack is up, open Output Settings → NATS in the web UI, set the URL to the NATS container on the shared network, and enable it:
nats://prelude-nats:4222
That single connection powers both data export and the collector's internal signaling (ICMP reachability, OneBoard device-sync). Then issue an API token for the REST API:
sudo podman exec prelude-collector ./prelude-collector user token \
-u admin -d api-token
Updating
Edit Image= in prelude-collector.container to the new tag, then:
sudo systemctl daemon-reload
sudo systemctl restart prelude-collector
The schema migrates automatically on startup. To track a moving tag
automatically, add AutoUpdate=registry to the [Container] section
and enable the podman-auto-update.timer.
Option 2 - podman kube play
podman kube play runs a Kubernetes manifest directly under Podman. A
single Pod holds all three containers; because they share the Pod's
network namespace, they reach each other on localhost rather than
by container name. Save this as
/etc/prelude-collector/prelude-collector.yaml:
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: prelude-collector-storage
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: prelude-collector-pgdata
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
name: prelude-collector
labels:
app: prelude-collector
spec:
containers:
- name: postgres
image: docker.io/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: prelude
- name: POSTGRES_PASSWORD
value: change-me-sE43kapqD8df5fds
- name: POSTGRES_DB
value: collector
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
- name: nats
image: docker.io/library/nats:2
- name: collector
image: registry.arolo-solutions.com/prelude/prelude-collector:v1.0.0
ports:
- containerPort: 4030
hostPort: 443
env:
- name: COLLECTOR_DB_HOST
value: localhost
- name: COLLECTOR_DB_PORT
value: "5432"
- name: COLLECTOR_DB_USER
value: prelude
- name: COLLECTOR_DB_PASSWORD
value: change-me-sE43kapqD8df5fds
- name: COLLECTOR_DB_NAME
value: collector
- name: COLLECTOR_APP_URL
value: https://collector.example.com
- name: COLLECTOR_TIMEZONE
value: Europe/Paris
- name: COLLECTOR_LOG_LEVEL
value: INFO
volumeMounts:
- name: storage
mountPath: /app/storage
volumes:
- name: storage
persistentVolumeClaim:
claimName: prelude-collector-storage
- name: pgdata
persistentVolumeClaim:
claimName: prelude-collector-pgdata
Because everything shares one Pod, COLLECTOR_DB_HOST=localhost and the
UI NATS URL becomes nats://localhost:4222 — not the container names
used in the Quadlet option.
Play and manage
# Bring the pod up
sudo podman kube play /etc/prelude-collector/prelude-collector.yaml
# Logs
sudo podman pod logs -f prelude-collector
# Tear down (the PVC-backed volumes survive)
sudo podman kube down /etc/prelude-collector/prelude-collector.yaml
To update, change the image tag in the manifest, then
podman kube down and podman kube play again.
Start at boot with a .kube unit
Quadlet also manages kube manifests, which gives the kube play option
the same systemd integration as option 1. Create
/etc/containers/systemd/prelude-collector.kube:
[Kube]
Yaml=/etc/prelude-collector/prelude-collector.yaml
[Service]
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start prelude-collector
On a Kubernetes cluster
The manifest is a starting point for a real cluster, with a few adjustments that replace the host-specific pieces:
- The
hostPortbecomes a Service. The collector terminates TLS itself, so expose4030through a TLS-passthrough Ingress or aLoadBalancerService. - Move
COLLECTOR_DB_PASSWORD/POSTGRES_PASSWORDinto a Secret rather than inlinevalue:fields. - Run the stateful pieces single-replica. The PostgreSQL data
directory and the collector
storage/volume must never be shared between two running instances — if you wrap them in a Deployment, use theRecreatestrategy, or split PostgreSQL into its own StatefulSet.
SELinux and volume mounts
SELinux is enforcing on AlmaLinux, RHEL, Rocky, and Fedora by default.
Bind mounts need the :Z suffix so Podman relabels them for the
container's SELinux context — the Quadlet units above already carry it.
Without the label the containers cannot write into their volumes and the
PostgreSQL data directory or the collector storage/ fails to
initialize on first boot.
podman kube play does not take mount suffixes; PVC-backed volumes (as
in the manifest above) are labelled automatically. If you swap a PVC for
a hostPath volume, relabel the directory on the host instead:
sudo chcon -Rt container_file_t /var/lib/prelude-collector/storage
sudo chcon -Rt container_file_t /var/lib/prelude-collector/pgdata
Rootless
Both options also run rootless (units under
~/.config/containers/systemd/, systemctl --user,
loginctl enable-linger $USER to survive logout). The collector's
in-container port 4030 is unprivileged, so a rootless deployment that
publishes 4030:4030 and sits behind a reverse proxy needs no extra
setup. To publish 443 directly as in the examples above, lower the
unprivileged port threshold on the host:
sudo sysctl net.ipv4.ip_unprivileged_port_start=443
Persist it in /etc/sysctl.d/ if you keep the stack rootless, or run
the containers rootful and skip the sysctl.
Podman vs Docker quick reference
| Operation | Docker | Podman |
|---|---|---|
| Start stack | docker compose up -d |
systemctl start prelude-* (Quadlet) |
| View logs | docker compose logs -f |
journalctl -u prelude-collector -f |
| Exec into container | docker compose exec collector sh |
podman exec -it prelude-collector sh |
| Pull images | docker compose pull |
podman pull … |
| Auto-start at boot | Docker daemon enabled | Quadlet [Install] section |
| Rootless by default | No | Yes |
| SELinux volume labels | Not needed | :Z on bind mounts |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Unit prelude-collector.service not found |
File named .service instead of .container — Quadlet ignores it |
Rename to prelude-collector.container, then systemctl daemon-reload |
| Unit not found after adding a file | systemd has not regenerated units | systemctl daemon-reload, then check /run/systemd/generator/ |
| Quadlet file rejected | Syntax error in the unit | /usr/libexec/podman/quadlet -dryrun prints the parse errors |
| Collector errors on DB connect at boot | PostgreSQL not ready yet, or password mismatch | Restart=always retries; confirm COLLECTOR_DB_PASSWORD matches POSTGRES_PASSWORD |
| Storage / pgdata write errors on first boot | SELinux blocking the bind mount | Add :Z to the Volume= line, or chcon -Rt container_file_t for hostPaths |
| No outputs or device reachability data | NATS URL not set in the UI | Open Output Settings → NATS, set nats://prelude-nats:4222 (or nats://localhost:4222 for kube play), enable it |
bind: permission denied on 443 |
Rootless privileged-port limit | Lower ip_unprivileged_port_start, publish 4030:4030, or run rootful |
Air-gapped environments
For an air-gapped Podman host, transfer the images with podman save
and podman load. See the
Air-gapped install tutorial for the
step-by-step, and Air-gap deployment for the operational
reference.
See also
- Docker deployment - same image, Docker specifics and the Compose template.
- Configuration reference - every
COLLECTOR_*environment variable. - Air-gap deployment - operational shape of an air-gapped install.
- Air-gapped install tutorial - step-by-step walkthrough.