Migrate from `podman generate systemd` to Quadlet

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Any Linux host with Podman 4.6+ and systemd
Privilege sudo for rootful examples; normal user with systemctl --user for rootless migration
Scope Deprecation status of podman generate systemd, legacy generated unit anatomy, --new vs start/stop style, podman run flag to Quadlet directive mapping, worked conversion, volume and network ownership, [Install] boot wiring, rootless path changes, container naming, dependency migration, validation sequence, and port or data pitfalls. Assumes existing generated units or a deprecation warning. Does not reteach Quadlet basics or full .container reference.

You already run Podman under systemd with units from podman generate systemd, or Podman just printed a deprecation warning when you tried it. This guide is the migration path: what the old command still does, how a generated .service maps to a .container file, and how to cut over without losing container names, host ports, or volume data.


Is podman generate systemd removed?

No. The command is deprecated, not deleted. Running it on Podman 5.8.2 still works and prints a deprecation notice:

bash
podman generate systemd --help

Sample output:

output
[DEPRECATED] Generate systemd units
...
DEPRECATED command:
It is recommended to use Quadlets for running containers and pods under systemd.

The man page states the official position more precisely: Quadlet is recommended for new workloads, there are currently no plans to remove podman generate systemd, it receives urgent bug fixes, and it does not receive new features. Deprecated does not mean your existing units stop working on the next minor release — do not rewrite stable production services only because the warning appeared.


Why Podman recommends Quadlet

The difference is where the source of truth lives.

Legacy generate systemd:

text
podman create/run
existing container configuration
podman generate systemd
.service file

Quadlet:

text
desired .container file
systemd generator
.service
container created from declaration

With generate systemd, the running container (or a snapshot of it) drives the unit. Edit the container later and the installed .service drifts. With Quadlet, you edit the declarative file, run daemon-reload, and the generator rebuilds ExecStart from that declaration. For Quadlet concepts and first-unit setup, see Podman Quadlet with systemd — this page stays on conversion.


What podman generate systemd produced

The lab starts from a container created the way many legacy workflows did:

bash
podman create --name old-web -p 8080:80 -e APP_ENV=production -v web-data:/data registry.access.redhat.com/ubi9/httpd-24:latest

Generate a unit that recreates the container on each start:

bash
podman generate systemd --new --name old-web

Sample output (trimmed):

output
# container-old-web.service
# autogenerated by Podman 5.8.2

[Unit]
Description=Podman container-old-web.service
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
ExecStart=/usr/bin/podman run \
	--cidfile=%t/%n.ctr-id \
	--rm --replace --sdnotify=conmon -d \
	--name old-web -p 8080:80 -e APP_ENV=production \
	-v web-data:/data registry.access.redhat.com/ubi9/httpd-24:latest
ExecStop=/usr/bin/podman stop --ignore -t 10 --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore -t 10 --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Lines worth mapping to Quadlet:

  • ExecStart — full podman run with ports, env, volumes, and image
  • ExecStop / ExecStopPost — stop and remove using a cidfile
  • Restart=on-failure — systemd restart policy (overridable with --restart-policy)
  • --cidfile — tracks the container ID for stop/remove
  • Type=notify — conmon sdnotify integration
  • RequiresMountsFor=%t/containers — runtime state directory mount

You do not copy this ExecStart into Quadlet. You translate the Podman flags into [Container] directives and let the generator write a fresh ExecStart.


Why --new mattered in legacy units

Without --new, generated services typically managed an existing container:

text
ExecStart=podman start old-web
ExecStop=podman stop old-web

That style keeps the same container instance across restarts. It cannot switch to a newer image unless you recreate the container yourself.

With --new, each service start runs podman run --replace --rm, which creates a fresh container and removes it on stop. That recreate-on-start behavior is what Quadlet does by default and what Podman auto-update expects when it restarts a unit after pulling an image. If your legacy unit used --new, you are already closer to the Quadlet model than a plain podman start unit.


Convert podman run options into .container directives

Use this table when reading an old ExecStart line or translating a podman create command:

Legacy option Quadlet directive
--name web ContainerName=web
image argument Image=
command after image Exec=
-e KEY=VAL Environment=KEY=VAL
--env-file EnvironmentFile=
-p 8080:80 PublishPort=8080:80
-v vol:/path Volume=vol:/path
--network net Network=net
--user / --group User= / Group=
--userns UserNS=
Podman --restart on CLI prefer [Service] Restart= (systemd owns restart)

