Containers for Linux admins: Podman and Docker basics
-
Phase 0: Before you start
-
Phase 1: The origins and OS architecture
-
Phase 2: Day-to-day administration and the command builder
-
Phase 3: Enterprise infrastructure and storage
-
Phase 4: Troubleshooting step by step
-
Phase 5: Final project and career
Sooner or later a team asks you to “run this container for us” on one of your servers, and Podman basics are what a Linux admin on Rocky Linux or RHEL needs for that: pull an image from a registry, run it without root, publish a port, give it a folder of data with the right SELinux label and make it start at boot like any other service. Docker does the same job with a different design, and you’ll meet it on Ubuntu servers and developer laptops.
In this lesson you’ll run Nginx in rootless Podman containers on rocky01, look at the same processes from inside and outside the container, mount a folder with :Z, and turn the container into a systemd service with a Quadlet file. Then you’ll install Docker on ubu01 for a short look, and see why membership of the docker group is the same as root and why Docker’s published ports walk straight past ufw.
Where this lesson fits
Lesson 57 of 82 in Linux Administration · Phase 3: Enterprise infrastructure and storage · Before this: Lesson 56, host a web app with Nginx and MariaDB · Next: Lesson 58, Apache Kafka on Linux with KRaft
| Time | You need | By the end you can |
|---|---|---|
| About 75 minutes | rocky01 with Nginx from Lesson 56 still on port 80, ubu01 with ufw from Lesson 34, internet access from both VMs to pull images | Run, inspect and clean up rootless Podman containers, fix the SELinux volume and low-port errors, run a container as a Quadlet service that survives a reboot, and explain the risks of the docker group |
What it is, in one line
A container is an ordinary Linux process that the kernel fences in with namespaces and cgroups, started from a read-only image, and Podman runs it without a central daemon and, by default on RHEL, without root.
In Simple Words
The PG room and the separate flat
A virtual machine is like renting a separate flat: its own kitchen, its own water tank, its own main door. It is very private, but it costs a lot and takes time to set up. A container is a room in a paying-guest house: you bring your own bag with everything you need, the house shares one kitchen and one water connection, and your room has its own lock, so you can’t walk into the next room.
Rootless Podman goes one step further: the room is rented in your own name, not the landlord’s. If someone breaks out of your room, they get only your rights in the house, not the keys to every room.
The shared kitchen is the Linux kernel, and the bag you bring is the image.
Engineer's View
What a container is under the hood
There is no “container” object in the kernel. A container runtime (crun on RHEL 10) starts a normal process and gives it its own namespaces: PID (its own process 1), mount (its own file tree from the image), network (its own interfaces), UTS (its own hostname), IPC, and for rootless containers a user namespace that maps UID 0 inside to your own UID outside. cgroups v2 limit its CPU, memory and number of processes. The image is a stack of read-only layers joined by overlayfs, with one thin writable layer on top that is thrown away with the container.
| Podman (Rocky 10.2) | Docker (Ubuntu 26.04 docker.io) | |
|---|---|---|
| Version | podman 5.8 on Rocky 10.2 (5.6 on 10.1) | docker.io 29.1 from universe; Docker CE from Docker’s own repo is newer |
| Design | No daemon: each podman command starts the container, conmon watches it | dockerd daemon running as root; the docker CLI talks to its socket |
| Default user | Rootless for normal users; root containers with sudo | Root (dockerd); rootless mode exists but is extra setup |
| Rootless network | pasta, a user-space process that listens on the host port | Kernel NAT rules (iptables or nftables) that dockerd manages |
| Start at boot | Quadlet .container file turned into a systemd service | Restart policy inside dockerd (--restart unless-stopped) |
| Who can run containers | Any user with subuid/subgid ranges, each in their own space | root and the docker group, which is the same as root |
| SELinux | Containers run as container_t, volumes need :Z or :z | AppArmor profile docker-default on Ubuntu |
The commands are nearly the same (podman run and docker run take the same flags), and both use the same OCI image format from the same registries. Always write the full image name, such as docker.io/library/nginx:alpine. A short name like nginx makes Podman search a list of registries in /etc/containers/registries.conf, which can prompt you or pull from a registry you didn’t mean.
How it works, step by step
Here is what happens between typing podman run -d -p 8080:80 docker.io/library/nginx:alpine as student and Nginx answering on port 8080.
1The image is pulled and stored in your home
Podman contacts docker.io, downloads each layer it doesn’t have yet, checks its digest and unpacks it under ~/.local/share/containers/storage. A rootful container (with sudo) keeps its images in /var/lib/containers instead, so root and student don’t see each other’s images.
2Podman builds the fences and starts the process
It creates the namespaces, a cgroup under your user session, the writable layer and the network, then asks crun to start the image’s command, here nginx -g "daemon off;". conmon stays behind as a small parent that keeps the logs and the exit code. The diagram below shows the chain from the command to a running process.
3Inside root, outside you
The user namespace uses the ranges in /etc/subuid and /etc/subgid. For student that is student:524288:65536: UID 0 in the container is student (1000) on the host, UID 1 is 524288, and Nginx’s worker user (UID 101 in the image) is 524388 on the host. If a process ever escaped the container, it would be a nobody with no rights on rocky01.
4Ports and the firewall
A rootless user can’t bind ports below 1024 on the host, the same rule as for any non-root program, so publish to 8080 or higher. With pasta, the published port is a normal listening socket on rocky01, which means firewalld filters it like any other service: you must open 8080 yourself. Docker works the other way, with NAT rules that bypass ufw on Ubuntu, and you’ll see that in step 8 of the lab.
5Data lives outside the container
When the container is removed, its writable layer goes with it, so anything worth keeping goes in a volume: a folder on the host mounted into the container. With SELinux enforcing, a container (container_t) may only read files labelled container_file_t. The :Z option relabels the folder for this one container (private MCS categories), and :z relabels it so several containers can share it.
The cards below compare the four ways you’ll see apps run on a Linux server, and what each means for you as the admin.
6Make it a service with Quadlet
The old podman generate systemd command is deprecated. Since Podman 4.4 you write a small .container file, in ~/.config/containers/systemd/ for a rootless service or /etc/containers/systemd/ for a root one. On systemctl daemon-reload, the Quadlet generator turns it into a normal .service unit with the right podman run line, so you manage it with systemctl and read it with journalctl.
A rootless service lives in student’s own systemd user manager, which normally starts at login and stops at logout. loginctl enable-linger student keeps that manager running from boot, which is what a server needs.
Containers suit stateless or easily rebuilt apps that ship as images, like a status page or a small API. A VM still makes sense when you need a different kernel, a full OS a vendor supports, or strong isolation between customers.
Hands-on lab: rootless Podman, Quadlet and a look at Docker
Before you run any command, double-check it
Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.
Steps 1 to 7 run on rocky01 as student; only package installs, the firewall and linger need sudo. Step 8 runs on ubu01. Both VMs need internet access to reach docker.io.
Nginx from Lesson 56 keeps port 80 on rocky01, so the containers use 8080 and 8081.
1Install Podman and see how it runs
Run this:
sudo dnf install -y podman
podman --version
podman info --format '{{.Host.Security.Rootless}} {{.Host.NetworkBackend}} {{.Host.RootlessNetworkCmd}} {{.Store.GraphDriverName}} {{.Host.OCIRuntime.Name}}'
grep student /etc/subuid /etc/subgidYou should see something like this:
podman version 5.8.2
true netavark pasta overlay crun
/etc/subuid:student:524288:65536
/etc/subgid:student:524288:65536You’re rootless, container networks are handled by netavark with pasta for rootless ports, images sit on overlayfs and crun starts the processes. The subuid and subgid lines were added when student was created; a user without them can’t run rootless containers, which is the first thing to check for a service account made by hand.
✅ Checkpoint: podman info prints “true” as the first word and student has a 65536-ID range in both files.
2Pull an image by its full name
Run this:
podman pull docker.io/library/nginx:alpine
podman imagesYou should see something like this:
Trying to pull docker.io/library/nginx:alpine...
Getting image source signatures
Copying blob 9824c27679d3 done |
Copying blob 6d79cc6084d4 done |
Copying blob 5f6ee5a7a4a8 done |
Copying config 5e7abcdd2021 done |
Writing manifest to image destination
5e7abcdd20216bbeedf1369529564ffd60f830ed3540c477938ca580b645dff5
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/nginx alpine 5e7abcdd2021 3 weeks ago 52.6 MBEach “blob” is one layer of the image, checked against its digest as it arrives. The tag alpine moves whenever the Nginx team publishes a new build, so at work you’d pin a version tag or the digest for anything that must not change under you.
✅ Checkpoint: podman images lists docker.io/library/nginx with the alpine tag.
3Run a container and look at it
Run this:
podman run -d --name web1 -p 8080:80 docker.io/library/nginx:alpine
podman ps
curl -s http://localhost:8080/ | grep '<title>'
podman logs web1 2>&1 | tail -n 3
podman exec web1 sh -c 'id -u; hostname'You should see something like this:
3f2a9c81d4b7e0c55a1f9d0e8b6c2a47d13e9f05b8c7a6d4e2f1b0c9a8d7e6f5
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f2a9c81d4b7 docker.io/library/nginx:alpine nginx -g daemon o... 4 seconds ago Up 4 seconds 0.0.0.0:8080->80/tcp web1
<title>Welcome to nginx!</title>
2026/10/27 07:02:08 [notice] 1#1: start worker processes
2026/10/27 07:02:08 [notice] 1#1: start worker process 30
2026/10/27 07:02:08 [notice] 1#1: start worker process 31
0
3f2a9c81d4b7-d runs it in the background and prints the full container ID, -p 8080:80 publishes container port 80 on host port 8080. The container’s logs are Nginx’s own stdout and stderr, kept by conmon. Times are UTC because the image has no time zone set.
Inside, the shell runs as UID 0 and the hostname is the short container ID.
✅ Checkpoint: curl shows the “Welcome to nginx!” title and podman ps lists web1 as Up.

