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.
Here’s the thing most people don’t realize: Podman is almost entirely CLI-compatible with Docker. Like, embarrassingly compatible. This works:
alias docker=podman
That’s it. Most of your existing Docker commands just work. Same flags, same image format (OCI-compatible), same registries. It’s not a gotcha, it’s literally by design.
Installing Podman on Ubuntu/Debian
sudo apt update
sudo apt install -y podman
podman --version
On Ubuntu 22.04+, you’ll get Podman 3.x from the default repos. For Podman 4.4+ (which you need for Quadlets), add the Kubic repo:
# For Ubuntu 22.04
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_22.04/ /" \
| sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_22.04/Release.key \
| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/devel_kubic_libcontainers_unstable.gpg
sudo apt update && sudo apt install -y podman
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 root
podman run --rm hello-world
# Compare: Docker (traditionally requires sudo or docker group — which is basically root anyway)
docker run --rm hello-world
Being 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:latest
PublishPort=8080:80
[Service]
Restart=always
[Install]
WantedBy=default.target
Drop 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 Server
After=network-online.target
[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
Volume=%h/nginx/html:/usr/share/nginx/html:ro,Z
[Service]
Restart=on-failure
TimeoutStartSec=30
[Install]
WantedBy=default.target
A 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 units
systemctl --user daemon-reload
# Enable and start
systemctl --user enable --now nginx.service
# Check status
systemctl --user status nginx.service
# Watch logs
journalctl --user -u nginx.service -f
That’s a container running as a proper systemd service. No Docker Compose, no daemon, no root. Just systemctl.
A More Complete Example: Web App with Volumes and Env Vars
Real workloads need environment variables and persistent storage. Here’s a more complete example — let’s say you’re running a simple web app:
[Unit]
Description=My Awesome Web App
After=network-online.target postgresql.service
[Container]
Image=docker.io/myuser/myapp:latest
PublishPort=3000:3000
# Environment variables
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=%h/.config/containers/myapp.env
# Persistent storage
Volume=myapp-data:/app/data:Z
Volume=%h/myapp/config:/app/config:ro,Z
# Resource limits (because containers without limits are just vibes)
PodmanArgs=--memory=512m --cpus=0.5
[Service]
Restart=on-failure
RestartSec=10s
TimeoutStartSec=60
[Install]
WantedBy=default.target
Your myapp.env file:
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
SECRET_KEY=your-secret-here
Notice EnvironmentFile — same pattern as a regular systemd service. Your secrets stay out of the unit file, which is a good habit regardless of whether you’re using containers.
Networks 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:
# ~/.config/containers/systemd/myapp.network
[Network]
Driver=bridge
Then reference it in your container files:
[Container]
Image=docker.io/myuser/myapp:latest
Network=myapp.network
Podman 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.
How This Compares to Docker Compose
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.
Here’s an honest comparison:
| Docker Compose | Podman + Quadlets | |
|---|---|---|
| Startup on boot | Needs docker-compose up in cron or rc.local | Native systemd, just WantedBy=default.target |
| Restart on failure | Compose restart: always (polling-based) | systemd supervision (proper) |
| Logs | docker compose logs | journalctl (integrated with system logs) |
| Root required | Usually yes (or docker group) | No, fully rootless |
| Multi-container orchestration | Single compose file | Multiple unit files + network quadlets |
| Learning curve | Low if you know YAML | Slightly higher (systemd concepts) |
| Daemon required | Yes | No |
Compose wins on simplicity when you’re just getting something running quickly. Quadlets win on long-term maintainability, proper service management, and security posture. They’re not mutually exclusive either — podman-compose exists as a drop-in for those transitioning.
When to Use Which
Stick with Docker + Compose if:
- You’re running a homelab and just want stuff to work with minimal friction
- Your apps ship with compose files and you’re not changing much
- You’re developing locally and want quick iteration
Switch to Podman + Quadlets if:
- Security matters (rootless is genuinely better)
- You want containers to behave like proper systemd services
- You’re already comfortable with systemd and hate context-switching to a different tool
- You’re on Fedora, RHEL, or any system where Podman ships by default
- You want containers integrated with
journalctl,systemctl status,systemd-analyze blame, etc.
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 Docker Compose” 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 running
podman ps
# Roll back (change image tag back, restart)
systemctl --user restart myapp.service
# See why it crashed
journalctl --user -u myapp.service --since "10 minutes ago"
# Stop it
systemctl --user stop myapp.service
# Remove the service entirely
systemctl --user disable myapp.service
# Delete the .container file
systemctl --user daemon-reload
That’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.