Quadlet has first-class keys for most flags you previously embedded in ExecStart. Reach for PodmanArgs= only when no directive exists on your Podman version — see Podman Quadlet container file.


Convert the generated service to Quadlet

The generated container-old-web.service becomes /etc/containers/systemd/old-web.container:

ini
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24:latest
ContainerName=old-web
PublishPort=8080:80
Environment=APP_ENV=production
Volume=web-data:/data

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

Migration decisions in plain terms:

  • Image= — same registry image from ExecStart
  • ContainerName=old-web — preserves the name scripts and podman ps filters expect (without it, Quadlet defaults to systemd-old-web)
  • PublishPort, Environment, Volume — lifted directly from -p, -e, and -v
  • [Service] Restart=always — moves restart policy out of Podman CLI; we chose always instead of the generated on-failure because this lab service should survive exits
  • [Install] WantedBy=multi-user.target — replaces installing the generated file under /etc/systemd/system/ and running systemctl enable

Reload and inspect what the generator built:

bash
systemctl daemon-reload

Read the generated ExecStart and compare it to your legacy unit:

bash
systemctl cat old-web.service

The generated ExecStart is a new podman run line with the same ports, env, and volume — but it now comes from the .container file, not a one-time export.


Move volumes to .volume units where useful

Legacy:

bash
-v web-data:/data

Minimal migration keeps the same line in Quadlet:

ini
Volume=web-data:/data

When you want Quadlet to own volume creation declaratively, add web-data.volume:

ini
[Volume]

then reference it from the container:

ini
Volume=web-data.volume:/data

That is optional. If another team or backup workflow already manages web-data, leaving the simple Volume= form is fine. For volume unit details, see Podman Quadlet volume and network.


Move networks to .network units where useful

Same ownership question applies to networks. An existing external or CLI-created network:

ini
Network=app-network

A Quadlet-managed network:

ini
Network=app.network

Pick based on who creates and deletes the network, not because every migration must add new unit types.


Replace the generated restart policy

Legacy generated unit:

ini
Restart=on-failure

Quadlet:

ini
[Service]
Restart=always

Do not encode Podman --restart in PodmanArgs= when systemd should own service-level restart behavior. Match the policy your application actually needs (always, on-failure, or no) in [Service], not in duplicate Podman flags.


Replace systemctl enable on the generated unit

Legacy workflow copied the generated file to /etc/systemd/system/ and enabled it:

bash
systemctl enable container-old-web.service

Quadlet-generated services live under the generator path and are transient. Enabling them directly fails:

bash
systemctl enable old-web.service

Sample output:

output
Failed to enable unit: Unit /run/systemd/generator/old-web.service is transient or generated

Boot wiring belongs in the Quadlet source:

ini
[Install]
WantedBy=multi-user.target

After daemon-reload, systemd links the generated unit into multi-user.target without systemctl enable on the generated path.


Rootless migration paths

Legacy rootless units often lived under:

text
~/.config/systemd/user/

Quadlet source files belong under:

text
~/.config/containers/systemd/

Manage them with the user manager:

bash
systemctl --user daemon-reload

Start the migrated service in the user manager:

bash
systemctl --user start old-web.service

When the service must survive logout, enable lingering for the user (loginctl enable-linger USER). Full rootless Quadlet setup — paths, UID mapping, and linger — is in Rootless Podman Quadlet.


Container naming during migration

A legacy service may manage a container named old-web. Quadlet service old-web.service without ContainerName= creates systemd-old-web instead. External scripts, health checks, or podman exec aliases that still target old-web will break.

Set the name explicitly when you need continuity:

ini
ContainerName=old-web

This is one of the easiest regressions to prevent and one of the easiest to miss.


Migrate dependencies

Generated units may include:

ini
Requires=
Wants=
After=

Move real application ordering into [Unit] on the Quadlet file when the dependency is not already implicit. Referencing data.volume or app.network in [Container] often adds the right Requires= lines automatically. Drop cidfile paths, conmon details, and other implementation-only lines from the old generated unit — they are not migration targets.


Do not copy generated ExecStart= into Quadlet

Wrong — this defeats Quadlet:

ini
[Service]
ExecStart=/usr/bin/podman run --name old-web ...

Right — declare intent in [Container]:

ini
[Container]
Image=registry.access.redhat.com/ubi9/httpd-24:latest
ContainerName=old-web
PublishPort=8080:80
...

The generator writes ExecStart, ExecStop, and sdnotify settings. You maintain the declaration.


Validate before removing the old unit

Recommended cutover sequence:

  1. Save the current podman generate systemd output or the installed .service file
  2. Stop the old service and container
  3. Resolve port and name conflicts before starting the Quadlet unit
  4. Write the .container file under /etc/containers/systemd/ or ~/.config/containers/systemd/
  5. Run systemctl daemon-reload (or systemctl --user daemon-reload)
  6. Inspect the generated unit with systemctl cat
  7. Start the Quadlet service
  8. Verify the container, port, and application behavior
  9. Confirm volume data is still present
  10. Reboot or log out and back in when boot or linger behavior matters
  11. Remove the old generated .service from /etc/systemd/system/ only after the new path is proven

Parallel migration causes predictable conflicts: if the legacy service still binds host port 8080, the Quadlet unit cannot claim it. The same applies to ContainerName= if the old container still exists. Stop and remove the legacy managed resources first — that is not a Quadlet bug.


Verify volume data was not lost

Before migration, record the volume:

bash
podman volume inspect web-data --format '{{.Name}} {{.Mountpoint}}'

Sample output:

output
web-data /var/lib/containers/storage/volumes/web-data/_data

Seed or confirm application data before cutover:

bash
podman run --rm -v web-data:/data registry.access.redhat.com/ubi9/httpd-24:latest sh -c 'echo migration-marker > /data/marker.txt'

After the Quadlet service starts, read the same path inside the running container:

bash
podman exec old-web cat /data/marker.txt

Sample output:

output
migration-marker

If you switch from Volume=web-data:/data to Volume=web-data.volume:/data without setting VolumeName=, Quadlet may create a differently named volume such as systemd-web-data and the application will look empty. When reusing existing data, pin the name in the volume unit:

ini
[Volume]
VolumeName=web-data

podman generate systemd can still be used

Deprecation is guidance, not removal. Existing stable workflows do not need an emergency rewrite solely because of the warning.

Situation Recommendation
Legacy unit works and rarely changes keep it until you have a reason to migrate
New systemd-managed Podman workload use Quadlet
Active modernization or drift problems migrate to Quadlet
Want first-class AutoUpdate= or declarative volumes/networks migrate to Quadlet

Upstream currently has no plans to remove the command, but new systemd-managed workloads should use Quadlet. Plan around Quadlet for new work without assuming legacy units break on upgrade.


Troubleshooting migration cutover

Symptom Likely cause Fix
address already in use on start old service still holds the host port stop legacy unit and container before starting Quadlet
name already in use old container name still exists podman rm -f old-web or match ContainerName= to a free name
Application data missing after cutover new volume name from .volume unit set VolumeName=web-data or keep Volume=web-data:/data
systemctl enable fails enabling a generated transient unit use [Install] WantedBy= in the Quadlet file
systemd-old-web instead of old-web no ContainerName= add ContainerName=old-web
Generated unit differs after edit edited /run/systemd/generator/ file edit the .container source and daemon-reload
Rootless unit does not start at boot no linger loginctl enable-linger USER — see rootless guide

References


Summary

podman generate systemd is deprecated but still available on Podman 5.8.2 — urgent fixes only, no new features, and no announced removal. The migration value is declarative configuration: you stop maintaining a frozen ExecStart line and instead edit a .container file that the generator turns into a current unit on every daemon-reload.

The practical work is translation. Map --name, -p, -e, and -v from your legacy unit to ContainerName=, PublishPort=, Environment=, and Volume=. Keep ContainerName= when anything outside systemd still calls the old name. Put boot wiring in [Install] instead of systemctl enable on the generated service path. If you used --new before, you already had recreate-on-start semantics — Quadlet continues that model and lines up with auto-update.

Cut over deliberately: stop the old service to free ports and names, validate with systemctl cat and podman exec, and confirm volume data before you delete the legacy unit file. Stable legacy services can wait; new workloads and active refactors should land on Quadlet from the start.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)