College-Level Course Module | RHEL System Administration
Learning Objectives
1
Understand container concepts
Images, containers, registries, and Podman architecture
2
Manage container images
Search, pull, list, inspect, and remove images
3
Run and manage containers
Create, start, stop, execute commands, and configure containers
4
Configure persistent storage and services
Use volumes for data persistence, run containers as systemd services
What Are Containers?
Containers are lightweight, isolated environments that package applications with their dependencies. They share the host kernel but have isolated filesystems, processes, and network stacks.
Virtual Machines
Full OS per VM Hypervisor required GB of memory each Minutes to start Strong isolation
Containers
Shared host kernel No hypervisor needed MB of memory each Seconds to start Process isolation
Container A | Container B | Container C
Container Runtime (Podman)
Host Operating System (RHEL)
Hardware
Images vs Containers
Container Image
Read-only template containing application and dependencies. Like a class definition or a recipe.
Built from layers
Stored in registries
Immutable once created
Shared across containers
Container
Running instance of an image with a writable layer. Like an object or a meal from a recipe.
Created from an image
Has isolated runtime
Writable layer on top
Can be started/stopped
Registry
→
Image
→
Container
→
Container
→
Container
Introduction to Podman
Podman (Pod Manager) is RHEL's container engine. It provides Docker-compatible commands without requiring a daemon or root privileges. It is the default container tool in RHEL 8 and 9.
Feature
Podman
Docker
Daemon
No daemon required
Requires dockerd daemon
Root access
Rootless by default
Requires root (or docker group)
Command syntax
docker-compatible
docker commands
Systemd integration
Native support
Limited
Pod support
Native pods
No native pods
# Install Podman[root@server ~]# dnf install container-tools
# Verify installation[user@server ~]$ podman --version
podman version 4.6.1# Podman commands are Docker-compatible[user@server ~]$ alias docker=podman # Works for most commands
Container Registries
Registries are servers that store and distribute container images. You pull images from registries and can push your own images to them.
Registry
URL
Description
Red Hat Registry
registry.redhat.io
Official Red Hat images (requires login)
Red Hat Catalog
registry.access.redhat.com
Certified partner images
Quay.io
quay.io
Red Hat's public registry
Docker Hub
docker.io
Public Docker registry
# Registry configuration[user@server ~]$ cat /etc/containers/registries.conf
unqualified-search-registries = ["registry.access.redhat.com", "registry.redhat.io", "docker.io"]# Login to registry (required for registry.redhat.io)[user@server ~]$ podman login registry.redhat.io
Username: your-redhat-username
Password:
Login Succeeded!# Full image name format:registry.redhat.io/rhel9/httpd-24:latestregistry/namespace/image:tag
Searching for Images
# Search registries for images[user@server ~]$ podman search httpd
INDEX NAME DESCRIPTION STARS OFFICIAL
redhat.io registry.redhat.io/rhel8/httpd-24 Apache HTTP Server 2.4 0
redhat.io registry.redhat.io/rhel9/httpd-24 Apache HTTP Server 2.4 0
docker.io docker.io/library/httpd The Apache HTTP Server 4523 [OK]
docker.io docker.io/centos/httpd-24-centos7 Platform for running... 44# Search specific registry[user@server ~]$ podman search registry.redhat.io/httpd
# Limit results[user@server ~]$ podman search --limit 5 nginx
# Show only official images[user@server ~]$ podman search --filter=is-official=true httpd
# List available tags (requires skopeo)[user@server ~]$ skopeo list-tags docker://registry.redhat.io/rhel9/httpd-24
{
"Repository": "registry.redhat.io/rhel9/httpd-24",
"Tags": ["1-290", "1-299", "latest"]
}
Pulling Images
# Pull an image from registry[user@server ~]$ podman pull registry.redhat.io/rhel9/httpd-24
Trying to pull registry.redhat.io/rhel9/httpd-24:latest...
Getting image source signatures
Copying blob 8e5fca777171 done
Copying blob 3c72a8ed6814 done
Copying config 8f1de90813 done
Writing manifest to image destination
8f1de908137c2c3b9f2c7e8e4c9d9de1f4a6b5c3d2e1f0...# Pull with specific tag[user@server ~]$ podman pull registry.redhat.io/ubi9/ubi:9.3
# Pull from Docker Hub[user@server ~]$ podman pull docker.io/library/nginx:1.25
# Pull without specifying registry (searches configured registries)[user@server ~]$ podman pull ubi9
? Please select an image:
registry.access.redhat.com/ubi9:latest
▸ registry.redhat.io/ubi9:latest
UBI (Universal Base Image): Red Hat provides freely distributable base images (UBI) that can be used without a subscription. Perfect for building custom containers.
Managing Images
# List local images[user@server ~]$ podman images
REPOSITORY TAG IMAGE ID CREATED SIZE
registry.redhat.io/rhel9/httpd-24 latest 8f1de90813c2 2 weeks ago 462 MB
registry.redhat.io/ubi9/ubi 9.3 a1b2c3d4e5f6 3 weeks ago 211 MB
docker.io/library/nginx 1.25 b2c3d4e5f6a7 1 month ago 142 MB# Inspect image details[user@server ~]$ podman inspect registry.redhat.io/ubi9/ubi:9.3
[{
"Id": "a1b2c3d4e5f6...",
"Config": {
"Cmd": ["/bin/bash"],
"Env": ["PATH=/usr/local/sbin:..."],
...
}
}]# Show image history (layers)[user@server ~]$ podman history registry.redhat.io/ubi9/ubi:9.3
# Remove an image[user@server ~]$ podman rmi registry.redhat.io/ubi9/ubi:9.3
# Remove all unused images[user@server ~]$ podman image prune -a
Running Containers
# Run a container (pulls image if needed)[user@server ~]$ podman run registry.redhat.io/ubi9/ubi echo "Hello Container"
Hello Container# Run interactively with terminal[user@server ~]$ podman run -it registry.redhat.io/ubi9/ubi /bin/bash
[root@a1b2c3d4 /]# cat /etc/redhat-release
Red Hat Enterprise Linux release 9.3 (Plow)[root@a1b2c3d4 /]# exit
# Run in detached mode (background)[user@server ~]$ podman run -d --name myweb registry.redhat.io/rhel9/httpd-24
a1b2c3d4e5f6g7h8i9j0...# Run with automatic removal when stopped[user@server ~]$ podman run --rm registry.redhat.io/ubi9/ubi cat /etc/os-release
# List running containers[user@server ~]$ podman ps
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
a1b2c3d4e5f6 registry.redhat.io/rhel9/httpd-24 /usr/bin/run-http... Up 5 minutes myweb# List all containers (including stopped)[user@server ~]$ podman ps -a
# Stop a container[user@server ~]$ podman stop myweb
# Start a stopped container[user@server ~]$ podman start myweb
# Restart a container[user@server ~]$ podman restart myweb
# Remove a stopped container[user@server ~]$ podman rm myweb
# Force remove a running container[user@server ~]$ podman rm -f myweb
# Remove all stopped containers[user@server ~]$ podman container prune
Image
→
Created
→
Running
→
Stopped
→
Removed
Executing Commands
# Execute command in running container[user@server ~]$ podman exec myweb cat /etc/httpd/conf/httpd.conf
# Get interactive shell in running container[user@server ~]$ podman exec -it myweb /bin/bash
[root@a1b2c3d4 /]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 12345 1234 ? Ss 10:00 0:00 httpd -D FOREGROUND
...[root@a1b2c3d4 /]# exit
# View container logs[user@server ~]$ podman logs myweb
[Mon Jan 20 10:00:00.000000 2024] [core:notice] Starting Apache...# Follow logs in real-time[user@server ~]$ podman logs -f myweb
# Show container resource usage[user@server ~]$ podman stats myweb
ID NAME CPU % MEM USAGE / LIMIT MEM % NET IO BLOCK IO
a1b2c3d4e5f6 myweb 0.50% 45.2MiB / 7.768GiB 0.57% 1.2kB/0B 0B/0B
Port Mapping
Containers have isolated network stacks. To access container services from outside, map container ports to host ports using -p.
# Map host port 8080 to container port 80[user@server ~]$ podman run -d --name web -p 8080:80 registry.redhat.io/rhel9/httpd-24
# Access from host[user@server ~]$ curl http://localhost:8080
<html>...</html># Map multiple ports[user@server ~]$ podman run -d -p 8080:80 -p 8443:443 --name secureweb nginx
# Map to specific host interface[user@server ~]$ podman run -d -p 192.168.1.100:8080:80 --name web nginx
# Let Podman choose host port[user@server ~]$ podman run -d -p 80 --name web nginx
[user@server ~]$ podman port web
80/tcp -> 0.0.0.0:43210# View port mappings[user@server ~]$ podman ps
... PORTS NAMES
... 0.0.0.0:8080->80/tcp web
Configuration pattern: Many container images use environment variables for configuration. Check image documentation for available variables.
Persistent Storage
By default, container data is ephemeral. When a container is removed, its writable layer is deleted. Use volumes for data that must persist.
# Bind mount host directory into container[user@server ~]$ podman run -d --name web \
-v /home/user/website:/var/www/html:Z \
-p 8080:80 \
registry.redhat.io/rhel9/httpd-24
# Create and use a named volume[user@server ~]$ podman volume create dbdata
[user@server ~]$ podman run -d --name mysql \
-v dbdata:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
docker.io/library/mysql:8.0
# List volumes[user@server ~]$ podman volume ls
DRIVER VOLUME NAME
local dbdata# Inspect volume[user@server ~]$ podman volume inspect dbdata
# Remove volume[user@server ~]$ podman volume rm dbdata
SELinux and Volumes
SELinux blocks container access to host files by default. Use volume options to set correct context, or container cannot read/write mounted directories.
# Without SELinux option - fails with permission denied[user@server ~]$ podman run -v /home/user/data:/data ubi9 ls /data
ls: cannot open directory '/data': Permission denied# With :Z - relabels for this container only (private)[user@server ~]$ podman run -v /home/user/data:/data:Z ubi9 ls /data
file1.txt file2.txt# With :z - relabels for shared access (multiple containers)[user@server ~]$ podman run -v /shared/data:/data:z ubi9 ls /data
# Check SELinux context[user@server ~]$ ls -lZ /home/user/data
-rw-r--r--. user user system_u:object_r:container_file_t:s0 file1.txt
:Z vs :z - Use uppercase :Z for private volumes (one container). Use lowercase :z for shared volumes (multiple containers). Both relabel for container access.
Containers as Services
For production, run containers as systemd services. This provides automatic startup, restart on failure, and standard service management.
# Create container first (do not start yet for user service)[user@server ~]$ podman create --name myweb -p 8080:80 registry.redhat.io/rhel9/httpd-24
# Generate systemd unit file[user@server ~]$ mkdir -p ~/.config/systemd/user
[user@server ~]$ podman generate systemd --name myweb --files --new
~/.config/systemd/user/container-myweb.service# Reload systemd and enable service[user@server ~]$ systemctl --user daemon-reload
[user@server ~]$ systemctl --user enable --now container-myweb
# Check status[user@server ~]$ systemctl --user status container-myweb
# Enable lingering (service runs without user login)[user@server ~]$ loginctl enable-linger $USER
Enable lingering! Without loginctl enable-linger, user services stop when the user logs out.
Rootless vs Root
Rootless (Default)
Run by regular users
No root privileges needed
Container escape = user access only
Cannot bind ports below 1024
Limited network features
User systemd services
Root Containers
Run by root user
Full system access
Container escape = root access!
Can bind any port
Full network features
System systemd services
# Rootless - run as regular user[user@server ~]$ podman run -d -p 8080:80 --name web nginx
# Root - run as root (when necessary)[root@server ~]# podman run -d -p 80:80 --name web nginx
# Storage locations differ:# Rootless: ~/.local/share/containers/# Root: /var/lib/containers/
Best practice: Use rootless containers whenever possible. Only run as root when you need privileged ports or specific system access.
Inspecting Containers
# Detailed container information[user@server ~]$ podman inspect myweb
[{
"Id": "a1b2c3d4e5f6...",
"State": { "Status": "running", "Pid": 12345 },
"NetworkSettings": { "IPAddress": "10.88.0.2" },
"Mounts": [...],
...
}]# Get specific value with format[user@server ~]$ podman inspect --format '{{.State.Status}}' myweb
running[user@server ~]$ podman inspect --format '{{.NetworkSettings.IPAddress}}' myweb
10.88.0.2# View container processes[user@server ~]$ podman top myweb
USER PID PPID %CPU ELAPSED TTY COMMAND
root 1 0 0.0 10m30s ? httpd -D FOREGROUND
apache 10 1 0.0 10m29s ? httpd -D FOREGROUND# Show container filesystem changes[user@server ~]$ podman diff myweb
C /var/log/httpd
A /var/log/httpd/access_log
A /var/log/httpd/error_log
Best Practices
Do
Use rootless containers when possible
Use specific image tags, not :latest
Use volumes for persistent data
Set resource limits (--memory, --cpus)
Use named containers for easy management
Run containers as systemd services
Keep images updated for security
Use :Z or :z for bind mounts (SELinux)
Do Not
Run as root unless necessary
Store important data only in containers
Use untrusted images in production
Expose unnecessary ports
Hard-code secrets in images
Forget to clean up unused resources
Ignore container logs
Skip SELinux labels on bind mounts
# Good: Specific tag, named, resource limits, volume for data[user@server ~]$ podman run -d --name mydb \
--memory 512m --cpus 1 \
-v dbdata:/var/lib/mysql:Z \
-e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password \
mysql:8.0.35