Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /Blog
  3. /Docker Compose to Podman Quadlet: A Practical Migration Guide
devops10 min read

Docker Compose to Podman Quadlet: A Practical Migration Guide

Convert a docker-compose.yml to Podman Quadlet files step by step, with a full example, rootless setup, and the daemon-reload gotchas that trip people up.

Zeeshan Tofiq
Zeeshan Tofiq
July 15, 2026
On this page

On this page

  • The Deprecation Warning You Just Hit
  • What a Quadlet Actually Is
  • The Example Stack
  • Converting Service by Service
  • podlet as a Starting Point
  • Gotchas
  • Rootful vs Rootless
  • Frequently Asked Questions

If you run Podman and you've typed podman generate systemd lately, you got a warning telling you to use Quadlet instead. Nothing broke. The command still works, and the maintainers have said they won't remove it. But it's frozen: no new features, only urgent bug fixes, while the whole ecosystem moves to Quadlet. If you run containers on a server and want them to survive reboots, this is worth doing properly once.

The Deprecation Warning You Just Hit

text — terminal
$ podman generate systemd --new --name my-app
DEPRECATED command:
It is recommended to use Quadlet files to run Podman containers and pods under systemd.

Please refer to the podman-systemd.unit(5) man page for details.

The official Podman docs mark generate systemd deprecated: no plans to remove it, but only urgent bug fixes, no new features. In a containers/podman GitHub Discussion, a maintainer explains the strategy directly: no new features will be added to generate systemd, and Quadlet aims for a Compose-and-Kubernetes-like declarative workflow instead. Quadlet became the recommended path once merged into Podman 4.4, which already shipped to CentOS Stream and Fedora, so this isn't an experimental feature you'd be migrating to early.

What a Quadlet Actually Is

A Quadlet is a small text file that looks like a systemd unit but is written in Podman's shorthand. You drop a file ending in .container into the right directory, run a daemon-reload, and systemd's Podman generator turns it into a real .service unit on the fly.

You never edit the generated service directly. You edit the short Quadlet file and reload. When Podman updates, the generated service updates with it automatically. That's the whole point, and it's exactly why the old generate-a-static-file approach is going away: a static generated .service file doesn't know when Podman's own defaults change underneath it.

ℹ The unit types you'll actually use

.container for a single container, .volume for a named volume, .network for a network, and .pod for grouping containers into a pod. Each type maps to its own systemd generator section ([Container], [Volume], [Network], [Pod]).

The Example Stack

Let's convert a real stack instead of a single podman run command, since that single-container case is already well covered elsewhere. Say you have this docker-compose.yml running a Caddy web server in front of a PostgreSQL database, on a shared network, with a named volume for the database data.

yaml — docker-compose.yml
services:
  web:
    image: docker.io/library/caddy:2
    ports:
      - "8080:80"
    depends_on:
      - db
    networks:
      - appnet
  db:
    image: docker.io/library/postgres:16
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - appnet
volumes:
  pgdata:
networks:
  appnet:

In Quadlet land, one compose file becomes several unit files. That feels like more work at first. It pays off because each piece is independently managed by systemd: you can restart the database without touching the web server's unit, and systemctl status tells you exactly which piece is unhealthy instead of one opaque compose stack.

Converting Service by Service

  1. 1

    Create the database Quadlet

    Start with the database, since the web server depends on it. Create ~/.config/containers/systemd/db.container:

    ini — ~/.config/containers/systemd/db.container
    [Unit]
    Description=App PostgreSQL
    
    [Container]
    Image=docker.io/library/postgres:16
    Environment=POSTGRES_PASSWORD=secret
    Volume=pgdata.volume:/var/lib/postgresql/data
    Network=appnet.network
    
    [Service]
    Restart=always
    
    [Install]
    WantedBy=default.target

    Notice the mapping: image becomes Image, environment becomes Environment, and each volume and network line points at a matching .volume or .network unit rather than a raw name. Use the full registry path for the image, docker.io/library/postgres:16 rather than just postgres, so the generated service doesn't stall resolving an ambiguous short name.

  2. 2

    Create the web server Quadlet

    Now the web server, web.container:

    ini — ~/.config/containers/systemd/web.container
    [Unit]
    Description=App Caddy
    Requires=db.service
    After=db.service
    
    [Container]
    Image=docker.io/library/caddy:2
    PublishPort=8080:80
    Network=appnet.network
    
    [Service]
    Restart=always
    
    [Install]
    WantedBy=default.target

    That depends_on in compose turns into Requires= plus After= in the [Unit] section. Requires makes the dependency hard, After controls ordering. Quadlet appends .service to the unit name for you, so referencing db.service (matching the db.container file) is enough.

  3. 3

    Create the volume and network units

    The volume and network are their own tiny files. pgdata.volume:

    ini — ~/.config/containers/systemd/pgdata.volume
    [Volume]

    An empty [Volume] section is valid. It creates a Podman volume named systemd-pgdata. appnet.network:

    ini — ~/.config/containers/systemd/appnet.network
    [Network]

    ⚠ Warning

    Don't skip the network unit. If your compose file has no explicit network and you let a converter drop it, your containers land on separate default networks and can't talk to each other. Define the network unit and reference it from every container that needs it.

  4. 4

    Reload systemd and start the services

    Because these are user units for rootless containers, they live under ~/.config/containers/systemd/ and you manage them with the --user flag. Tell systemd to regenerate the service units from your Quadlet files, then start the web service (its Requires= pulls in the database automatically):

    bash
    systemctl --user daemon-reload
    systemctl --user start web.service

    You start the .service, not the .container. Forgetting daemon-reload after editing a Quadlet file is the single most common reason "my change didn't take."

  5. 5

    Enable lingering so containers survive logout

    If you want these containers to keep running after you log out and to start at boot, enable lingering once for your user:

    bash
    sudo loginctl enable-linger $USER

    ⚠ Warning

    Skip this step and your rootless containers stop the moment your session ends, which surprises almost everyone the first time they reboot a server running rootless Quadlets.

