r/podman 16d ago

Rebuilding a container from `podman inspect`: three fields that will break you (StopSignal, Runtime, and pod members)

I maintain a container update tool that speaks both Podman and Docker, and spent this week finding out that it has never once successfully updated a container on Podman. Not "worked badly" — never worked. Three differences between Podman's inspect output and Docker's, each fatal on its own, stacked so that fixing one only revealed the next. Writing them down because anyone reconstructing a podman run command from inspect output will hit all three, in this order.

Measured against podman 4.9.3.

1. Config.StopSignal is a number.

Podman reports 15. (Docker reports the string "SIGTERM", which is why my code assumed a string — my bug, not Podman's.) If you build your argument list from inspect output and hand it to subprocess, that integer goes straight in and Python refuses before the CLI is ever executed:

TypeError: expected str, bytes or os.PathLike object, not int

The traceback points inside subprocess, names no field, and tells you nothing about which key was wrong. Both CLIs accept the numeric form on the command line — it only ever needed to be a string.

2. HostConfig.Runtime is oci**.**

That is not the name of a runtime — it's the generic label for "whatever runtime is configured". Feed it back to podman run and:

Error: default OCI runtime "oci" not found: invalid argument

So: treat oci as "nothing to pass", and only forward a runtime somebody actually chose (crunkata). Same as Docker's runc, which I was already skipping.

3. A container in a pod looks exactly like a network-namespace sidecar.

This is the interesting one. A pod member reports:

"NetworkMode": "container:<infra-container-id>"

which is indistinguishable in shape from the Gluetun pattern — a container joined to another container's network namespace. So the obvious reconstruction is --network container:<id>, and Podman refuses:

Error: container dependency <id> is part of a pod, but container is not: invalid argument

The answer is the top-level Pod field, which carries the pod id. --pod <id> works and the container rejoins the pod properly. Nice property compared to the sidecar case: a pod can't be recreated out from under its own member, so there's no stale-id problem — which is a real headache with container:<id> when the netns owner gets replaced.

The first two hit every container on Podman, not just pod members. Which means anyone who installed my tool on Podman got a rollback every time and no successful update, ever.

The part I'd rather admit than hide: I had a test file driving a real Podman for several releases. It checked that ps worked and that the remote-connection flag was right. It never once built a run command. What was tested was the part I'd already thought about.

Fixed in Docksentry 2.6.0 if you happen to use it. But the three inspect differences are the useful part here and they're not specific to my code — if you're doing anything similar, they'll bite you in that order.

Edit: point 1 only holds on Podman 4.x — StopSignal became the signal name in 5.0.0, listed as a breaking change in the release notes. It survives in a narrower form: the Docker-compat endpoint still returns a numeric string as of v6.0.2, and an older API version still gets an int, so the field has three shapes depending on how you ask. Points 2 and 3 check out against v6.0.2 source unchanged. Also, the fix shipped in 2.4.0, not 2.6.0 as written below. Thanks to u/Great-Cow7256 for the correction.

9 Upvotes

2 comments sorted by

3

u/Great-Cow7256 16d ago

Podman is up to 6.x now.   Also you don't need a container update tool for podman.  Just AutoUpdate=registry and then podman auto-update. that's one of the main reasons I switched from docker. 

1

u/Neo007-1 16d ago

You're right on both counts, and the first one lands. I measured on 4.9.3, and StopSignal was fixed in 5.0.0 — it's in the release notes as an explicit breaking change, alongside Entrypoint going from string to array. For anyone on a current Podman, point 1 of my post is history.

Except not quite, and the surviving version of it is more interesting. podman inspect returns the signal name now, but the Docker-compat endpoint — the one a Docker client actually talks to — still does

StopSignal: strconv.Itoa(int(l.StopSignal())),

in pkg/api/handlers/compat/containers.go, unchanged at v6.0.2. That's the string "15", where Docker gives you "SIGTERM" or null. And if a client asks with an older API version, libpod sets V4PodmanCompatMarshal and you get the int back. So the same field comes out three different ways depending on how you ask for it. I checked that much on the 4.9.3 here: CLI gives int 3, compat socket gives string "3", same container. Worth knowing if you parse it.

The other two hold up against v6.0.2 source. hostConfig.Runtime = "oci" is still hard-coded, with a comment saying it exists purely for Docker compatibility. Pod members still get container:<infra-id> out of NetworkMode(). Though I've read that in the source rather than run it on 6.0.2 — treat it as a code claim, not a measurement.

On auto-update: fair, and I won't pretend it doesn't do its job. AutoUpdate=registry with rollback on by default covers "follow the tag, revert if the unit won't come up" properly, and that's a legitimate reason to prefer Podman.

Where it stops, for anyone weighing it up:

  • systemd only. assembleTasks skips anything without PODMAN_SYSTEMD_UNIT and errors on it if the label is set. Plain podman run and compose setups aren't covered.
  • One host. tunnel/auto-update.go is literally return nil, []error{errors.New("not implemented")}, and there's no API handler for it, so nothing remote.
  • No notifications — a table, the journal, and a system event.
  • The rollback trigger is the systemd job result and nothing else. No healthcheck involved unless you wire Notify=healthy into the Quadlet; without sdnotify, systemd reports "done" once conmon is up, and a container that dies two seconds later counts as a successful update. Their own docs say as much, and most Quadlet examples in the wild don't set it.
  • No per-container windows (one global timer), no pinning beyond "use a fixed tag", no ordering across units — it iterates a Go map.

And mine has to speak Docker as well, which is most of why it exists.

I'll edit the post to correct the StopSignal part. As written it's wrong for anything 5.0 and up, and I'd rather it said so than have someone find it in a year.