RED HAT ENTERPRISE LINUX

Container Management

Managing Containers with Podman

CIS238RH | RHEL System Administration
Mesa Community College

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 Architecture

Container A  |  Container B  |  Container C
Container Runtime (Podman)
Host Operating System (RHEL)
Hardware
All containers share the host kernel but are isolated from each other using Linux namespaces and cgroups.

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

Image to Container Workflow

Registry
Image
Container
Container
Container
You can create many containers from the same image. Each gets its own writable layer but shares the read-only image layers. Images are pulled from registries; containers are run locally.

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.

FeaturePodmanDocker
DaemonNo daemon requiredRequires dockerd daemon
Root accessRootless by defaultRequires root (or docker group)
Command syntaxdocker-compatibledocker commands
Systemd integrationNative supportLimited
Pod supportNative podsNo native pods

Installing Podman

# 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-tools is a meta-package that includes Podman, Buildah, Skopeo, and other utilities for a complete container toolchain.

Container Registries

Registries are servers that store and distribute container images. You pull images from registries and can push your own images to them.

RegistryURLDescription
Red Hat Registryregistry.redhat.ioOfficial Red Hat images (requires login)
Red Hat Catalogregistry.access.redhat.comCertified partner images
Quay.ioquay.ioRed Hat's public registry
Docker Hubdocker.ioPublic Docker registry

Registry Configuration

# 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
Full image names follow registry/namespace/image:tag. Without a tag, Podman pulls latest — but always use explicit version tags in production.

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

Listing Image Tags

# 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"]
}
Tip: Use 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.

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

Removing Images

# Remove an image
[user@server ~]$ podman rmi registry.redhat.io/ubi9/ubi:9.3

# Remove all unused images
[user@server ~]$ podman image prune -a
Caution: You cannot remove an image that has containers (running or stopped) based on it. Remove the containers first, or use -f to force removal. Use podman image prune -a carefully in production — it removes all images not currently used by any container.

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
Key options: -i interactive (keep STDIN open), -t allocate TTY, -d detached (background), --name assign name, --rm auto-remove on exit.

Container Lifecycle

# 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

Container State Flow

Image
Created
Running
Stopped
Removed
Containers can be referenced by ID (or partial ID) or by name. You can start and stop containers multiple times. When removed, the writable layer is deleted — the underlying image remains intact.

Executing Commands in Containers

# 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
Unlike podman run which creates a new container, podman exec runs a command inside an already running container. Use -it for an interactive shell session.

Monitoring Containers

# 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.

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

Port Mapping (continued)

# 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
Rootless note: Non-root users cannot bind ports below 1024. Use higher ports like 8080 instead of 80. Also open firewall ports on the host if external access is needed.

Environment Variables

# 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

Environment Variable Security

# 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
Security note: Environment variables are visible in plain text via podman inspect. Use podman secret or external secret managers for sensitive credentials in production.

Persistent Storage

Container filesystems are ephemeral — data written inside a container is lost when the container is removed. Use volumes or bind mounts to persist data.

Named Volumes

  • Managed by Podman
  • Stored in container storage
  • Portable and easy to backup
  • Preferred for application data

Bind Mounts

  • Map host directory into container
  • Full host path control
  • Useful for config files
  • Requires SELinux label (:Z)

Volume Commands

# 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 Mounts

# 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
SELinux: On RHEL, always append :Z to bind mounts. Without it, SELinux blocks the container from accessing the host directory.

Container Networking

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

Network Inspection

# 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
DNS resolution: On user-defined networks, containers resolve each other by container name. This is not available on the default podman bridge network.

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
The --new flag creates a service that builds a fresh container on each start rather than restarting an existing one — recommended for production deployments.

Enabling Container Services

# 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. With lingering enabled, services continue running even without an active session.

Rootless vs Root Containers

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 vs Root: Storage & Commands

# 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. Root and rootless containers are completely separate — they do not share images or containers.

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
Use --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.

Container Process & Diff Inspection

# 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.

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

Best Practices: Example

# 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
This single command demonstrates multiple best practices: named container, resource limits, persistent volume with SELinux label, secret-file credential injection, and a pinned image version tag.

Key Takeaways

1

Podman: RHEL's daemonless, rootless container engine. Docker-compatible commands. Install with dnf install container-tools.

2

Images: podman search/pull/images/rmi. Use specific tags. Pull from trusted registries.

3

Containers: podman run/ps/stop/start/rm/exec/logs. Use -d for background, -p for ports, -e for environment, -v for volumes.

4

Services: podman generate systemd creates unit files. Enable lingering for user services. Use :Z for SELinux.

Graded Lab Exercises

HANDS-ON PRACTICE

  • Pull images from registry.redhat.io and run a web server container
  • Map ports and access container service from host
  • Use volumes to persist data across container restarts
  • Configure container with environment variables
  • Create systemd service for container with auto-restart
  • Practice rootless container management as regular user

Next: Building Custom Container Images with Containerfiles