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):
[Unit]Description=Nginx ContainerAfter=network.target
[Service]Type=simpleExecStart=/usr/bin/docker run --rm --name nginx \ -p 8080:80 \ -v /data/nginx:/etc/nginx/conf.d:ro \ nginx:latest
[Install]WantedBy=multi-user.targetThis works, but:
- You’re hardcoding the entire
docker runcommand as a string - If the container exits with an error, systemd sees an ExecStart failure, not a container failure
- Restarting the service requires killing the container and hoping
--rmcleans up - No native support for container-specific features like auto-update labels
Podman Quadlet (the right way):
[Unit]Description=Nginx ContainerAfter=network-online.targetWants=network-online.target
[Container]Image=nginx:latestPublishPort=8080:80Volume=/data/nginx:/etc/nginx/conf.d:roRestart=always
[Service]Restart=alwaysRestartSec=10
[Install]WantedBy=multi-user.targetThis is:
- Declarative, not imperative
- Type-safe (the
[Container]section has specific directives, not a raw command string) - Systemd-native (all the Restart logic works exactly like normal services)
- Composable (you can reference other Quadlets in the same directory)
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):
[Unit]Description=Uptime Kuma Status PageAfter=network-online.targetWants=network-online.target
[Container]Image=louislam/uptime-kuma:latestPublishPort=3001:3001Volume=%h/.local/share/uptime-kuma:/app/dataEnvironment=NODE_ENV=production
[Service]Restart=alwaysRestartSec=30
[Install]WantedBy=multi-user.targetCommon directives:
Image=— the OCI image to runPublishPort=— map ports (can be repeated)Volume=— bind mounts or named volumes (can be repeated)Environment=— env vars (can be repeated)User=— run as this userRestart=— systemd restart policy (no, on-success, on-failure, always)
When you systemctl enable uptime-kuma.container, Podman generates the full podman run command behind the scenes. You’ll see it in systemctl status:
$ 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:
[Unit]Description=Application Network
[Network]# Quadlets will auto-join this network if you reference it[Unit]Description=Application Data Volume
[Volume]# Creates a named volume that containers can referenceThen in your .container files, reference them:
[Unit]Description=PostgreSQLAfter=app-net.network app-data.volumeWants=app-net.network app-data.volume
[Container]Image=postgres:15-alpineEnvironment=POSTGRES_PASSWORD=localdevVolume=app-data:/var/lib/postgresql/dataNetwork=app-net
[Service]Restart=always
[Install]WantedBy=multi-user.targetAuto-Update: Keep Your Containers Fresh
Quadlets play nicely with Podman’s auto-update feature. Add a label to your container:
[Unit]Description=My AppAfter=network-online.target
[Container]Image=myregistry/myapp:latestLabel=io.containers.autoupdate=registry
[Service]Restart=always
[Install]WantedBy=multi-user.targetThen systemd can trigger updates:
$ podman auto-update --dry-run$ podman auto-updateOr wire it into a systemd timer to auto-update daily:
[Unit]Description=Podman auto-update timer
[Timer]OnCalendar=dailyOnBootSec=15min
[Install]WantedBy=timers.target[Unit]Description=Podman auto-updateAfter=network-online.target
[Service]Type=oneshotExecStart=podman auto-updateReal-World Example: Vaultwarden
Here’s a complete, production-ready Quadlet setup for Vaultwarden (self-hosted password manager):
[Unit]Description=Vaultwarden Data Volume
[Volume][Unit]Description=Vaultwarden Password ManagerAfter=network-online.target vaultwarden-data.volumeWants=network-online.target vaultwarden-data.volume
[Container]Image=vaultwarden/server:latestPublishPort=8000:80Volume=vaultwarden-data:/dataEnvironment=DOMAIN=https://vault.example.comEnvironment=SMTP_HOST=smtp.example.comEnvironment=SMTP_FROM=vault@example.comEnvironment=SMTP_SECURITY=starttlsEnvironment=SMTP_PORT=587HealthCmd=curl -f http://localhost:80/alive || exit 1HealthInterval=60s
[Service]Restart=alwaysRestartSec=30TimeoutStopSec=120
[Install]WantedBy=multi-user.targetTo deploy:
$ 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 -fQuadlets vs Compose vs Docker
When do you reach for each?
Use Quadlets when:
- You’re running a few containers on a single machine or homelab
- You want systemd to be your orchestrator (no separate daemon)
- You need tight integration with OS-level init and lifecycle management
- You’re comfortable thinking in systemd terms
Use Podman Compose when:
- You have a multi-container app that’s easier to think of as a single unit
- You want
docker-compose upconvenience but with Podman semantics - You need to reproduce the exact same setup across machines (compose is portable)
Use Docker Compose when:
- You’re already in the Docker ecosystem and don’t want to migrate
- Your team is familiar with Compose and it’s working fine
- You need the exact Docker daemon and don’t want alternatives
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
- Install Podman 4.4+ (or backport from Kubic repos if your distro is old)
- Create your first Quadlet — start with a simple container like Nginx
- Test locally:
podman runthe image first, then translate that command into a.containerfile - Place it in
/etc/systemd/system/(or~/.config/systemd/user/for user-level containers) - Reload and enable:
systemctl daemon-reload && systemctl enable myapp.container - 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.