sysadmin · July 20, 2026 · 15 min read

Rootless podman.
run, mount, map ports, keep SELinux on.

The dev team’s stack is moving to containers. Stand up rootless nginx and redis under an unprivileged user, with a bind-mount, a persistent volume, and SELinux staying enforcing.

// what we’re getting into
  1. The ticket
  2. Concept review. Why podman, and the run flags.
  3. Switch to the container user and pull
  4. Run nginx with a bind-mount and port map
  5. Run redis with a persistent volume
  6. Inspect, exec, logs, lifecycle
  7. The two pitfalls: :Z and privileged ports
  8. Exam questions
  9. Final checklist: confirm everything works

Phase 3 starts here: the dev team’s stack moves into containers, and cuser1 wants the first two stood up. An nginx serving the same /srv/webcontent that has followed us since the shared-folder ticket on day three, with host port 8080 mapped to the container’s 80, and a redis named dev-cache with its data on a real volume at /srv/cuser1/redis-data. Both run rootless as the unprivileged cuser1, no root containers anywhere, and SELinux stays enforcing.

Two things bite everyone on rootless podman, and this ticket walks straight into both. A bind-mount without the SELinux relabel flag hands the container a directory it is not allowed to read, so nginx 403s. And a rootless user cannot bind a port under 1024, so anyone who reflexively maps port 80 gets permission denied. Map above 1024, remember the :Z, and the rest is easy.

The ticket

bash
id cuser1
sudo dnf install -y podman container-tools
podman --version

Concept review. Why podman, and the run flags.

bash
man podman-run

On RHEL, podman is rootless by default, so each user has their own container store, and there is no daemon, each container is a child of podman itself. The CLI matches docker closely. The run flags you use constantly are below.

podman run flags
-d               detached, run in the background
--name NAME      name the container
-p host:cont     port map, e.g. -p 8080:80
-v host:cont:Z   volume mount; :Z relabels for SELinux
-e KEY=VAL       environment variable
-it              interactive with a TTY
--rm             auto-remove on exit

Switch to the container user and pull

enable-linger lets the user’s services keep running when they are logged out, which you will need for boot persistence later. Then work as the user and pull the images by their fully qualified names, since podman does not assume Docker Hub.

bash
sudo loginctl enable-linger cuser1
sudo -iu cuser1
podman info | head -20    # confirms rootless

podman pull docker.io/library/nginx:latest
podman pull docker.io/library/redis:latest
podman images

Run nginx with a bind-mount and port map

The :Z on the volume is the part that matters. It tells podman to relabel the host directory with a private SELinux container label so the container can read it. Leave it off and SELinux denies access, giving you a 403.

bash
podman run -d \
    --name dev-web \
    -p 8080:80 \
    -v /srv/webcontent:/usr/share/nginx/html:ro,Z \
    nginx:latest

podman ps
curl http://localhost:8080/

Run redis with a persistent volume

The volume keeps the data across container restarts, and :Z again handles SELinux.

bash
mkdir -p /srv/cuser1/redis-data
podman run -d \
    --name dev-cache \
    -v /srv/cuser1/redis-data:/data:Z \
    -p 6379:6379 \
    redis:latest redis-server --appendonly yes

podman ps
podman logs dev-cache | tail

Inspect, exec, logs, lifecycle

bash
podman inspect dev-web | head -50
podman inspect dev-web --format '{{.NetworkSettings.IPAddress}}'
podman stats --no-stream

podman exec -it dev-cache redis-cli ping    # PONG
podman exec -it dev-web bash                 # a shell inside, then exit

podman logs --tail 20 dev-web
podman stop dev-web dev-cache
podman start dev-web dev-cache
podman rm -f dev-web dev-cache

The two pitfalls: :Z and privileged ports

These are the failures worth reproducing once so you recognize them instantly.

bash
# forgot :Z, SELinux denies the mount, nginx 403s
podman run -d --name broken -v /srv/webcontent:/usr/share/nginx/html:ro nginx:latest
podman logs broken | tail    # permission errors
podman rm -f broken          # fix by re-running with :Z

# a privileged port under 1024, rootless, is denied
podman run -d --name p80 -p 80:80 nginx:latest    # permission denied
# use a port >= 1024, or raise net.ipv4.ip_unprivileged_port_start

Exam questions

Write the command first.

Q1. Pull the latest httpd image from Docker Hub and list local images.

Q2. Run an httpd container named webserver detached, mapping host 8080 to container 80, with a read-only bind-mount from /srv/webcontent.

Q3. Run a rootless redis named cache that survives reboot when the user is not logged in, volume at ~/redis-data, port 6379.

Answers.

bash
# A1
podman pull docker.io/library/httpd:latest; podman images
# A2 (httpd content path differs from nginx)
podman run -d --name webserver -p 8080:80 \
    -v /srv/webcontent:/usr/local/apache2/htdocs:ro,Z httpd:latest
# A3: linger + run + generate a systemd user unit
sudo loginctl enable-linger cuser1
podman run -d --name cache -p 6379:6379 -v ~/redis-data:/data:Z redis:latest
podman generate systemd --new --name cache --files
systemctl --user enable --now container-cache.service

Final checklist: confirm everything works

If every check passes, the ticket is done.

bash
# 1. cuser1 has linger enabled
loginctl show-user cuser1 | grep -i linger

# 2. dev-web on 8080, dev-cache on 6379, both rootless
podman ps

# 3. volume mounts used :Z, SELinux still enforcing
getenforce

# 4. all three exam commands written from scratch
# 5. tracker entry checked off

Reply to cuser1: both containers run rootless under cuser1. nginx on 8080 with the /srv/webcontent bind-mount (read-only, :Z), redis with a persistent volume at /srv/cuser1/redis-data. Next: wire them into systemd for boot persistence.

next post
Containers that survive reboot: systemd units and pods