Podman Series Overview

September 20, 2025

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.

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

September 24, 2025

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

September 23, 2025

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 to 127.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

September 22, 2025

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