podlet as a Starting Point

Don't hand-write all of this if you don't have to. podlet is a Rust CLI, roughly 1.5k GitHub stars, that reads a compose file and generates Quadlet units:

bash
podlet compose docker-compose.yml

It's a good starting point and saves typing. It's also not complete. Its own docs are blunt about it: if podlet hits an unsupported compose option, it errors out and you have to comment that option out to continue. And in a real multi-service compose file with no explicit network defined, podlet has been reported to generate no .network file at all, silently leaving Quadlet to fall back to separate default networks per container so they can't reach each other.

Treat podlet's output as a scaffold, not a finished config. Read every generated file line by line, and specifically check that a .network unit exists and is referenced by every container that needs to talk to the others.

Gotchas

  • Pin image tags. Use postgres:16, not postgres:latest. A Quadlet with AutoUpdate=registry plus a latest tag can pull a breaking major version update at 3am with nobody watching.
  • Use full registry paths. docker.io/library/postgres:16 instead of postgres, so the generated service doesn't stall resolving an ambiguous short image name.
  • Only add `AutoUpdate=registry` if you actually want it. It opts the container into podman auto-update managing upgrades for you, which is convenient but should be a deliberate choice, not a default you copy-pasted.
  • daemon-reload after every edit. Any change to a .container, .volume, or .network file needs systemctl --user daemon-reload (or without --user for system units) before it takes effect.
  • Check logs with journalctl, not scattered files. journalctl --user -u web.service puts your container's output next to everything else on the system, which is one of the real wins of moving off compose.

Rootful vs Rootless

RootlessRootful
Unit file location~/.config/containers/systemd//etc/containers/systemd/
Manage withsystemctl --user ...systemctl ... (no --user)
Survives logout by defaultNo, needs enable-lingerYes, always
Runs asYour userroot
Best forPersonal services, homelabs, least privilegeSystem-wide services, privileged ports below 1024

Most self-hosted setups should default to rootless unless a specific container genuinely needs root or a privileged port. If you're validating your compose file's health checks before making this move, our ComposeHealthCheck tool can catch missing depends_on conditions and healthcheck issues in your existing Docker Compose setup first.

Once it's running, you have containers that start on boot, restart on failure, order themselves correctly via Requires=/After=, and log to one place, without a daemon and without root. That's a better deal than the compose setup you started with, and it's where Podman is going.

If part of what you're migrating off cron also includes scheduled jobs rather than long-running services, our Cron to systemd Timer Converter handles the other half of the systemd migration: paste a crontab line and get a ready-to-save .timer and .service pair.

Frequently Asked Questions

Is podman generate systemd removed?

No. It's deprecated and frozen, meaning it still ships and receives urgent bug fixes, but gets no new features. New setups should use Quadlet instead. Existing scripts using generate systemd will keep working for now, but you shouldn't build anything new on top of it.

Do I still need Docker Compose or podman-compose?

For local development, compose is still fine: the fast iteration loop of docker compose up is hard to beat when you're actively changing code. For services running long-term on a server, Quadlet plus systemd is the more robust choice, since you get restart policies, ordering, and logging that are managed by the same init system as everything else on the box.

Should I use rootful or rootless Quadlets?

Rootless files go in ~/.config/containers/systemd/ and are managed with systemctl --user. Rootful files go in /etc/containers/systemd/ and use plain systemctl without --user.

  • Default to rootless for personal services and homelabs, since it runs as your user rather than root.
  • Use rootful only when a container genuinely needs root privileges or a port below 1024 that rootless can't bind directly.
Why does one compose file turn into so many separate unit files?

Each Quadlet unit type maps to one systemd-managed resource: a .container per container, a .volume per named volume, a .network per network. It looks like more files up front, but each one is independently restartable and inspectable with systemctl status, instead of one opaque compose stack you can only manage as a whole.

How do I convert docker-compose's depends_on to Quadlet?

Add both Requires= and After= to the dependent container's [Unit] section, pointing at the generated service name of the dependency (the .container filename with .service instead of .container):

ini
[Unit]
Requires=db.service
After=db.service

Requires= makes systemd treat the dependency as mandatory (stopping db.service stops anything requiring it too). After= only controls startup ordering. Use both together to replicate what depends_on implies in compose.

Can I just trust podlet's output without checking it?

No. podlet is a good scaffold but its own documentation warns it errors out on unsupported compose options rather than approximating them, and it has been reported to skip generating a .network unit entirely when a compose file doesn't declare one explicitly. Always read the generated files, and specifically confirm a .network unit exists if your containers need to reach each other.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.

Related Articles

devops

40 Kubernetes Interview Questions and Answers (2026)

40 Kubernetes interview questions covering Pods, Deployments, networking, RBAC, and real troubleshooting scenarios like CrashLoopBackOff and OOMKilled. Updated for 2026.

Jun 26, 2026·38 min read
devops

50 Cloud & DevOps Interview Questions and Answers (2026)

50 cloud and DevOps interview questions covering AWS Lambda, Docker, Microservices, API Gateway, S3, serverless, and Azure Entra ID. With code examples.

Jun 15, 2026·41 min read

On this page

  • The Deprecation Warning You Just Hit
  • What a Quadlet Actually Is
  • The Example Stack
  • Converting Service by Service
  • podlet as a Starting Point
  • Gotchas
  • Rootful vs Rootless
  • Frequently Asked Questions
Advertisement