Skip to content

Expert · Boot, recovery and containers · topic 43 of 57 · 35 min

Persistent storage and containers as systemd services

Explanation

Data inside a container disappears with it, so mount a host directory with -v HOST:CONTAINER:Z; the :Z option gives it the right SELinux label. To start a rootless container at boot, run it as a systemd user service: on current RHEL 9 with a Quadlet .container file (or the older podman generate systemd), and enable lingering with loginctl enable-linger.

Keeping data outside the container

A container's writable layer is thrown away when the container is removed. A database or upload folder must live on the host. A bind mount maps a host directory into the container: -v /srv/web:/var/www/html. Podman also has named volumes (podman volume create), stored under the user's container storage; bind mounts are easier to back up and inspect.

SELinux and the :Z option

Container processes run with the SELinux type container_t, which may only use files labelled for containers (container_file_t). A normal host directory has another label, so the container gets "Permission denied". Adding :Z to the volume tells Podman to relabel the directory privately for this container. Lower-case :z labels it so several containers can share it.

bash
$ mkdir -p ~/web-content
$ echo "Namaste from the host" > ~/web-content/index.html
$ podman run -d --name web -p 8080:8080 -v ~/web-content:/var/www/html:Z registry.access.redhat.com/ubi9/httpd-24
$ ls -Zd ~/web-content
system_u:object_r:container_file_t:s0:c215,c840 /home/asha/web-content
$ curl localhost:8080
Namaste from the host
Without :Z the page would give 403 and an AVC denial for container_t.
Careful: Never use :Z on system directories such as /home or /etc; it would relabel them for one container and break the host. Make a dedicated directory for each container's data.

Rootless containers map user IDs: root inside the container is your own UID outside, and other IDs inside map to a private range from /etc/subuid. If an image runs as a non-root user, files it writes on the host may show a high UID number. podman unshare chown UID:GID dir changes ownership as the container sees it.

Starting a container at boot: Quadlet

A container started by hand does not come back after a reboot. The modern RHEL 9 way (Podman 4.4 and later) is Quadlet: you write a small .container file, and systemd generates a service from it. For a rootless container the file goes in ~/.config/containers/systemd/; for a root container, in /etc/containers/systemd/.

ini
# ~/.config/containers/systemd/web.container
[Unit]
Description=Company website container

[Container]
Image=registry.access.redhat.com/ubi9/httpd-24
ContainerName=web
PublishPort=8080:8080
Volume=%h/web-content:/var/www/html:Z

[Install]
WantedBy=default.target
%h means the user's home directory. The [Install] section makes it start with the user's systemd instance.
bash
$ systemctl --user daemon-reload          # Quadlet generates web.service
$ systemctl --user start web.service
$ systemctl --user status web.service --no-pager | head -3
● web.service - Company website container
     Loaded: loaded (/home/asha/.config/containers/systemd/web.container; generated)
     Active: active (running) since Mon 2026-10-19 18:02:11 IST; 4s ago
$ loginctl enable-linger asha              # start asha's services at boot, without a login
$ loginctl show-user asha | grep Linger
Linger=yes
Generated units cannot be "enabled"; the [Install] section in the .container file does that job.

The older way: podman generate systemd

Earlier RHEL 9 releases, and many exam guides, use podman generate systemd --name web --files --new to write a unit file from an existing container, then copy it to ~/.config/systemd/user/ and run systemctl --user enable --now container-web.service. This still works but is deprecated in favour of Quadlet. Check the current exam objectives and use whatever method they expect; both give a systemd user service plus enable-linger.

Common mistakes

  • Using su - asha and then systemctl --user: it fails with "Failed to connect to bus". Log in as the user over SSH (or a real login) so the user's systemd and XDG_RUNTIME_DIR exist.
  • Forgetting loginctl enable-linger: the container starts when the user logs in and stops when they log out, not at boot.
  • Leaving out :Z and getting Permission denied, or using it on a shared system directory.
  • Editing the .container file and forgetting systemctl --user daemon-reload.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Persistent storage and containers as systemd services · 1/7

-v HOST_DIR:CONTAINER_DIR

Data inside a container is lost when it is removed. Keep it on the host with a volume.

Prefer a video by a teacher? Find free videos on “podman quadlet rootless systemd user service enable-linger rhel 9” on YouTube ↗

In real life: A survey app for an NGO in Odisha

An NGO in Odisha collects field survey data through a small web app. A volunteer admin runs it as a rootless container under a dedicated user, with its SQLite data in /home/survey/data mounted with :Z. A Quadlet file and enable-linger make it start after every power cut without anyone logging in, and the nightly backup simply copies the data directory.

bash
$ cat ~/.config/containers/systemd/survey.container
[Container]
Image=registry.example.org/ngo/survey:1.4
PublishPort=8080:8000
Volume=%h/data:/app/data:Z
[Install]
WantedBy=default.target
$ loginctl show-user survey -p Linger
Linger=yes

Practice (0/4 done)

Task 1 · Question

A container gets Permission denied on a mounted host folder, and ausearch shows container_t. Fix?

Task 2 · Question

What does loginctl enable-linger asha do?

Task 3 · Code · Python

Write parse_volume(spec) for a podman -v value "HOST:CONTAINER[:OPTIONS]". Return a dict with host, container and options (a list, empty if none). Also add "selinux": "private" if options include "Z", "shared" if they include "z", else None. Example: "/srv/web:/var/www/html:Z,ro" → {"host": "/srv/web", "container": "/var/www/html", "options": ["Z", "ro"], "selinux": "private"}.

Task 4 · Practice

As a user named "webops" (log in over SSH, not with su), run the UBI httpd container with content from ~/web-content mounted with :Z on port 8080, make it a systemd user service with Quadlet, enable lingering, reboot, and confirm it is running before anyone logs in.

Quick quiz

Question 1

1.What is the difference between :Z and :z on a volume?

Question 2

2.Where does a rootless Quadlet .container file go?

Question 3

3.Why does systemctl --user fail after su - webops?

Question 4

4.What happens to files written inside a container (not in a volume) after podman rm?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the difference between :Z and :z on a volume?

Answer: :Z labels it for this container only; :z lets several containers share it

Private versus shared SELinux labels.

Where does a rootless Quadlet .container file go?

Answer: ~/.config/containers/systemd/

Root-level files go in /etc/containers/systemd/.

Why does systemctl --user fail after su - webops?

Answer: su does not start a full login session with the user's systemd and runtime directory

Log in directly (SSH) as the user.

What happens to files written inside a container (not in a volume) after podman rm?

Answer: They are deleted with the container

The container layer is removed.

Why can't a rootless container publish host port 80 by default?Related

Answer: Ports below 1024 need root privileges

Unprivileged users cannot bind low ports by default.

From Containers with Podman: images, run, ports and logs

A service shows "active" and "disabled". What happens after a reboot?Related

Answer: It does not start

Disabled means it is not started at boot.

From systemd services and systemctl

What happens to files a container wrote into its own file system when you run docker rm on it?Related

Answer: They are deleted with the container

The writable layer belongs to the container. Use a volume for data that must survive.

From Running containers: ports, volumes, networks and limits

What does Persistent=true do in a timer?Related

Answer: Runs a missed job soon after boot if the machine was off at the scheduled time

It catches up on missed runs.

From One-time jobs with at, and systemd timers

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Boot, recovery and containers”. The module exam comes next.
  2. Module 8 exam: Boot, recovery and containers · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Ansible fundamentals. 57 topics left in the course before the final exam.

← Previous: Containers with Podman: images, run, ports and logs