Linux Tutorial

Set Up Podman Quadlet for Systemd-Managed Containers on Fedora 39

5views

Podman Quadlet revolutionizes how containers integrate with systemd. Instead of manually creating service files or using the deprecated podman generate systemd command, Quadlet provides a declarative way to manage containers as native systemd services. This guide shows you how to set up and use Podman Quadlet on Fedora 39.

Time Required: 25-35 minutes
Difficulty Level: Intermediate
Prerequisites:

  • Fedora 39 with sudo access
  • Basic understanding of containers and systemd
  • Podman 4.4+ (included in Fedora 39)

What is Podman Quadlet?

Quadlet is a tool merged into Podman 4.4 that simplifies running containers under systemd using a systemd generator. Unlike docker-compose, Quadlet uses systemd’s native features to manage containers, networks, and volumes through dedicated unit files.

Key Benefits:

  • Declarative container definitions
  • Native systemd integration
  • Automatic service generation
  • Support for dependencies between containers
  • Auto-update capabilities
  • No daemon required

Step 1: Verify Podman Installation

Check your Podman version (must be 4.4 or higher):

podman --version

If you need to update:

sudo dnf update podman -y

Verify Quadlet is available:

ls /usr/libexec/podman/quadlet

Step 2: Understand Quadlet Directory Structure

Quadlet unit files are placed in specific directories:

System-wide (rootful) containers:

/etc/containers/systemd/

User-specific (rootless) containers:

~/.config/containers/systemd/

Create the user directory:

mkdir -p ~/.config/containers/systemd

For this guide, we’ll use rootless containers (recommended for security).


Step 3: Create Your First Quadlet Container

Let’s create a simple web server container using Nginx.

Create the container unit file:

nano ~/.config/containers/systemd/nginx.container

Add this configuration:

[Unit]
Description=Nginx Web Server
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/nginx:latest
ContainerName=nginx-web
PublishPort=8080:80
Volume=%h/nginx-html:/usr/share/nginx/html:Z

[Install]
WantedBy=default.target

Configuration Breakdown:

  • [Unit] section: Standard systemd unit options
  • Image: Container image to use
  • ContainerName: Name for the running container
  • PublishPort: Map host port to container port (host:container)
  • Volume: Mount host directory (%h = home directory)
  • [Install] section: Determines when the service starts

Step 4: Reload Systemd and Start the Container

Reload systemd to discover new Quadlet files:

systemctl --user daemon-reload

For systemd to discover the new service file, you need to run systemctl daemon-reload.

Start the container:

systemctl --user start nginx

Check status:

systemctl --user status nginx

Verify container is running:

podman ps

You should see a container named systemd-nginx-web.


Step 5: Enable Container on Boot

To automatically start the container at boot:

systemctl --user enable nginx

For rootless containers to start at boot without user login:

loginctl enable-linger $USER

Verify linger is enabled:

loginctl show-user $USER | grep Linger

Should show: Linger=yes


Step 6: Create a Multi-Container Stack

Let’s set up a WordPress site with MariaDB. We’ll need two containers with dependencies.

Create MariaDB Container

nano ~/.config/containers/systemd/wordpress-db.container

Add:

[Unit]
Description=WordPress Database
After=network-online.target

[Container]
Image=docker.io/mariadb:latest
ContainerName=wordpress-db
Environment=MYSQL_ROOT_PASSWORD=rootpass123
Environment=MYSQL_DATABASE=wordpress
Environment=MYSQL_USER=wpuser
Environment=MYSQL_PASSWORD=wppass123
Volume=%h/wordpress-db:/var/lib/mysql:Z

[Install]
WantedBy=default.target

Create WordPress Container

nano ~/.config/containers/systemd/wordpress.container

Add:

[Unit]
Description=WordPress Application
After=network-online.target
Requires=wordpress-db.service
After=wordpress-db.service

