Skip to content
Go back

Podman Quadlets: Systemd-Native Containers

By SumGuy 6 min read
Podman Quadlets: Systemd-Native Containers

The Problem With Running Containers via Systemd (Before Quadlets)

Here’s what most people do when they want a container to survive a reboot: they write a systemd unit file that calls podman run or docker run in ExecStart, dump it in /etc/systemd/system/, cross their fingers, and hope the next 2 AM doesn’t involve debugging why the container didn’t start.

It’s not elegant. You’ve got a fragile shell command in your unit file, you have to manually manage volumes and networks before the container even touches them, and if your container needs to update or restart, systemd has no idea what state it’s actually in.

Then came Quadlets.

Quadlets are .container, .network, and .volume unit files that systemd understands natively. Instead of shoehorning podman run into an ExecStart directive, you describe your container as a unit file, and systemd treats it like a first-class service. No daemon. No hacks. Just clean, idiomatic integration.

It’s what Podman was built toward from the start.

What Makes Quadlets Different

A Quadlet is a unit file that lives alongside your systemd units but gets translated into a native Podman container at runtime. When you enable some-app.container, systemd doesn’t call podman run — it generates that command from the Quadlet definition and manages the lifecycle like it would any other service.

The magic: systemd is the init system for your containers. The container becomes an actual systemd unit that you restart with systemctl restart some-app, check status with systemctl status some-app, and manage with all the normal systemd tools.

No separate daemon. No process wrapper. Just systemd doing systemd things.

Why That Matters

Compare these two approaches:

Docker + systemd hack (the old way):

/etc/systemd/system/nginx.service
[Unit]
Description=Nginx Container
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/docker run --rm --name nginx \
-p 8080:80 \
-v /data/nginx:/etc/nginx/conf.d:ro \
nginx:latest
[Install]
WantedBy=multi-user.target

This works, but:

Podman Quadlet (the right way):

/etc/systemd/system/nginx.container
[Unit]
Description=Nginx Container
After=network-online.target
Wants=network-online.target
[Container]
Image=nginx:latest
PublishPort=8080:80
Volume=/data/nginx:/etc/nginx/conf.d:ro
Restart=always
[Service]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

This is:

The Quadlet File Formats

.container Files

A .container file describes a single container. Here’s a full working example running Uptime Kuma (a self-hosted status page):

/etc/systemd/system/uptime-kuma.container
[Unit]
Description=Uptime Kuma Status Page
After=network-online.target
Wants=network-online.target
[Container]
Image=louislam/uptime-kuma:latest
PublishPort=3001:3001
Volume=%h/.local/share/uptime-kuma:/app/data
Environment=NODE_ENV=production
[Service]
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

Common directives:

When you systemctl enable uptime-kuma.container, Podman generates the full podman run command behind the scenes. You’ll see it in systemctl status:

Terminal window
$ systemctl status uptime-kuma
uptime-kuma.service - Uptime Kuma Status Page
Loaded: loaded (/etc/systemd/system/uptime-kuma.container; enabled)
Active: active (running) since Wed 2026-04-09 12:15:42 UTC; 2h 30min ago
Main PID: 2847 (conmon)
Tasks: 5 (limit: 1118)
Memory: 89.2M

.network and .volume Files

For multi-container setups or shared volumes, define them as Quadlets too:

/etc/systemd/system/app-net.network
[Unit]
Description=Application Network
[Network]
# Quadlets will auto-join this network if you reference it
/etc/systemd/system/app-data.volume
[Unit]
Description=Application Data Volume
[Volume]
# Creates a named volume that containers can reference

Then in your .container files, reference them:

/etc/systemd/system/postgres.container
[Unit]
Description=PostgreSQL
After=app-net.network app-data.volume
Wants=app-net.network app-data.volume
[Container]
Image=postgres:15-alpine
Environment=POSTGRES_PASSWORD=localdev
Volume=app-data:/var/lib/postgresql/data
Network=app-net
[Service]
Restart=always
[Install]
WantedBy=multi-user.target

Auto-Update: Keep Your Containers Fresh

Quadlets play nicely with Podman’s auto-update feature. Add a label to your container:

/etc/systemd/system/app.container
[Unit]
Description=My App
After=network-online.target
[Container]
Image=myregistry/myapp:latest
Label=io.containers.autoupdate=registry
[Service]
Restart=always
[Install]
WantedBy=multi-user.target

Then systemd can trigger updates:

Terminal window
$ podman auto-update --dry-run
$ podman auto-update

Or wire it into a systemd timer to auto-update daily:

/etc/systemd/system/podman-autoupdate.timer
[Unit]
Description=Podman auto-update timer
[Timer]
OnCalendar=daily
OnBootSec=15min
[Install]
WantedBy=timers.target
/etc/systemd/system/podman-autoupdate.service
[Unit]
Description=Podman auto-update
After=network-online.target
[Service]
Type=oneshot
ExecStart=podman auto-update

Real-World Example: Vaultwarden

Here’s a complete, production-ready Quadlet setup for Vaultwarden (self-hosted password manager):

/etc/systemd/system/vaultwarden-data.volume
[Unit]
Description=Vaultwarden Data Volume
[Volume]
/etc/systemd/system/vaultwarden.container
[Unit]
Description=Vaultwarden Password Manager
After=network-online.target vaultwarden-data.volume
Wants=network-online.target vaultwarden-data.volume
[Container]
Image=vaultwarden/server:latest
PublishPort=8000:80
Volume=vaultwarden-data:/data
Environment=DOMAIN=https://vault.example.com
Environment=SMTP_HOST=smtp.example.com
Environment=SMTP_FROM=vault@example.com
Environment=SMTP_SECURITY=starttls
Environment=SMTP_PORT=587
HealthCmd=curl -f http://localhost:80/alive || exit 1
HealthInterval=60s
[Service]
Restart=always
RestartSec=30
TimeoutStopSec=120
[Install]
WantedBy=multi-user.target

To deploy:

Terminal window
$ sudo cp vaultwarden*.container /etc/systemd/system/
$ sudo systemctl daemon-reload
$ sudo systemctl enable vaultwarden.container
$ sudo systemctl start vaultwarden.container
$ systemctl status vaultwarden
$ journalctl -u vaultwarden -f

Quadlets vs Compose vs Docker

When do you reach for each?

Use Quadlets when:

Use Podman Compose when:

Use Docker Compose when:

Honestly? For a self-hosted homelab with a handful of services, Quadlets are the sweetest spot. You get the simplicity of Compose, the system-level integration of systemd, and zero daemon overhead.

Getting Started

  1. Install Podman 4.4+ (or backport from Kubic repos if your distro is old)
  2. Create your first Quadlet — start with a simple container like Nginx
  3. Test locally: podman run the image first, then translate that command into a .container file
  4. Place it in /etc/systemd/system/ (or ~/.config/systemd/user/ for user-level containers)
  5. Reload and enable: systemctl daemon-reload && systemctl enable myapp.container
  6. Start and check logs: systemctl start myapp && journalctl -u myapp -f

No daemon. No secrets. Just you, Podman, and systemd doing what they do best.


That’s the whole story. Quadlets aren’t revolutionary — they’re just containers done the Unix way: small, composable, and managed by your init system. Once you’ve lived with that, going back to Docker feels like driving a car with a shotgun-wielding middle manager in the passenger seat.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
SBCs in 2026: Homelab on a Budget
Next Post
LUKS Full Disk Encryption on Linux

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts