Skip to main content

Command Palette

Search for a command to run...

Rootless Containerization

With Redhat Podman

Published
•6 min read•View as Markdown
P

As a associate system administrator I worked on Redhat Linux servers, including user management, permissions, services, and performance monitoring Automated routine administrative tasks using Bash scripting and cron jobs, reducing manual effort by ~30% I am aws certified sysops administrator and Google Certified Cloud Engineer. Determined to transition my career into cloud architect /Cloud Support role

Today we are tackling the single most confusing topic in modern Redhat Linux System Administration.

We are going to deploy a containerized service. Sounds easy, right? podman run and you're done?

Wrong.

Today, you must deploy a Rootless Container that integrates fully with the host system.

This changes the rules of everything: networking, file permissions, and startup behavior. If you miss one flag, it fails silently.

The "Secure" Web Server

You need to deploy an Nginx web server container.

The Constraints(Rules) :

  1. Zero Privilege: The container must run as the user web_dev. You are NOT allowed to run Podman as root/sudo.

  2. The Data: It must serve files located on the host at /home/web_dev/html.

  3. The Persistence: The container must start automatically when the server boots, even if the user web_dev never logs in.

The Map:

Rootless containers break standard rules. Use these maps to find your way back:

  • man podman-generate-systemd (Or man podman-systemd.unit for Quadlets)

  • man loginctl (The secret to reboot survival)

  • man podman-run (Look for the :Z option)

Your Architect Challenge:

You need to navigate 4 specific traps to make this work. How do you solve them?

Trap 1: The Network Block

Nginx wants to listen on port 80 inside the container. You map it to host port 80 (-p 80:80). It fails immediately "Permission Denied."

  • Why? And what port must you use on the host side to fix this without becoming root?

Trap 2: The SELinux Wall

You map the volume: -v /home/web_dev/html:/usr/share/nginx/html. The container starts, but Nginx gives a 403 Forbidden because SELinux blocked access to the host files.

  • The Fix: What one letter must you add to the end of your volume string to tell SELinux to automatically relabel the content for the container? (e.g., :?)

Trap 3: The Ghost Reboot

You generated the Systemd unit file. You enabled it with systemctl --user enable container-nginx. You reboot the server. The container does not start. It only starts when you SSH in as web_dev.

  • The Fix: What loginctl command must you run to allow this user's services to start at boot without an active session?

Trap 4: The Firewall (Bonus)

The container is running on a high port (e.g., 8080). You opened port 8080 in firewall-cmd, but external users still can't connect.

  • The Fix: Since rootless containers use slirp4netns/pasta, they don't always hit the standard tables. How do you verify the container is actually bound to the public interface?
  • Solutions:

This is a classic RHEL/Fedora scenario where the intersection of User Namespaces, SELinux, and Systemd creates a "perfect storm" for administrators.

Here is the step-by-step resolution to the Architect Challenge.

Trap 1: The Network Block

The Why: In Linux, ports below 1024 are considered "privileged ports." A rootless user (like web_dev) does not have the authority to bind to these ports on the host network interface. The Fix: You must use a port number 1024 or higher on the host side.

  • Recommended Port: 8080 (or any non-privileged port).

  • Command adjustment: -p 8080:80.

  • Note: If you absolutely must use port 80, you would have to modify net.ipv4.ip_unprivileged_port_start via sysctl, but the standard architectural fix is mapping to a high port.

Trap 2: The SELinux Wall

The Fix: You must add the :Z (uppercase Z) flag to the volume string.

  • The adjustment: -v /home/web_dev/html:/usr/share/nginx/html:Z.

  • Why it works: The :Z flag tells Podman to relabel the files with a private unshared label (container_file_t with a unique MCS label). This grants the container process permission to read/write those specific host files without turning off SELinux globally.

Trap 3: The Ghost Reboot

The Fix: You must enable linger for the user.

  • The Command: loginctl enable-linger web_dev

  • Why it works: By default, a user's systemd --user instance is killed when their last session logs out. Enabling "linger" tells the system to start that user's manager at boot and keep it running regardless of whether the user is logged in. Without this, your enabled service sits dormant until the first SSH connection.

