| 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:
podman generate systemd --helpSample 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:
podman create/run
↓
existing container configuration
↓
podman generate systemd
↓
.service fileQuadlet:
desired .container file
↓
systemd generator
↓
.service
↓
container created from declarationWith 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:
podman create --name old-web -p 8080:80 -e APP_ENV=production -v web-data:/data registry.access.redhat.com/ubi9/httpd-24:latestGenerate a unit that recreates the container on each start:
podman generate systemd --new --name old-webSample output (trimmed):
# 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.targetLines worth mapping to Quadlet:
ExecStart— fullpodman runwith ports, env, volumes, and imageExecStop/ExecStopPost— stop and remove using a cidfileRestart=on-failure— systemd restart policy (overridable with--restart-policy)--cidfile— tracks the container ID for stop/removeType=notify— conmon sdnotify integrationRequiresMountsFor=%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:
ExecStart=podman start old-web
ExecStop=podman stop old-webThat 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:
[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.targetMigration decisions in plain terms:
Image=— same registry image fromExecStartContainerName=old-web— preserves the name scripts andpodman psfilters expect (without it, Quadlet defaults tosystemd-old-web)PublishPort,Environment,Volume— lifted directly from-p,-e, and-v[Service] Restart=always— moves restart policy out of Podman CLI; we chosealwaysinstead of the generatedon-failurebecause this lab service should survive exits[Install] WantedBy=multi-user.target— replaces installing the generated file under/etc/systemd/system/and runningsystemctl enable
Reload and inspect what the generator built:
systemctl daemon-reloadRead the generated ExecStart and compare it to your legacy unit:
systemctl cat old-web.serviceThe 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:
-v web-data:/dataMinimal migration keeps the same line in Quadlet:
Volume=web-data:/dataWhen you want Quadlet to own volume creation declaratively, add web-data.volume:
[Volume]then reference it from the container:
Volume=web-data.volume:/dataThat 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:
Network=app-networkA Quadlet-managed network:
Network=app.networkPick 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:
Restart=on-failureQuadlet:
[Service]
Restart=alwaysDo 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:
systemctl enable container-old-web.serviceQuadlet-generated services live under the generator path and are transient. Enabling them directly fails:
systemctl enable old-web.serviceSample output:
Failed to enable unit: Unit /run/systemd/generator/old-web.service is transient or generatedBoot wiring belongs in the Quadlet source:
[Install]
WantedBy=multi-user.targetAfter 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:
~/.config/systemd/user/Quadlet source files belong under:
~/.config/containers/systemd/Manage them with the user manager:
systemctl --user daemon-reloadStart the migrated service in the user manager:
systemctl --user start old-web.serviceWhen 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:
ContainerName=old-webThis is one of the easiest regressions to prevent and one of the easiest to miss.
Migrate dependencies
Generated units may include:
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:
[Service]
ExecStart=/usr/bin/podman run --name old-web ...Right — declare intent in [Container]:
[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:
- Save the current
podman generate systemdoutput or the installed.servicefile - Stop the old service and container
- Resolve port and name conflicts before starting the Quadlet unit
- Write the
.containerfile under/etc/containers/systemd/or~/.config/containers/systemd/ - Run
systemctl daemon-reload(orsystemctl --user daemon-reload) - Inspect the generated unit with
systemctl cat - Start the Quadlet service
- Verify the container, port, and application behavior
- Confirm volume data is still present
- Reboot or log out and back in when boot or linger behavior matters
- Remove the old generated
.servicefrom/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:
podman volume inspect web-data --format '{{.Name}} {{.Mountpoint}}'Sample output:
web-data /var/lib/containers/storage/volumes/web-data/_dataSeed or confirm application data before cutover:
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:
podman exec old-web cat /data/marker.txtSample output:
migration-markerIf 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:
[Volume]
VolumeName=web-datapodman 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
- podman-generate-systemd(1)
- Podman Quadlet documentation — podman-systemd.unit(5) — transient generated units,
[Install],.volume, and.networkunits
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.

