Podman stores container images and layers under /var/lib/containers/storage by default. SELinux labels that path with the container_var_lib_t type, which allows the container runtime full access. When you move Podman storage to a custom directory – a separate disk, an NFS mount, or just a different partition – SELinux blocks access because the new path carries a generic label like default_t. Setting the correct SELinux context on custom storage directories is the fix.
This guide walks through configuring SELinux contexts for Podman storage on Rocky Linux 10, AlmaLinux 10, and RHEL 10. We cover custom graphroot and runroot paths, volume bind mount labels, and the :Z and :z mount flags.
Why Podman Needs Custom SELinux Contexts
SELinux enforces mandatory access control by labeling every file, directory, and process with a security context. Podman’s default storage at /var/lib/containers carries the container_var_lib_t label, and the runtime directory at /run/containers uses container_runtime_tmpfs_t. These labels grant Podman the permissions it needs to pull images, create overlay layers, and launch containers.
When you point Podman at a custom directory – say /data/containers on a dedicated disk – that directory inherits whatever label its parent has (often default_t or var_t). SELinux denies access, and you get errors like “permission denied” or “cannot change memory protections” even though standard Unix permissions look fine. The solution is to assign the correct SELinux type to the custom path using semanage fcontext and restorecon.
Prerequisites
- Rocky Linux 10, AlmaLinux 10, or RHEL 10 with SELinux in enforcing mode
- Root or sudo access
- Podman installed (ships by default on RHEL family systems)
- The
policycoreutils-python-utilspackage (providessemanage)
Install the required SELinux management tools if they are not already present:
sudo dnf install -y policycoreutils-python-utils
Confirm SELinux is enforcing:
getenforce
The output should show Enforcing. If it shows Permissive or Disabled, enable it in /etc/selinux/config and reboot before continuing. For more on managing SELinux modes and policies, see our guide on troubleshooting SELinux on Rocky Linux 10 / AlmaLinux 10.
Step 1: Check Current Podman Storage Configuration
Before making changes, check where Podman currently stores data. Run podman info and filter for the storage paths:
podman info | grep -E 'graphRoot|runRoot|graphDriverName'
On a default installation, you should see paths under /var/lib/containers and /run/containers:
graphDriverName: overlay
graphRoot: /var/lib/containers/storage
runRoot: /run/containers/storage
You can also view the storage configuration file directly:
sudo cat /etc/containers/storage.conf | grep -E '^graphroot|^runroot'
The graphroot setting controls where images, containers, and layers are stored on disk. The runroot setting controls where runtime state (lock files, temporary mounts) goes. Both paths need correct SELinux labels when changed to custom locations.
Step 2: Create Custom Storage Directory
Create the directory where Podman will store container data. This is typically on a separate partition or disk with enough space for your container images:
sudo mkdir -p /data/containers/storage
Check the current SELinux label on the new directory:
ls -Zd /data/containers/storage
The output shows the default label, which is not suitable for Podman:
unconfined_u:object_r:default_t:s0 /data/containers/storage
The default_t type tells us SELinux has no specific policy for this path. Podman will fail with permission errors if we try to use it as-is.
Step 3: Set SELinux Context for graphroot
Use semanage fcontext to add a persistent file context rule that labels the custom directory and all its contents with the container_var_lib_t type. This is the same type used on the default /var/lib/containers path:
sudo semanage fcontext -a -t container_var_lib_t "/data/containers/storage(/.*)?"
This adds a rule to the local SELinux policy. The regex (/.*)? ensures the label applies to the directory and everything inside it. Now apply the label to existing files:
sudo restorecon -Rv /data/containers/storage
The restorecon command relabels files based on the policy rules. You should see output confirming the context change:
Relabeled /data/containers/storage from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:container_var_lib_t:s0
Verify the label is set correctly:
ls -Zd /data/containers/storage
The output should now show container_var_lib_t:
unconfined_u:object_r:container_var_lib_t:s0 /data/containers/storage
An alternative approach uses semanage fcontext -a -e to copy the context from an existing path. This maps the new directory to match the labels of /var/lib/containers exactly:
sudo semanage fcontext -a -e /var/lib/containers /data/containers
Both methods achieve the same result. The -t container_var_lib_t approach is more explicit and easier to audit later.
Step 4: Set SELinux Context for runroot
If you are also changing the runroot path (the runtime state directory), it needs the container_runtime_tmpfs_t type. This directory holds lock files, temporary mounts, and other runtime data.
Create the custom runroot directory:
sudo mkdir -p /data/containers/run
Add the SELinux file context rule:
sudo semanage fcontext -a -t container_runtime_tmpfs_t "/data/containers/run(/.*)?"
Apply the label:
sudo restorecon -Rv /data/containers/run
Verify the label shows the correct type:
ls -Zd /data/containers/run
The output confirms the runtime label is applied:
unconfined_u:object_r:container_runtime_tmpfs_t:s0 /data/containers/run
If you are only changing graphroot and keeping runroot at the default /run/containers/storage, skip this step.
Step 5: Update Podman Storage Configuration
With the SELinux labels in place, update the Podman storage configuration to use the custom paths. First, stop any running containers and reset Podman storage:
sudo podman system reset --force
This removes all existing images, containers, and volumes from the old storage location. Back up any important data first. Now edit the storage configuration file:
sudo vi /etc/containers/storage.conf
Find the [storage] section and update the graphroot and optionally the runroot values:
[storage]
driver = "overlay"
graphroot = "/data/containers/storage"
runroot = "/data/containers/run"
Save the file and verify Podman picks up the new configuration:
podman info | grep -E 'graphRoot|runRoot'
The output should reflect the custom paths:
graphRoot: /data/containers/storage
runRoot: /data/containers/run
Step 6: Verify Podman Works with Custom Storage
Pull a test image to confirm that Podman can write to the new storage directory without SELinux denials:
sudo podman pull docker.io/library/alpine:latest
The pull should complete without errors. Verify the image is stored in the custom location:
sudo podman images
You should see the Alpine image listed:
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/library/alpine latest a606584aa9aa 2 weeks ago 8.05 MB
Now run a test container to verify full functionality:
sudo podman run --rm alpine cat /etc/os-release
The container should start, print the Alpine release info, and exit cleanly. If it does, SELinux is properly configured for the custom storage path. You can also confirm that data physically lives in the new directory:
ls /data/containers/storage/overlay-images/
This directory should contain the Alpine image layers. If you want to run containers as systemd services, the same SELinux labels apply – Podman generates unit files that reference the configured storage paths.
SELinux Contexts for Container Volumes
Beyond storage directories, SELinux also controls access to bind-mounted volumes. When you mount a host directory into a container with -v /host/path:/container/path, the container process needs permission to read (and possibly write) that host directory. SELinux denies this by default because the container runs in the container_t domain, which cannot access arbitrary host paths.
The container_file_t type is the label that grants containers access to bind-mounted directories. You can set it permanently with semanage:
sudo semanage fcontext -a -t container_file_t "/srv/appdata(/.*)?"
sudo restorecon -Rv /srv/appdata
However, Podman provides a more convenient approach with the :Z and :z volume mount flags that handle SELinux labeling automatically.
The :z flag (lowercase) relabels the volume content with a shared label. Multiple containers can access the same volume simultaneously:
sudo podman run -v /srv/appdata:/data:z alpine ls /data
The :Z flag (uppercase) relabels the volume with a private label. Only the specific container can access it. This is more secure but prevents sharing the volume between containers:
sudo podman run -v /srv/appdata:/data:Z alpine ls /data
Use :Z for single-container volumes (database data directories, application configs). Use :z for shared volumes that multiple containers read from (static assets, shared logs). Never use :Z on system directories like /etc or /home – it relabels them and can break your host system.
The following table summarizes the key SELinux types used with Podman:
| SELinux Type | Purpose |
|---|---|
container_var_lib_t | Podman image and layer storage (graphroot) |
container_runtime_tmpfs_t | Podman runtime state and locks (runroot) |
container_file_t | Host directories bind-mounted into containers |
container_t | Process domain for running containers |
container_log_t | Container log files |
When building container images with tools like Buildah, the same SELinux context rules apply to the build storage directories.
Troubleshooting SELinux Denials with Podman
When SELinux blocks Podman operations, the denial is logged to the audit log. Check for recent denials related to containers:
sudo ausearch -m avc -ts recent | grep container
For a human-readable explanation of why a denial occurred, pipe the audit log through audit2why:
sudo ausearch -m avc -ts recent | audit2why
The output explains the denial reason and often suggests the fix. A typical missing context denial looks like this:
type=AVC msg=audit(1711234567.123:456): avc: denied { write } for pid=12345 comm="conmon" name="storage" dev="sda1" ino=67890 scontext=system_u:system_r:container_runtime_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=dir permissive=0
Was caused by:
Missing type enforcement (TE) allow rule.
You can use audit2allow to generate a loadable module to allow this access.
The key detail is tcontext=...default_t – this tells you the target directory has the wrong SELinux type. The fix is to set the correct label as shown in Steps 3 and 4 above.
If you suspect SELinux is causing a problem but are not sure, temporarily switch to permissive mode to test:
sudo setenforce 0
If Podman works in permissive mode but fails in enforcing mode, the problem is definitely SELinux-related. Check the audit log for the specific denial and fix the label. Switch back to enforcing when done:
sudo setenforce 1
Other common issues and fixes:
- Containers cannot write to bind-mounted volumes – add the
:Zor:zflag to the volume mount, or manually setcontainer_file_ton the host path - “error while loading shared libraries” inside containers – the graphroot directory has the wrong SELinux label. Verify with
ls -Zdand fix withrestorecon - Containers work as root but fail rootless – rootless Podman uses
~/.local/share/containerswhich should inherituser_home_t. Check that your home directory is not on an NFS mount with different SELinux behavior - Labels reset after reboot or restorecon – your
semanage fcontextrule may be missing or incorrect. Verify withsemanage fcontext -l | grep containers - SELinux boolean restrictions – some container features require SELinux booleans. Check with
getsebool -a | grep containerand enable as needed withsetsebool -P container_manage_cgroup on
For detailed SELinux debugging techniques including audit2allow and custom policy modules, see the Red Hat SELinux documentation. For SELinux port management (useful when containers expose custom ports), our guide on changing SSH ports with SELinux covers the semanage port workflow.
Conclusion
Custom Podman storage directories need two things to work under SELinux: the container_var_lib_t label on graphroot and container_runtime_tmpfs_t on runroot. For bind-mounted volumes, use the :Z or :z flags to let Podman handle labeling automatically, or set container_file_t manually for persistent labels.
In production, always keep SELinux in enforcing mode and fix denials properly with semanage fcontext and restorecon rather than switching to permissive. Regularly audit your container SELinux contexts with ls -Z and check for denials in /var/log/audit/audit.log to catch misconfigurations early.