4See rootless from the host side
Run this:
podman top web1 user huser pid hpid comm
ps -o user:12,pid,cmd -p 3412,3439,3440You should see something like this:
USER HUSER PID HPID COMMAND
root student 1 3412 nginx
nginx 524388 30 3439 nginx
nginx 524388 31 3440 nginx
USER PID CMD
student 3412 nginx: master process nginx -g daemon off;
524388 3439 nginx: worker process
524388 3440 nginx: worker processThe same three processes appear twice: PID 1, 30 and 31 inside, 3412, 3439 and 3440 on rocky01. Container root is student on the host, and the nginx user (UID 101 inside) is 524388, which is 524288 + 100 from the subuid range. ps on the host shows no name for 524388 because no account has that UID, which is the point. Use the HPID numbers from your own podman top in the ps command; ps -C nginx would also list the Lesson 56 Nginx, whose master runs as root.
✅ Checkpoint: podman top shows a HUSER of student for PID 1 and 524388 for the workers.
5Serve your own page from a volume with :Z
Create a folder with a page and mount it read-only into a second container:
mkdir -p ~/saatweb
echo '<h1>SaatShop status: all good</h1>' > ~/saatweb/index.html
podman run -d --name web2 -p 8081:80 -v ~/saatweb:/usr/share/nginx/html:ro,Z docker.io/library/nginx:alpine
curl -s http://localhost:8081/
ls -ldZ ~/saatwebYou should see something like this:
b81c06e4f2d95a7310c8e4d2a6f9b1c37e05d8a2f4c6b9e1d3a5c7f9e2b4d6a8
<h1>SaatShop status: all good</h1>
drwxr-xr-x. 2 student student system_u:object_r:container_file_t:s0:c241,c907 24 Oct 27 12:34 /home/student/saatweb:ro makes the mount read-only inside, so a compromised Nginx can’t change your page. :Z relabelled the folder to container_file_t with two MCS categories (c241,c907) that only this container gets, so another container can’t read it. Now remove both test containers; the image stays for the next steps:
podman rm -f web1 web2
podman ps -aweb1
web2
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES✅ Checkpoint: curl on 8081 printed your own heading, ls -Z shows container_file_t, and podman ps -a is empty.
6Turn it into a service with Quadlet
Write the Quadlet file, check what systemd will get, then start it:
mkdir -p ~/.config/containers/systemd
cat > ~/.config/containers/systemd/saatweb.container <<'EOF'
# ~/.config/containers/systemd/saatweb.container (SaatVedha Linux course, Lesson 57)
[Unit]
Description=SaatShop status page (rootless Podman)
[Container]
Image=docker.io/library/nginx:alpine
ContainerName=saatweb
PublishPort=8080:80
Volume=/home/student/saatweb:/usr/share/nginx/html:ro,Z
[Service]
Restart=always
[Install]
WantedBy=default.target
EOF
/usr/libexec/podman/quadlet -dryrun -user | grep '^ExecStart='
systemctl --user daemon-reload
systemctl --user start saatweb.service
systemctl --user status saatweb.service --no-pager | head -n 4
podman quadlet listYou should see something like this:
ExecStart=/usr/bin/podman run --name saatweb --replace --rm --cgroups=split --sdnotify=conmon -d -v /home/student/saatweb:/usr/share/nginx/html:ro,Z --publish 8080:80 docker.io/library/nginx:alpine
● saatweb.service - SaatShop status page (rootless Podman)
Loaded: loaded (/home/student/.config/containers/systemd/saatweb.container; generated)
Active: active (running) since Tue 2026-10-27 12:38:51 IST; 3s ago
Invocation: 6c1e0b9a2f4d47e8b3a5c7d9e1f20a4b
NAME UNIT NAME PATH ON DISK STATUS APPLICATION
saatweb.container saatweb.service /home/student/.config/containers/systemd/saatweb.container active/runningThe dry run shows the exact podman run line the generator wrote: --replace and --rm mean every start gets a fresh container from the image, which is how containers are meant to live. “generated” in the Loaded line tells you this unit came from Quadlet, so you edit the .container file, never the .service. The [Install] section is what makes it start at boot; systemctl --user enable doesn’t work on generated units.
✅ Checkpoint: saatweb.service is active (running) and podman quadlet list shows it as active/running.
7Keep it running after logout and reboot, and open the port
Run this:
sudo loginctl enable-linger student
loginctl show-user student --property=Linger
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reloadLinger=yes
success
successReboot rocky01 and, without logging in on it, run this on ubu01:
curl -s http://rocky01.saatlab.test:8080/
curl -s http://rocky01.saatlab.test/health.phpYou should see this:
<h1>SaatShop status: all good</h1>
OK db=up orders=5With linger on, systemd starts student’s user manager at boot, which starts saatweb.service from the [Install] section. The firewall port was needed because pasta listens on 8080 like any normal program. SaatShop from Lesson 56 on port 80 came back too, so both stacks live side by side.
✅ Checkpoint: After a reboot with nobody logged in, ubu01 gets the status page from port 8080 and “OK db=up” from port 80.
8A short look at Docker on ubu01
On ubu01, install Ubuntu’s Docker package and run a test image:
sudo apt install -y docker.io
docker --version
sudo docker run --rm docker.io/library/hello-world 2>/dev/null | head -n 3You should see something like this:
Docker version 29.1.3, build 29.1.3-0ubuntu4
Hello from Docker!
This message shows that your installation appears to be working correctly.Here a root daemon, dockerd, did the work, and you needed sudo. Many guides tell you to run sudo usermod -aG docker student to drop the sudo. Look at what any member of that group could then do, no sudo asked:
sudo docker run --rm -v /etc:/host-etc:ro docker.io/library/alpine head -n 1 /host-etc/shadowYou should see something like this:
root:*:20746:0:99999:7:::The container’s root is the host’s root here, so mounting /etc gives it the password hashes, and mounting / read-write would give it the whole server. That’s why the docker group is treated as root in audits. Now see what happens to the firewall from Lesson 34:
sudo docker run -d --name ufwtest -p 8090:80 docker.io/library/nginx:alpine
sudo ufw status | head -n 6Then on rocky01:
curl -sI http://10.10.10.21:8090/ | head -n 1You should see something like this:
HTTP/1.1 200 OKufw allows only SSH (and syslog from rocky01), yet port 8090 answered. Docker writes its own NAT rules that send the traffic to the container before ufw’s INPUT rules ever see it, and Docker’s own documentation says the two are incompatible. Publish on 127.0.0.1 (for example -p 127.0.0.1:8090:80) when only the server itself needs the port.
Clean up and stop Docker so it doesn’t hold memory on ubu01:
sudo docker rm -f ufwtest
sudo systemctl disable --now docker.service docker.socket✅ Checkpoint: You saw the shadow line through a container and port 8090 answer through ufw, and docker.service is now inactive.
Break It, Then Fix It
Take a snapshot of rocky01 first (VBoxManage snapshot rocky01 take before-l57-break). All three failures happen as student on rocky01.
1A volume without :Z gives 403 Forbidden
Make a promo page and mount it the way most Docker guides show, without a label option:
mkdir -p ~/promo
echo '<h1>Diwali offers</h1>' > ~/promo/index.html
podman run -d --name promo -p 8082:80 -v ~/promo:/usr/share/nginx/html:ro docker.io/library/nginx:alpine
curl -s http://localhost:8082/ | grep '<title>'
podman logs promo 2>&1 | grep -o '".*" is forbidden.*denied)'
sudo ausearch -m AVC -ts recent | grep index.html | tail -n 1You’ll get this error:
<title>403 Forbidden</title>
"/usr/share/nginx/html/index.html" is forbidden (13: Permission denied)
type=AVC msg=audit(1793085700.318:1342): avc: denied { getattr } for pid=3871 comm="nginx" path="/usr/share/nginx/html/index.html" dev="dm-0" ino=1049321 scontext=system_u:system_r:container_t:s0:c118,c560 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0The file mode is fine (644), but the folder still has its home-folder label user_home_t, and container_t isn’t allowed to touch it. The same command works on Ubuntu, which is why people copy it and then disable SELinux on RHEL. Don’t.
Fix: recreate the container with :Z (or :z if several containers share the folder).
podman rm -f promo
podman run -d --name promo -p 8082:80 -v ~/promo:/usr/share/nginx/html:ro,Z docker.io/library/nginx:alpine
curl -s http://localhost:8082/
podman rm -f promo<h1>Diwali offers</h1>Never use :Z or :z on a system folder or your whole home, such as -v /etc:/etc:Z or -v ~:/data:Z. Relabelling them breaks SSH keys, logins and services that expect their own labels; Podman refuses the most dangerous paths, but not every one. Make a small folder for the container instead.
✅ Checkpoint: With :Z the promo page loads, and the AVC no longer appears for new requests.
2Port 80 as a normal user
Try to publish the status page on port 80:
podman run -d --name web80 -p 80:80 docker.io/library/nginx:alpineYou’ll get this error:
Error: pasta failed with exit code 1:
Failed to bind port 80 (Permission denied) for option '-t 80-80:80-80', exitingPorts below 1024 are privileged, and student is a normal user on the host, container or not:
cat /proc/sys/net/ipv4/ip_unprivileged_port_start1024You could lower that limit with a sysctl drop-in in /etc/sysctl.d, but it opens every low port to every user, and on rocky01 port 80 already belongs to Nginx from Lesson 56, so you’d swap “Permission denied” for “Address already in use”. The container was created before the port failed, so it also blocks the name web80.
Fix: remove the half-made container and keep rootless services on ports from 1024 up; put Nginx on 80 or 443 in front of them when users need a standard port.
podman rm -f web80
podman run -d --name web80 -p 8083:80 docker.io/library/nginx:alpine
podman ps --format '{{.Names}} {{.Ports}}'
podman rm -f web80You should see something like this:
saatweb 0.0.0.0:8080->80/tcp
web80 0.0.0.0:8083->80/tcp
web80✅ Checkpoint: web80 ran on 8083 and was removed, and saatweb is still up on 8080.
3A typo in the Quadlet file makes the service disappear
Change one key the way people often misspell it, and reload:
sed -i 's/^PublishPort=/PublishPorts=/' ~/.config/containers/systemd/saatweb.container
systemctl --user daemon-reload
systemctl --user restart saatweb.serviceYou’ll get this error:
Failed to restart saatweb.service: Unit saatweb.service not found.The generator refused the file, so no service was made, and systemd says nothing about why. Ask Quadlet directly:
/usr/libexec/podman/quadlet -dryrun -user 2>&1 | grep saatwebYou should see something like this:
quadlet-generator[4120]: converting "saatweb.container": unsupported key 'PublishPorts' in group 'Container' in /home/student/.config/containers/systemd/saatweb.containerFix: correct the key, reload and start.
sed -i 's/^PublishPorts=/PublishPort=/' ~/.config/containers/systemd/saatweb.container
systemctl --user daemon-reload
systemctl --user start saatweb.service
systemctl --user is-active saatweb.serviceactive✅ Checkpoint: quadlet -dryrun prints no error for saatweb.container and the service is active again.
This happened at work
The map tiles that waited for someone to log in
The on-call engineer at a maps and navigation startup in Chennai told me about this one. They served map tiles for delivery apps from rootless Podman containers on six Rocky servers, each run by a service account called tiles, and it had worked well for months. Then, the Sunday after monthly kernel patching, two delivery partners called at 6 a.m.: blank grey squares wherever their riders zoomed in.
The servers were up, SSH worked and podman ps as root showed nothing at all. Someone guessed that the Podman update had broken the images and started pulling them again, which took twenty minutes and changed nothing. As soon as the engineer logged in as tiles to look around, the containers started on their own, and that was the real clue.
The services were rootless Quadlet units in the tiles user’s systemd manager. Linger had been turned on by hand on the first two servers during the original build and never on the other four, and before that patch nobody had rebooted those four. Without linger, the user manager only started when someone logged in, so the tiles waited for a human.
The team added loginctl enable-linger tiles to the Ansible role that creates the account, and a post-patch check that runs loginctl show-user tiles --property=Linger and curls each tile endpoint from outside. They also stopped checking rootless containers with sudo, because sudo podman ps only shows root’s containers, which had made the first ten minutes of that morning so confusing.
Common mistakes and troubleshooting
| Mistake | What happens | What to do instead |
|---|---|---|
Using short image names like nginx | A prompt in scripts, or a pull from a registry you didn’t intend | Always write registry/namespace/name:tag, like docker.io/library/nginx:alpine |
| Mounting a host folder without :Z on RHEL | 403 or Permission denied inside the container, then someone disables SELinux | Add :Z (one container) or :z (shared); keep the folder small |
Checking rootless containers with sudo podman ps | Root sees only its own containers, so everything looks gone | Run podman as the user who owns the containers |
Forgetting loginctl enable-linger for a rootless service | The container starts only when the user logs in, not at boot | Enable linger for the service account and test with a reboot |
| Editing the generated .service file | The change vanishes on the next daemon-reload | Edit the .container file and run systemctl --user daemon-reload |
| Adding people to the docker group for convenience | Every member can mount / and act as root without sudo or a log line | Use sudo docker, or rootless Podman, and treat the docker group as root in access reviews |
| Trusting ufw to protect Docker ports | Published ports are reachable from the network despite ufw | Publish on 127.0.0.1, or filter in the DOCKER-USER chain or an external firewall |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. What’s the difference between a container and a virtual machine, and when would you pick each?
Why they ask this: A basic concept question; they want the shared-kernel point and a practical rule for choosing.
How you might answer: A VM has its own kernel on virtual hardware, so it can run any OS and is strongly isolated, but it takes gigabytes and minutes to start. A container is a process on the host kernel, fenced in with namespaces and cgroups, so it starts in a second and uses only what the app needs.
I’d pick containers for apps that ship as images and can be rebuilt, like a web front end or an API, and a VM when I need a different kernel or OS version, a vendor-supported full system, or strong separation between customers.
2. What does “rootless” mean in Podman, and why does it matter?
Why they ask this: They’re checking that you understand user namespaces at a basic level and the security benefit.
How you might answer: Rootless means a normal user runs the containers, with no root daemon involved. Podman maps UID 0 inside the container to my own UID on the host, and other container UIDs to a range from /etc/subuid, like 524288 and up.
So if someone breaks out of the container, they land on the host as me or as an unused UID, not as root. The trade-offs are that I can’t bind ports below 1024 and I need linger for services that must start at boot.
2–5 years experience
3. A container on RHEL gets “Permission denied” reading a mounted folder, though the file modes look fine. How do you fix it?
Why they ask this: They want to hear SELinux labels, the difference between :Z and :z, and a safe fix rather than setenforce 0.
How you might answer: First I’d confirm it’s SELinux with ausearch -m AVC -ts recent, which usually shows container_t denied on a file labelled user_home_t or var_t. The fix is to run the container with :Z on the volume so Podman relabels the folder to container_file_t for that container, or :z if several containers share it.
I’d never do that on a system folder like /etc or a whole home folder, because relabelling them breaks other services. In that case I’d copy the needed files into a dedicated folder for the container.
4. How do you run a container as a proper service on RHEL 10, so it restarts and starts at boot?
Why they ask this: They’re checking whether you know Quadlet replaced podman generate systemd, and the rootless details.
How you might answer: I’d write a Quadlet .container file with Image, PublishPort, Volume and an [Install] section, in /etc/containers/systemd for a root service or ~/.config/containers/systemd for a rootless one. After a daemon-reload, systemd has a generated .service that I start and check with systemctl and journalctl.
For rootless I’d also enable linger for the account, and I’d test the whole thing with a reboot. If the service doesn’t appear, /usr/libexec/podman/quadlet -dryrun shows the parsing error.
Senior level
5. A developer team wants to be added to the docker group on shared build servers. What do you say?
Why they ask this: Seniors should explain the root-equivalence risk and offer workable alternatives instead of a flat no.
How you might answer: I’d explain that the docker group can start a container with / mounted and get full root on the server, with nothing in sudo’s log, so it’s the same as handing out root on a shared box. For shared build servers I’d offer rootless Podman, where each developer’s containers run under their own UID, or rootless Docker if their tooling needs the Docker API.
If they truly need root containers, I’d use dedicated, disposable build VMs per team, managed by automation, rather than one shared server where any member can reach everyone else’s work.
6. How would you keep container images on 50 servers patched against security fixes?
Why they ask this: They want to see that you know images don’t get dnf updates and that patching is a rebuild-and-redeploy process.
How you might answer: Containers don’t pick up host patches for their userland, so I’d rebuild or re-pull images regularly from a trusted base, scan them in the registry, and pin by digest or version tag so I know what runs where. With Quadlet I can set AutoUpdate=registry and run podman-auto-update.timer, which pulls a newer image for the same tag and restarts the service, rolling back if it fails to start.
The host still gets normal OS patches for the kernel, Podman and crun. I’d report image age and scan results per server so old images show up before an audit does.
Quick quiz
Q1. In rootless Podman, which host user owns the container’s PID 1 if it runs as root inside?
- root
- The user who started the container
- nobody
- UID 524288
Show answer
B — UID 0 inside maps to your own UID; other container UIDs map to your subuid range.
Q2. Why did the promo container return 403 Forbidden on rocky01?
- The file mode was 600
- The folder kept the user_home_t label and container_t can’t read it
- Port 8082 was closed
- The image was corrupt
Show answer
B — Add :Z so Podman relabels the folder to container_file_t.
Q3. What does a Quadlet .container file become after systemctl daemon-reload?
- A Docker Compose file
- A generated systemd .service unit
- A cron job
- A kernel module
Show answer
B — The Quadlet generator writes the .service; you edit the .container file, not the generated unit.
Q4. What makes a rootless Quadlet service start at boot without anyone logging in?
systemctl --user enableloginctl enable-lingerfor that user plus an [Install] sectionRestart=alwayspodman generate systemd
Show answer
B — Linger starts the user manager at boot; [Install] WantedBy=default.target starts the service in it.
Q5. Why is the docker group treated as root?
- It can read /etc/sudoers
- Members can start a root container with host folders mounted and change anything
- It disables SELinux
- It owns /var/lib/docker
Show answer
B — dockerd runs as root, so docker -v /:/host gives full control of the host.
Quick recap
- A container is a normal process fenced in by namespaces and cgroups, started from read-only image layers.
- Podman has no daemon and runs rootless by default: root inside maps to your UID, other UIDs to /etc/subuid.
- Use full image names, publish rootless ports from 1024 up, and open them in firewalld yourself.
- On RHEL, mount volumes with :Z (private) or :z (shared), never on system folders.
- Run containers as services with a Quadlet .container file, daemon-reload, and loginctl enable-linger for rootless ones.
- The docker group equals root, and Docker’s published ports bypass ufw: publish on 127.0.0.1 when you can.
Next lesson
Lesson 58: Apache Kafka on Linux with KRaft → You’ll build kafka01, install Kafka 4.4 with Java 21, run a three-node KRaft cluster as systemd template units with a LimitNOFILE drop-in, and watch the quorum survive a lost node.
Further reading: Podman basics
Want to go deeper into Podman basics? Read the Podman documentation, especially podman-systemd.unit(5) for Quadlet, Red Hat’s RHEL 10 documentation on building, running and managing containers, and Docker’s page on packet filtering and firewalls, which explains the ufw problem. You can also see the full plan for this course on the Linux Administration course page.
