Podman Series Overview
This series explores Podman from the basics to more advanced features, focusing on rootless containers, networking, and pods. Along the way, we demonstrate how to replace Docker in a real-world setup, step by step.
- #1 – Getting Started: Running rootful containers that survive reboots using Quadlets.
- #2 – Rootless: Running containers as a regular user, including lingering and systemd integration.
- #3 – Cheat Sheet: Rootful vs rootless commands for easy reference.
- #4 – Networking: Creating isolated and internal networks for secure container communication.
- #5 – Pods: Running multiple containers in the same network namespace, Kubernetes-style.
- #6 – WordPress in a Pod: Deploying a real application with multiple containers, secrets, and dependencies.
- #7 – Enough, Just Give Me the Files: All Quadlets in one place, with paths, start commands, and troubleshooting tips for rootless pods.
Each post builds on the previous ones, showing practical examples and systemd integration, so you can confidently move from Docker to Podman in your own environment.
If you run into issues or have questions, reach out via Bluesky: @netsecu.red.
Podman #7 – Enough, Just Give Me the Files
By now, we’ve gone through rootful/rootless containers, networking, pods, and even WordPress. Sometimes you just want the files, put them in the right place, and start things. Here’s a “all-in-one” reference for your Quadlets.
0. Enable User Linger
Before rootless services can run when you’re not logged in, enable lingering for your user:
sudo loginctl enable-linger $UID
0b. Create Required Directories
Create the folders for Quadlets and persistent container storage:
mkdir -p ~/.config/containers/systemd
mkdir -p ~/.local/share/containers/storage/volumes/wp-mariadb
mkdir -p ~/.local/share/containers/storage/volumes/wp-html
0c. Create Secrets
Before starting the containers, create the secrets required for MariaDB and WordPress:
# MariaDB root password
echo -n "example-root-pw" | podman secret create blog_db_rootpassword -
# Database name
echo -n "wordpress" | podman secret create blog_db_name -
# Database user
echo -n "wpuser" | podman secret create blog_db_user -
# User password
echo -n "example-wp-pw" | podman secret create blog_db_password -
Verify the secrets:
podman secret ls
1. Network Quadlet
nano ~/.config/containers/systemd/intisostrictnet.network
[Unit]
Description=Isolated internal network
[Network]
Driver=bridge
Internal=true
Options=isolate=strict
[Install]
WantedBy=default.target
2. Pod Quadlet
nano ~/.config/containers/systemd/podracer.pod
[Unit]
Description=It goes brrrrr
[Pod]
PublishPort=8080:80
Network=intisostrictnet.network
[Install]
WantedBy=default.target
3. MariaDB Container
nano ~/.config/containers/systemd/mariadb.container
[Unit]
Description=MariaDB container for WordPress
PartOf=podracer.pod
[Container]
Image=docker.io/library/mariadb:11
Pod=podracer.pod
AutoUpdate=registry
StartWithPod=true
NoNewPrivileges=true
ContainerName=mariadb
Volume=%h/.local/share/containers/storage/volumes/wp-mariadb:/var/lib/mysql:z
Secret=blog_db_name,type=env,target=MYSQL_DATABASE
Secret=blog_db_user,type=env,target=MYSQL_USER
Secret=blog_db_password,type=env,target=MYSQL_PASSWORD
Secret=blog_db_rootpassword,type=env,target=MARIADB_ROOT_PASSWORD
[Service]
Restart=always
4. WordPress Container
nano ~/.config/containers/systemd/wordpress.container
[Unit]
Description=WordPress container
PartOf=podracer.pod
After=mariadb.service
[Container]
Image=docker.io/library/wordpress:latest
Pod=podracer.pod
AutoUpdate=registry
StartWithPod=true
NoNewPrivileges=true
ContainerName=wordpress
Volume=%h/.local/share/containers/storage/volumes/wp-html:/var/www/html:z
Secret=blog_db_name,type=env,target=WORDPRESS_DB_NAME
Secret=blog_db_user,type=env,target=WORDPRESS_DB_USER
Secret=blog_db_password,type=env,target=WORDPRESS_DB_PASSWORD
Environment=WORDPRESS_DB_HOST=127.0.0.1
[Service]
Restart=always
5. Start Everything
Reload systemd and start the services:
systemctl --user daemon-reload
Check if the service was created:
ls /run/user/$UID/systemd/generator/
systemctl --user start intisostrictnet-network.service
systemctl --user start podracer-pod.service
The containers will start automatically with the pod as specified in the Quadlet.
systemctl --user start mariadb.service
systemctl --user start wordpress.service
6. Troubleshoot
If something doesn’t start, check the generator:
/usr/lib/systemd/user-generators/podman-user-generator -dryrun
Follow logs with:
journalctl -fe
Or check container logs directly:
podman logs <container_name>
7. Access WordPress
If everything started correctly, open your browser and go to http://localhost:8080
to configure WordPress.
Podman #6 - Putting WordPress in a Pod
For this example, I wanted to pick something everyone recognizes: WordPress. It’s simple to deploy, has two containers that need to talk to each other, and is perfect for testing. We’ll need a MariaDB container and a WordPress container.
Step 1: Create Secrets
Like Kubernetes and Docker Swarm, Podman has the concept of secrets. We don’t want to store database passwords in an environment file, so we can use secrets:
# MariaDB root password
echo -n "example-root-pw" | podman secret create blog_db_rootpassword -
# Database name
echo -n "wordpress" | podman secret create blog_db_name -
# Database user
echo -n "wpuser" | podman secret create blog_db_user -
# User password
echo -n "example-wp-pw" | podman secret create blog_db_password -
Check your secrets with:
podman secret ls
Step 2: Create the MariaDB Quadlet
Now we create the MariaDB container Quadlet:
nano ~/.config/containers/systemd/mariadb.container
[Unit]
Description=MariaDB container for WordPress
PartOf=podracer.pod
[Container]
Image=docker.io/library/mariadb:11
Pod=podracer.pod
AutoUpdate=registry
StartWithPod=true
NoNewPrivileges=true
ContainerName=mariadb
Volume=%h/.local/share/containers/storage/volumes/wp-mariadb:/var/lib/mysql:z
Secret=blog_db_name,type=env,target=MYSQL_DATABASE
Secret=blog_db_user,type=env,target=MYSQL_USER
Secret=blog_db_password,type=env,target=MYSQL_PASSWORD
Secret=blog_db_rootpassword,type=env,target=MARIADB_ROOT_PASSWORD
[Service]
Restart=always
The target
values above correspond to the environment variable names inside the container.
Also, Podman doesn’t create mapped folders for you. Systemd resolves %h
to your home directory:
mkdir -p ~/.local/share/containers/storage/volumes/wp-mariadb
mkdir -p ~/.local/share/containers/storage/volumes/wp-html
Reload systemd and start the container:
systemctl --user daemon-reload
systemctl --user start mariadb.service
If the container fails to start, you can follow logs with journalctl -fe
or podman logs mariadb
.
Check if the service was created:
ls /run/user/$UID/systemd/generator/
If it isn’t there, troubleshoot using:
/usr/lib/systemd/user-generators/podman-user-generator -dryrun
Step 3: Create the WordPress Quadlet
nano ~/.config/containers/systemd/wordpress.container
[Unit]
Description=WordPress container
PartOf=podracer.pod
After=mariadb.service
[Container]
Image=docker.io/library/wordpress:latest
Pod=podracer.pod
AutoUpdate=registry
StartWithPod=true
NoNewPrivileges=true
ContainerName=wordpress
Volume=%h/.local/share/containers/storage/volumes/wp-html:/var/www/html:z
# Inject database secrets as environment variables
Secret=blog_db_name,type=env,target=WORDPRESS_DB_NAME
Secret=blog_db_user,type=env,target=WORDPRESS_DB_USER
Secret=blog_db_password,type=env,target=WORDPRESS_DB_PASSWORD
# Connect to MariaDB by container name
Environment=WORDPRESS_DB_HOST=127.0.0.1
[Service]
Restart=always
The key points here are:
After=mariadb.service
ensures WordPress starts only after MariaDB.WORDPRESS_DB_HOST
is set to127.0.0.1
, connecting to the database on localhost.
Reload systemd and start WordPress:
systemctl --user daemon-reload
systemctl --user start wordpress.service
If everything started correctly, you can browse to your host on port 8080 (as specified in the pod Quadlet) and see WordPress running.
Extra Options: StartWithPod and AutoUpdate
The configuration option StartWithPod=true
helps to avoid having to manually start each container. Once the pod is started, the containers will automatically follow. This makes pod-level orchestration much smoother.
Another useful setting is AutoUpdate=registry
. This ties into the podman-auto-update.service
which runs daily at midnight by default (though this schedule can be changed). It will pull the latest image of the container and apply updates. You can preview this behavior with:
podman auto-update --dry-run
And you can trigger it manually with:
podman auto-update
Enable the service for automatic updates
systemctl --user enable --now podman-auto-update.timer
You can check the timer to verify the schedule of the service
systemctl --user list-timers podman-auto-update.timer
Container update history can be followed with
journalctl --user -u podman-auto-update.service
This ensures your containers stay up to date with minimal effort.
https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html
Podman #5 - Pods - Rootless
We’ve already looked at .container
and .network
Quadlets. The next step is pods. Pods are multiple containers sharing the same localhost (network namespace), similar to how they work in Kubernetes. It’s a really handy feature.
Documentation is, as usual, a bit lacking. Googling gives you snippets and outdated examples from pre-Podman 5.x that won’t work. Here’s a working example:
Step 1: Create the Pod Quadlet
nano ~/.config/containers/systemd/podracer.pod
[Unit]
Description=It goes brrrrr
[Pod]
PublishPort=8080:80
Network=intisostrictnet.network
[Install]
WantedBy=default.target
Keep in mind that intisostrictnet.network
was created in the previous post. Publishing a port from the pod is important, especially for production scenarios.
Step 2: Generate the Systemd Service
systemctl --user daemon-reload
Check if the service was created:
ls /run/user/$UID/systemd/generator/
If it isn’t there, troubleshoot using:
/usr/lib/systemd/user-generators/podman-user-generator -dryrun
Step 3: Start the Pod
systemctl --user start podracer-pod.service
Now podman pod ls
will show your newly created pod.
Running podman ps -p
also shows a dummy container called localhost/podman-pause
. This container keeps the pod alive and is always present in the pod.
Step 4: Add Containers to the Pod
At this point, you have a rootless pod with a user-created network. You can now deploy one or more containers inside this pod, all sharing the same network namespace, just like in Kubernetes. This allows them to communicate over localhost, and you can also publish ports from the pod to your host.
podman run --rm -it --pod systemd-podracer docker.io/library/alpine:latest sh
This completes the setup of a basic rootless pod with Podman.
https://docs.podman.io/en/latest/markdown/podman-pod-create.1.html