[Container]
Image=docker.io/wordpress:latest
ContainerName=wordpress-app
PublishPort=8081:80
Environment=WORDPRESS_DB_HOST=wordpress-db:3306
Environment=WORDPRESS_DB_USER=wpuser
Environment=WORDPRESS_DB_PASSWORD=wppass123
Environment=WORDPRESS_DB_NAME=wordpress
Network=wordpress.network

[Install]
WantedBy=default.target

Create Network for Communication

nano ~/.config/containers/systemd/wordpress.network

Add:

[Unit]
Description=WordPress Network

[Network]

This creates a Podman network named systemd-wordpress.


Step 7: Start the Multi-Container Stack

Reload systemd:

systemctl --user daemon-reload

Start the database first:

systemctl --user start wordpress-db

Start WordPress (dependencies will auto-start the database):

systemctl --user start wordpress

Check both services:

systemctl --user status wordpress-db
systemctl --user status wordpress

Access WordPress at: http://localhost:8081


Step 8: Configure Container Volumes

Quadlet supports multiple volume types.

Bind mount (host directory):

Volume=/host/path:/container/path:Z

Named volume:

Create volume unit file:

nano ~/.config/containers/systemd/app-data.volume

Add:

[Unit]
Description=Application Data Volume

[Volume]

Reference in container:

Volume=app-data.volume:/data:Z

SELinux context flags:

  • :Z – Private unshared label
  • :z – Shared label
  • :ro – Read-only

Step 9: Implement Auto-Updates

Podman provides auto-update functionality out of the box by setting AutoUpdate=registry.

Add to your container unit:

[Container]
Image=docker.io/nginx:latest
AutoUpdate=registry

Reload and enable the container:

systemctl --user daemon-reload
systemctl --user start nginx

Run auto-update manually:

podman auto-update

Set up automatic updates with a timer:

systemctl --user enable --now podman-auto-update.timer

Check timer status:

systemctl --user list-timers podman-auto-update.timer

Step 10: Advanced Quadlet Features

Environment Files

Store sensitive data in separate files:

nano ~/.config/containers/env/db.env

Add:

MYSQL_ROOT_PASSWORD=secret123
MYSQL_DATABASE=myapp

Reference in container:

[Container]
Image=docker.io/mariadb:latest
EnvironmentFile=%h/.config/containers/env/db.env

Health Checks

Add health monitoring:

[Container]
Image=docker.io/nginx:latest
HealthCmd=/usr/bin/curl -f http://localhost/ || exit 1
HealthInterval=30s
HealthRetries=3
HealthStartPeriod=60s

Resource Limits

Limit container resources:

[Container]
Image=docker.io/nginx:latest
Memory=512M
MemorySwap=1G
CPUQuota=50%

Labels

Add metadata:

[Container]
Image=docker.io/nginx:latest
Label=app=webserver
Label=version=1.0

Step 11: Manage Quadlet Containers

View all container services:

systemctl --user list-units --type=service | grep container

Stop a container:

systemctl --user stop nginx

Restart a container:

systemctl --user restart nginx

View logs:

journalctl --user -u nginx -f

Or with Podman:

podman logs -f systemd-nginx-web

Disable auto-start:

systemctl --user disable nginx

Step 12: Debug Quadlet Configuration

View generated systemd unit:

/usr/libexec/podman/quadlet -dryrun -user

This dumps the automatically generated systemd unit to your console.

Check for syntax errors:

systemctl --user daemon-reload

Any errors will be displayed during reload.

View container details:

podman inspect systemd-nginx-web

Troubleshooting

Container Not Starting

Check service status:

systemctl --user status nginx

View detailed logs:

journalctl --user -u nginx -n 50

Verify image exists:

podman images

Permission Denied on Volumes

Ensure correct SELinux context:

Volume=/path/to/data:/data:Z

Check directory permissions:

ls -la /path/to/data

Containers Can’t Communicate

Verify network configuration:

podman network ls
podman network inspect systemd-wordpress

Ensure both containers use the same network.

Auto-Update Not Working

Check if label is set:

podman inspect systemd-nginx-web | grep io.containers.autoupdate

Manually run update:

podman auto-update --dry-run

Real-World Examples

Example 1: Redis Cache

