You Don’t Actually Need the Docker Daemon
Let me paint you a picture. It’s 2 AM. Your home server is running Docker, which is running a daemon, which is running as root, which is silently doing whatever it wants to your filesystem. Your Nginx container restarted itself three times for reasons unknown, your Watchtower container is updating things you didn’t ask it to update, and somewhere in that mess is a compose file you last touched eight months ago and no longer fully trust.
You know what would’ve helped? Not having a single all-knowing daemon sitting between you and your containers like an overconfident middle manager.
That’s the pitch for Podman. And once you add Quadlets into the mix, containers that systemd manages natively, like any other service, you get something that actually feels like it was designed for the way sane people run servers.
Let’s dig in.
What Even Is Podman?
Podman (short for Pod Manager) is a container engine that does what Docker does, pulls images, runs containers, manages volumes and networks, but without the daemon.
No background service. No root requirement. No single point of failure that takes down every container on your machine if it crashes.
Most people don’t realize that Podman is almost entirely CLI-compatible with Docker. Like, embarrassingly compatible. This works:
alias docker=podmanThat’s it. Most of your existing Docker commands just work. Same flags, same image format (OCI-compatible), same registries. That’s by design.
Installing Podman on Ubuntu/Debian
sudo apt updatesudo apt install -y podmanpodman --versionOn Ubuntu 24.04 LTS (and 22.04), you’ll get a recent Podman build straight from the default repos that already includes Quadlet support, no third-party repo needed anymore. A heads-up if you find older tutorials: the OpenSUSE Kubic repo that everyone used to recommend is now abandoned (it hasn’t shipped an updated Podman in years, and it relied on the deprecated apt-key). Don’t add it. Just use the official Ubuntu packages.
If you genuinely need a bleeding-edge Podman that’s newer than your distro ships, the cleaner route these days is a Fedora-based container or VM, or just run a current distro where Podman is fresh out of the box.
On Fedora or RHEL-based distros, Podman is already installed by default and you’re already living in the future.
The Rootless Part Actually Matters
Running containers as root is like giving every delivery driver a master key to your house because it’s easier than handing out specific room keys. Technically functional. Deeply cursed.
Podman’s rootless mode means containers run under your own user account. If something goes wrong inside a container (escape attempt, misconfiguration, whatever) it’s limited to what your user can do. No root access means no “oops, container wrote to /etc” incidents.
# This runs as YOU, not rootpodman run --rm hello-world
# Compare: Docker (traditionally requires sudo or docker group, which is basically root anyway)docker run --rm hello-worldBeing in the docker group is functionally equivalent to sudo access, by the way. That’s not a hot take, it’s documented by Docker themselves. Podman sidesteps this entirely.
Enter Quadlets: Systemd Actually Managing Your Containers
Okay, so you’ve switched to Podman. Now what? How do you make containers start on boot, restart on failure, and generally behave like proper services?
The old answer was podman generate systemd, a command that would spit out a systemd unit file based on an existing container. It worked, but it was janky. The generated files were verbose, hard to maintain, and felt like they were auto-generated (because they were). Every time you changed your container config, you had to regenerate the unit file.
The new answer is Quadlets.
Quadlets, introduced in Podman 4.4, let you write simple .container files that systemd reads natively through a generator. Instead of a 60-line auto-generated unit file, you write a clean INI-style config that looks like this:
[Unit]Description=My Nginx Container
[Container]Image=docker.io/library/nginx:latestPublishPort=8080:80
[Service]Restart=always
[Install]WantedBy=default.targetDrop that in the right directory, run two commands, and systemd is managing your container. That’s it.
Where Do Quadlet Files Live?
For rootless (user) containers:
~/.config/containers/systemd/For system-wide (root) containers:
/etc/containers/systemd/Your First Quadlet: Nginx
Let’s do this properly. Create the file:
mkdir -p ~/.config/containers/systemd/nano ~/.config/containers/systemd/nginx.container[Unit]Description=Nginx Web ServerAfter=network-online.target
[Container]Image=docker.io/library/nginx:latestPublishPort=8080:80Volume=%h/nginx/html:/usr/share/nginx/html:ro,Z
[Service]Restart=on-failureTimeoutStartSec=30
[Install]WantedBy=default.targetA few notes:
%his a systemd specifier that expands to your home directory. Handy.- The
:Zon the volume is a SELinux label: needed on Fedora/RHEL, harmless on Ubuntu. After=network-online.targetmeans the container won’t try to start before networking is ready. You’d think this was the default. It is not.
Now tell systemd to pick it up and start it:
# Reload the systemd user daemon to pick up new unitssystemctl --user daemon-reload
# Enable and startsystemctl --user enable --now nginx.service
# Check statussystemctl --user status nginx.service
# Watch logsjournalctl --user -u nginx.service -fThat’s a container running as a proper systemd service. No Docker Compose, no daemon, no root. Just systemctl.
A Production Example: Vaultwarden with Health Checks
The nginx example is fine for learning, but real workloads need environment variables, persistent storage, and something watching whether the container is actually healthy, not just running. Here’s a complete Quadlet for Vaultwarden (the self-hosted Bitwarden-compatible password manager):
[Unit]Description=Vaultwarden Password ManagerAfter=network-online.targetWants=network-online.target
[Container]Image=docker.io/vaultwarden/server:latestPublishPort=8000:80Volume=vaultwarden-data.volume:/dataEnvironment=DOMAIN=https://vault.example.comEnvironment=SMTP_HOST=smtp.example.comEnvironment=SMTP_FROM=[email protected]Environment=SMTP_SECURITY=starttlsEnvironment=SMTP_PORT=587EnvironmentFile=%h/.config/containers/vaultwarden.envHealthCmd=curl -f http://localhost:80/alive || exit 1HealthInterval=60s
# Resource limits, because containers without limits are just vibesPodmanArgs=--memory=512m --cpus=0.5
[Service]Restart=alwaysRestartSec=30TimeoutStopSec=120
[Install]WantedBy=default.targetYour vaultwarden.env file holds the one thing you really don’t want sitting in plaintext inside a unit file:
ADMIN_TOKEN=your-long-random-admin-tokenEnvironmentFile works exactly like it does on a regular systemd service; keep it out of your dotfiles repo and chmod it 600. HealthCmd and HealthInterval are the part most tutorials skip: without them, systemd only knows the container process is running, not that Vaultwarden is actually answering requests. If the app wedges itself without crashing (it happens), a health check catches it, a bare Restart=always doesn’t.
Deploy it:
mkdir -p ~/.config/containers/systemd/# copy the .container file and .env file into that directorysystemctl --user daemon-reloadsystemctl --user enable --now vaultwarden.servicesystemctl --user status vaultwarden.serviceNetworks as Quadlets Too
If you’re running multiple containers that need to talk to each other, you’ll want a dedicated network. That’s also a Quadlet:
[Network]Driver=bridgeThen reference it in your container files:
[Container]Image=docker.io/myuser/myapp:latestNetwork=myapp.networkPodman will create the network as a systemd service (myapp-network.service) and your container unit will automatically depend on it. The dependency graph is managed for you.
Auto-Update: Keep Images Fresh Without Watchtower
Quadlets play nicely with Podman’s built-in auto-update mechanism, so you don’t need to bolt on a separate updater container. Add a label to any .container file:
[Container]Image=docker.io/myuser/myapp:latestLabel=io.containers.autoupdate=registryThat label tells Podman to check the registry for a newer digest and pull it when asked:
# See what would update, without touching anythingpodman auto-update --dry-run
# Actually updatepodman auto-updateWire it into a systemd timer and it runs itself 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-updatesystemctl --user daemon-reloadsystemctl --user enable --now podman-autoupdate.timerPodman pulls the new image, checks that the digest actually changed, and restarts only the containers carrying that label. No separate container watching your Docker socket, no extra attack surface. It’s auto-updates the Unix way: another small unit doing one job.
How This Compares to Compose (Both Flavors)
Let’s be real for a second. Docker Compose is genuinely good for local dev and single-host deployments. It’s readable, it handles dependencies, and pretty much every self-hosted project ships a docker-compose.yml. Abandoning it entirely is not necessarily the right call. But once Podman’s in the picture, you’ve actually got three options, not two: plain Docker Compose, podman-compose running Podman with Compose YAML, or Quadlets going straight to systemd.
Here’s an honest comparison:
| Docker Compose | Podman Compose | Podman + Quadlets | |
|---|---|---|---|
| Startup on boot | docker-compose up in cron or rc.local | Same, via podman-compose up | Native systemd, WantedBy=default.target |
| Restart on failure | Compose restart: always (polling) | Compose restart: always (polling) | systemd supervision (proper) |
| Logs | docker compose logs | podman-compose logs | journalctl, integrated with the system log |
| Root required | Usually yes (or docker group, same thing) | No, rootless | No, rootless |
| Config format | YAML, portable across hosts | Same YAML, Podman semantics | INI-style unit files per container |
| Daemon required | Yes | No | No |
| Multi-container orchestration | Single compose file | Single compose file | Multiple unit files + network quadlets |
Stick with Docker Compose if you’re already in the Docker ecosystem, your team knows Compose, and migrating buys you nothing this quarter.
Use Podman Compose if you want Podman’s rootless daemonless model but you’re not ready to give up the portability of a single YAML file you can copy to any host.
Switch to Quadlets if you want systemd itself to be the orchestrator, you’re comfortable thinking in units instead of YAML, and you want systemctl status, journalctl, and systemd-analyze blame to just work on your containers like they do on everything else.
Honestly, the “I already know Docker” angle is mostly a non-issue. The CLI is identical for 90% of operations. The real mental shift is from “containers managed by a compose file” to “containers managed by systemd.” If you already think in terms of services and units, Quadlets are going to feel immediately natural.
A Note on the podman generate systemd Ghost
You’ll still find a lot of tutorials recommending podman generate systemd --new --name mycontainer. This was the pre-Quadlet approach and it worked, it generated a full systemd unit file from a running container. But it produced ugly, hard-to-maintain output, and the workflow was backwards: run the container first, then generate the service definition from it.
Quadlets flip this around. You define the intent, systemd handles the execution. It’s the right direction.
If you’re on Podman < 4.4 and can’t upgrade, generate systemd is still there. But if you’re setting up anything new, just use Quadlets.
The Practical Upshot
Here’s what the full loop looks like once you’ve got a Quadlet set up:
# Deploy a new version (update the image tag in your .container file, then:)systemctl --user restart myapp.service
# Check what's runningpodman ps
# Roll back (change image tag back, restart)systemctl --user restart myapp.service
# See why it crashedjournalctl --user -u myapp.service --since "10 minutes ago"
# Stop itsystemctl --user stop myapp.service
# Remove the service entirelysystemctl --user disable myapp.service# Delete the .container filesystemctl --user daemon-reloadThat’s a container lifecycle managed entirely through standard Linux tooling. No separate CLI to remember, no daemon to babysit, no compose file that’s technically a different tool entirely.
Go Forth and Run Rootless
Podman and Quadlets aren’t a revolution, they’re more like a quiet correction. Container management got complicated in ways it didn’t need to be, and this stack politely unfastens some of that complexity. You get containers that behave like services, run without root, and integrate with the init system that’s already managing everything else on your server.
Is it worth migrating your entire Docker setup tomorrow? Probably not. But for new services, for anything where security actually matters, or for anyone who’s ever thought “I wish this container just acted like a normal systemd service”, Podman + Quadlets is genuinely the answer.
Start with one container. Write the .container file. Run systemctl --user enable --now. Watch it just work.
Your 2 AM self will appreciate it.