Trap 4: The Firewall (Bonus)

The Fix: Verify the binding using ss or netstat and check the IP address binding.

  • The Command: ss -tulpn | grep 8080

  • What to look for: Since rootless Podman uses slirp4netns (or pasta), it often binds to 0.0.0.0 or [::] by default, but if it's only showing on 127.0.0.1, external traffic won't reach it.

  • Additional check: Ensure you didn't just add the rule, but also reloaded: firewall-cmd --reload.

  • Rootless Nuance: Ensure the user's slirp4netns process hasn't hung. A quick podman port <container_name> will confirm exactly how the host sees the mapping.


The Final "Perfect" Command

To bring it all together as the web_dev user:

Bash

# 1. Allow the user to run services at boot
loginctl enable-linger web_dev

# 2. Run the container with the correct port and SELinux context
podman run -d \
  --name secure-web \
  -p 8080:80 \
  -v /home/web_dev/html:/usr/share/nginx/html:Z \
  --restart always \
  nginx

# 3. Generate the systemd unit (The modern Quadlet way or legacy generate)
mkdir -p ~/.config/systemd/user/
podman generate systemd --new --name secure-web > ~/.config/systemd/user/container-secure-web.service

# 4. Enable the service
systemctl --user daemon-reload
systemctl --user enable --now containe

Trap 1: The Network Block

The Why: In Linux, ports below 1024 are considered "privileged ports." A rootless user (like web_dev) does not have the authority to bind to these ports on the host network interface. The Fix: You must use a port number 1024 or higher on the host side.

  • Recommended Port: 8080 (or any non-privileged port).

  • Command adjustment: -p 8080:80.

  • Note: If you absolutely must use port 80, you would have to modify net.ipv4.ip_unprivileged_port_start via sysctl, but the standard architectural fix is mapping to a high port.

Trap 2: The SELinux Wall

The Fix: You must add the :Z (uppercase Z) flag to the volume string.

  • The adjustment: -v /home/web_dev/html:/usr/share/nginx/html:Z.

  • Why it works: The :Z flag tells Podman to relabel the files with a private unshared label (container_file_t with a unique MCS label). This grants the container process permission to read/write those specific host files without turning off SELinux globally.

Trap 3: The Ghost Reboot

The Fix: You must enable linger for the user.

  • The Command: loginctl enable-linger web_dev

  • Why it works: By default, a user's systemd --user instance is killed when their last session logs out. Enabling "linger" tells the system to start that user's manager at boot and keep it running regardless of whether the user is logged in. Without this, your enabled service sits dormant until the first SSH connection.

Trap 4: The Firewall (Bonus)

The Fix: Verify the binding using ss or netstat and check the IP address binding.

  • The Command: ss -tulpn | grep 8080

  • What to look for: Since rootless Podman uses slirp4netns (or pasta), it often binds to 0.0.0.0 or [::] by default, but if it's only showing on 127.0.0.1, external traffic won't reach it.

  • Additional check: Ensure you didn't just add the rule, but also reloaded: firewall-cmd --reload.

  • Rootless Nuance: Ensure the user's slirp4netns process hasn't hung. A quick podman port <container_name> will confirm exactly how the host sees the mapping.


The Final "Perfect" Command

To bring it all together as the web_dev user:

# 1. Allow the user to run services at boot
loginctl enable-linger web_dev

# 2. Run the container with the correct port and SELinux context
podman run -d \
  --name secure-web \
  -p 8080:80 \
  -v /home/web_dev/html:/usr/share/nginx/html:Z \
  --restart always \
  nginx

# 3. Generate the systemd unit (The modern Quadlet way or legacy generate)
mkdir -p ~/.config/systemd/user/
podman generate systemd --new --name secure-web > ~/.config/systemd/user/container-secure-web.service

# 4. Enable the service
systemctl --user daemon-reload
systemctl --user enable --now container-secure-web.service