nano ~/.config/containers/systemd/redis.container
[Unit]
Description=Redis Cache Server
After=network-online.target

[Container]
Image=docker.io/redis:latest
ContainerName=redis
PublishPort=6379:6379
Volume=redis-data.volume:/data:Z
AutoUpdate=registry

[Install]
WantedBy=default.target

Example 2: PostgreSQL Database

nano ~/.config/containers/systemd/postgres.container
[Unit]
Description=PostgreSQL Database
After=network-online.target

[Container]
Image=docker.io/postgres:16
ContainerName=postgres
PublishPort=5432:5432
Environment=POSTGRES_PASSWORD=secret
Environment=POSTGRES_DB=appdb
Volume=%h/postgres-data:/var/lib/postgresql/data:Z
HealthCmd=pg_isready -U postgres
HealthInterval=10s

[Install]
WantedBy=default.target

Example 3: Nginx Reverse Proxy

nano ~/.config/containers/systemd/proxy.container
[Unit]
Description=Nginx Reverse Proxy
After=network-online.target

[Container]
Image=docker.io/nginx:latest
ContainerName=nginx-proxy
PublishPort=80:80
PublishPort=443:443
Volume=%h/nginx/conf:/etc/nginx/conf.d:Z,ro
Volume=%h/nginx/certs:/etc/nginx/certs:Z,ro
Network=app.network

[Install]
WantedBy=default.target

Quadlet vs Docker Compose

Similarities:

  • Declarative configuration
  • Multi-container management
  • Network and volume support

Quadlet Advantages:

  • Native systemd integration
  • No daemon required
  • Better security (rootless by default)
  • Built-in auto-updates
  • System-level service management

When to Use Quadlet:

  • Production servers
  • Single-node deployments
  • System services
  • Rootless containers
  • Integration with existing systemd services

Best Practices

  1. Use rootless containers – More secure, runs as regular user
  2. Enable linger for boot startloginctl enable-linger $USER
  3. Organize related containers – Use subdirectories in ~/.config/containers/systemd/
  4. Set resource limits – Prevent containers from consuming all resources
  5. Use named volumes – Better than bind mounts for persistence
  6. Implement health checks – Monitor container health automatically
  7. Enable auto-updates – Keep containers secure with latest images
  8. Use environment files – Keep secrets out of unit files
  9. Add proper dependencies – Use Requires= and After= correctly
  10. Test before enabling – Start manually first, enable after verification

Migration from podman generate systemd

If you’re migrating from the deprecated podman generate systemd:

Old method:

podman run -d --name nginx -p 8080:80 nginx
podman generate systemd --new --name nginx > nginx.service

New Quadlet method:

nano ~/.config/containers/systemd/nginx.container
[Container]
Image=docker.io/nginx:latest
PublishPort=8080:80

The tool podlet can help convert podman run commands to Quadlet units, though it’s not officially packaged in Fedora.


Useful Commands Reference

# Reload systemd units
systemctl --user daemon-reload

# List all container services
systemctl --user list-units --type=service | grep container

# Start/stop/restart container
systemctl --user start nginx
systemctl --user stop nginx
systemctl --user restart nginx

# Enable/disable auto-start
systemctl --user enable nginx
systemctl --user disable nginx

# View logs
journalctl --user -u nginx -f
podman logs -f systemd-nginx-web

# Check container status
systemctl --user status nginx
podman ps

# Debug Quadlet generation
/usr/libexec/podman/quadlet -dryrun -user

# Auto-update containers
podman auto-update
systemctl --user list-timers podman-auto-update.timer

# Enable linger (boot without login)
loginctl enable-linger $USER

Conclusion

You’ve successfully set up Podman Quadlet on Fedora 39 for systemd-managed containers. Quadlet provides a modern, declarative approach to container management that integrates seamlessly with systemd. Similar to how we automated ZFS snapshots with Sanoid and configured WireGuard split tunneling, Quadlet brings automation and native integration to container management.

Related Resources

Thank you for visiting our website, TechsBucket. If you liked the article, then share it with others.

Leave a Response