CIS238RH | RHEL System Administration
Mesa Community College
Images, containers, registries, and Podman architecture
Search, pull, list, inspect, and remove images
Create, start, stop, execute commands, and configure containers
Use volumes for data persistence, run containers as systemd services
Containers are lightweight, isolated environments that package applications with their dependencies. They share the host kernel but have isolated filesystems, processes, and network stacks.
Full OS per VM
Hypervisor required
GB of memory each
Minutes to start
Strong isolation
Shared host kernel
No hypervisor needed
MB of memory each
Seconds to start
Process isolation
Read-only template containing application and dependencies. Like a class definition or a recipe.
Running instance of an image with a writable layer. Like an object or a meal from a recipe.
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
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:latest
registry/namespace/image:tag
# 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"]
}
skopeo list-tags to inspect available versions in a registry without downloading the image. Always pin a specific tag for reproducible deployments — avoid :latest in production.
# 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
# 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
-f to force removal. Use podman image prune -a carefully in production — it removes all images not currently used by any container.
# 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
-i interactive (keep STDIN open), -t allocate TTY, -d detached (background), --name assign name, --rm auto-remove on exit.
# List running containers
[user@server ~]$ podman ps
CONTAINER ID IMAGE COMMAND STATUS 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
# 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
podman run which creates a new container, podman exec runs a command inside an already running container. Use -it for an interactive shell session.
# 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
podman logs -f follows output in real-time (like tail -f). podman stats shows live CPU, memory, network, and block I/O usage — useful for identifying resource issues.
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
# Set environment variable
[user@server ~]$ podman run -d --name mydb \
-e MYSQL_ROOT_PASSWORD=secret123 \
-e MYSQL_DATABASE=appdb \
docker.io/library/mysql:8.0
# Verify environment inside container
[user@server ~]$ podman exec mydb env | grep MYSQL
MYSQL_ROOT_PASSWORD=secret123
MYSQL_DATABASE=appdb
# Pass environment from file
[user@server ~]$ cat myenv.txt
DB_HOST=database.example.com
DB_USER=appuser
DB_PASSWORD=dbpass456
[user@server ~]$ podman run -d --name myapp --env-file myenv.txt myapp:1.0
# Inspect environment variables (visible in plain text!)
[user@server ~]$ podman inspect --format '{{.Config.Env}}' mydb
[MYSQL_ROOT_PASSWORD=secret123 MYSQL_DATABASE=appdb PATH=/usr/local/sbin:...]
# Better: use secrets for sensitive values
[user@server ~]$ printf "secret123" | podman secret create db-password -
[user@server ~]$ podman run -d --name mydb \
--secret db-password \
-e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password \
docker.io/library/mysql:8.0
podman inspect. Use podman secret or external secret managers for sensitive credentials in production.
Container filesystems are ephemeral — data written inside a container is lost when the container is removed. Use volumes or bind mounts to persist data.
:Z)# Create a named volume
[user@server ~]$ podman volume create dbdata
# Run container with named volume
[user@server ~]$ podman run -d --name mydb \
-v dbdata:/var/lib/mysql \
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
[{ "Name": "dbdata", "Mountpoint": "/home/user/.local/share/containers/storage/volumes/dbdata/_data" }]
# Remove volume
[user@server ~]$ podman volume rm dbdata
# Bind mount: map host directory to container
[user@server ~]$ mkdir -p ~/webdata
[user@server ~]$ podman run -d --name web \
-v ~/webdata:/var/www/html:Z \
-p 8080:80 \
registry.redhat.io/rhel9/httpd-24
# :Z sets SELinux label automatically (required on RHEL)
# :z shares the label across multiple containers
# Read-only bind mount
[user@server ~]$ podman run -d \
-v /etc/myapp.conf:/etc/myapp/config.conf:ro,Z \
myapp:1.0
:Z to bind mounts. Without it, SELinux blocks the container from accessing the host directory.
Podman creates a private network for containers. Containers can communicate with each other by name when on the same network.
# List networks
[user@server ~]$ podman network ls
NETWORK ID NAME VERSION PLUGINS
2f259bab93aa podman 0.4.0 bridge,portmap,firewall,tuning
# Create a custom network
[user@server ~]$ podman network create mynet
# Run containers on the same network
[user@server ~]$ podman run -d --name db --network mynet mysql:8.0
[user@server ~]$ podman run -d --name app --network mynet -e DB_HOST=db myapp:1.0
# Containers can reach each other by name
[user@server ~]$ podman exec app ping db
# Inspect a network
[user@server ~]$ podman network inspect mynet
[{
"name": "mynet",
"driver": "bridge",
"subnets": [{"subnet": "10.89.0.0/24", "gateway": "10.89.0.1"}],
...
}]
# Connect a running container to a network
[user@server ~]$ podman network connect mynet existingcontainer
# Disconnect from network
[user@server ~]$ podman network disconnect mynet existingcontainer
# Remove unused networks
[user@server ~]$ podman network prune
podman bridge network.
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
--new flag creates a service that builds a fresh container on each start rather than restarting an existing one — recommended for production deployments.
# 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
loginctl enable-linger, user services stop when the user logs out. With lingering enabled, services continue running even without an active session.
# 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/
# 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
--format with Go template syntax to extract specific values from podman inspect. This is especially useful in scripts that need to query container properties programmatically.
# 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
podman top shows processes scoped to a single container. podman diff shows filesystem changes relative to the original image — C = changed, A = added, D = deleted.
# 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
Podman: RHEL's daemonless, rootless container engine. Docker-compatible commands. Install with dnf install container-tools.
Images: podman search/pull/images/rmi. Use specific tags. Pull from trusted registries.
Containers: podman run/ps/stop/start/rm/exec/logs. Use -d for background, -p for ports, -e for environment, -v for volumes.
Services: podman generate systemd creates unit files. Enable lingering for user services. Use :Z for SELinux.
Next: Building Custom Container Images with Containerfiles