Running Podman Quadlet on SELinux

Updated:

Originally, I ran servers using rootless Docker. The reason for not using rootful was primarily security. Rootful Docker can bypass iptables at will and access the entire system space without proper permission management, so I chose rootless. When transitioning to SELinux, I wanted to try Podman, which is designed specifically for rootless container operations.

Podman uses the same OCI images as Docker and supports most Docker commands. In this post, I’ll cover how to install Podman, how to use it with existing Docker Compose files, and how to use Quadlet, which is more native to Podman.

Installing Podman

Installation is very simple:

sudo dnf -y install podman

Two configurations are required:

  1. Configure subuid/subgid ranges
  2. Enable linger
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(id -un)"
sudo loginctl enable-linger "$(id -un)"

Now you can use Podman by default. If you want to manage data storage location, you can configure it per user or globally. I chose global configuration to allocate all Podman data space:

# /etc/containers/storage.conf
[storage]
driver = "overlay"
rootless_storage_path = "/path/to/your/podman/$USER"

You can’t use this data path directly; additional SELinux policy configuration is required:

sudo semanage fcontext -a -t container_var_lib_t "/path/to/your/podman/$USER(/.*)?"
sudo restorecon -Rv /path/to/your/podman/$USER

Now you can use Podman:

podman run --rm -it alpine:latest

Using Docker Compose

Now you can replace most operations with alias docker=podman, but Docker Compose can’t be used directly since there’s no native podman compose command. However, you can connect the Docker socket to the Podman socket, allowing Docker to use the Podman socket for Compose operations:

export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock

Then you’ll see that containers from Docker Compose files are executed as Podman containers.

Using Quadlet

Unfortunately, the above approach is not perfect either. Unlike Docker, Podman has no daemon, so containers don’t automatically restart when the system reboots. To solve this, Podman uses systemd services to manage containers. This is Quadlet.

Quadlet documents have four file extensions: network, volume, container, and kube. Since I don’t use k8s specifically, I’ll exclude the kube extension here.

First, let’s look at a complete file example to understand the rules. Personal systemd files are located at ~/.config/containers/systemd. Creating the following files there will set up a service that automatically runs Prometheus:

# monitoring.network
[Unit]
Description=Network for prometheus monitoring

[Network]
NetworkName=monitoring
Driver=bridge

[Install]
WantedBy=default.target

If you set NetworkName to match the network name from Docker Compose (specifically <directory>_<network_name>), you can reuse networks created with Docker Compose.

[Unit]
Description=Prometheus data volume

[Volume]
VolumeName=prometheus_data

[Install]
WantedBy=default.target

Similarly to Network, specifying VolumeName allows you to reuse volumes created with Docker Compose.

[Unit]
Description=Prometheus (Quadlet)

[Container]
Image=docker.io/prom/prometheus:latest
ContainerName=prometheus

PublishPort=127.0.0.1:9090:9090

Volume=prometheus.volume:/prometheus
Volume=/path/to/config/prometheus.yml:/etc/prometheus/prometheus.yml:ro,z

# Writing the network file name as-is automatically creates a Requires dependency
Network=monitoring.network

AddHost=host.docker.internal:host-gateway  
PodmanArgs=--cpus=0.5 --memory=512m

[Service]
TimeoutStartSec=900
Restart=always

[Install]
WantedBy=default.target

As shown above, you can configure basic port mappings, volume mappings, network mappings, host.docker.internal mappings, resource limits, etc. Note that for volume mappings, additional configuration is required according to SELinux policies. Add :Z if only a single container needs access, or :z if multiple containers need access. The path must also have the container_t permission.

Now reload the daemon and start the service. Since all operations are rootless, run at the user level:

systemctl --user daemon-reload
systemctl --user start prometheus
journalctl --user -xeu prometheus

Additional options for advanced use:

# Environment configuration
EnvironmentFile=/path/to/env.env
Environment=<ENV_NAME>=<ENV_VALUE>

# Health checks
HealthCmd=pg_isready -U "$POSTGRES_USER" -h 127.0.0.1
HealthInterval=2s
Notify=healthy

# Execution arguments
Exec= --path.procfs=/host/proc \
  --path.rootfs=/rootfs \
  --path.sysfs=/host/sys \
  --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)

# Keep user ID mapping
UserNS=keep-id