CertGrid CertGrid
Installation·Podman

Podman Installation on Ubuntu 26.04

Podman installs from Ubuntu's own repository in a single command. Then `systemctl is-active podman.service` reports `inactive`, and a container runs anyway - which is the whole architecture in one line of output.

Foundations Guide 1 of 47 Beginner

Written against the versions above. Podman follows the distribution here rather than a vendor repository, so the version you get is the one Ubuntu shipped. The commands are stable across 5.x.

Every command on this page ran on podman01.
Server NameIP AddressOSRolesCPURAMHDD
PODMAN01192.168.0.24Ubuntu 26.04 LTSPrimary Container Host2 Core4 GB50 GB

Before you start

  1. Start from a host with no engine on it

    Worth proving rather than assuming, because almost every confusing result later in this path comes from state left behind by something else.

    Three answers to collect. The release, so you know which package archive you are pulling from. Then podman --version, which should fail - and note the exit code, 127, meaning the command does not exist rather than the engine being broken. Then the systemd unit files, which should list none.

    That last one is the one to remember. Come back to it after the install.

    bash Example session
    . /etc/os-release && echo "$PRETTY_NAME"Ubuntu 26.04 LTSpodman --versionbash: line 1: podman: command not found[exit 127]systemctl list-unit-files 'podman*'UNIT FILE STATE PRESET 0 unit files listed.[exit 1]

    Expected resultUbuntu 26.04 LTS, command not found with exit 127, and 0 unit files listed.

    Success conditionYou have confirmed there is no Podman and no Podman service on the machine.

  2. One command, from Ubuntu's own repository

    Podman needs no third-party repository. This is a real difference from installing Docker Engine, where the first half of the job is adding a GPG key and an apt source. Here the package is in universe, which is enabled by default.

    Check what you are about to get first. apt-cache policy names the candidate version and the archive it comes from, which is the honest answer to "which Podman will I have" - it is whatever the distribution froze, not the latest release.

    Then install. Note the size of what arrives: 31 new packages, 291 MB. Podman is a command, not a service, but it is a command with a lot of parts, and step 5 goes through the ones that matter.

    bash Example session
    apt-cache policy podmanpodman:  Installed: (none)  Candidate: 5.7.0+ds2-3build1  Version table:     5.7.0+ds2-3build1 500        500 http://in.archive.ubuntu.com/ubuntu resolute/universe amd64 Packagessudo -n apt-get updateHit:1 http://security.ubuntu.com/ubuntu resolute-security InReleaseHit:2 http://in.archive.ubuntu.com/ubuntu resolute InReleaseGet:3 http://in.archive.ubuntu.com/ubuntu resolute-updates InRelease [137 kB]Hit:4 http://deb.gierens.de stable InReleaseHit:5 http://in.archive.ubuntu.com/ubuntu resolute-backports InReleaseGet:6 http://in.archive.ubuntu.com/ubuntu resolute-updates/main amd64 Packages [492 kB]Get:7 http://in.archive.ubuntu.com/ubuntu resolute-updates/universe amd64 Packages [244 kB]Fetched 873 kB in 3s (302 kB/s)Reading package lists...sudo -n apt-get install -y podmanReading package lists...Building dependency tree...Reading state information...Solving dependencies...The following additional packages will be installed:  aardvark-dns buildah catatonit conmon containers-storage cpp cpp-15  cpp-15-x86-64-linux-gnu cpp-x86-64-linux-gnu criu crun fuse-overlayfs  gcc-15-base golang-github-containers-common golang-github-containers-image  libcompel1 libcriu2 libisl23 libmpc3 libnet9 libprotobuf-c1 libslirp0  libsubid5 libyajl2 netavark passt python3-protobuf python3-pycriu  slirp4netns uidmapSuggested packages:  cpp-doc gcc-15-locales cpp-15-doc libwasmedge0 containernetworking-plugins  docker-composeThe following NEW packages will be installed:  aardvark-dns buildah catatonit conmon containers-storage cpp cpp-15  cpp-15-x86-64-linux-gnu cpp-x86-64-linux-gnu criu crun fuse-overlayfs  gcc-15-base golang-github-containers-common golang-github-containers-image  libcompel1 libcriu2 libisl23 libmpc3 libnet9 libprotobuf-c1 libslirp0  libsubid5 libyajl2 netavark passt podman python3-protobuf python3-pycriu  slirp4netns uidmap0 upgraded, 31 newly installed, 0 to remove and 54 not upgraded.

    Expected resultCandidate 5.7.0+ds2-3build1 from resolute/universe, then 31 newly installed packages.

    Success conditionapt-get install completed with no held or broken packages.

  3. Nothing is running, and that is correct

    This is the step that matters, and it is why the install guide is worth reading even if you already know apt.

    podman --version answers. podman info reports a working runtime, a network backend and rootless=true. So the engine is installed and functional. Now ask systemd about it:

    • systemctl is-active podman.service says inactive, exit 3
    • systemctl is-enabled podman.socket says disabled, exit 1

    Both of those are the correct, healthy, intended state. There is no daemon to start. podman is a command that forks a container and exits; the container's supervisor is a small process called conmon, one per container, not a long-running service that owns them all.

    A podman.socket unit does exist and you will enable it later - it serves the Docker-compatible API that lets real docker-compose talk to Podman - but it is opt-in, and nothing you do in the next twenty guides needs it.

    bash Example session
    podman --versionbash: line 1: podman: command not found[exit 127]podman info --format 'runtime={{.Host.OCIRuntime.Name}} conmon={{.Host.Conmon.Version}}'runtime=crun conmon=conmon version 2.1.13, commit: unknownpodman info --format 'network={{.Host.NetworkBackend}} rootless={{.Host.Security.Rootless}}'network=netavark rootless=truesystemctl is-active podman.serviceinactive[exit 3]systemctl is-enabled podman.socketdisabled[exit 1]

    Expected resultpodman version 5.7.0, runtime=crun, network=netavark, rootless=true, then inactive and disabled.

    Success conditionPodman answers for itself while systemd reports no running Podman service.

  4. Run a container, and read what it claims

    The image name is written out in full on purpose - docker.io/library/hello-world rather than hello-world. Podman does not assume a registry, and the short form asks you a question it cannot ask without a terminal. That is its own guide later in this track; for now, be explicit.

    Read the output carefully, because it is wrong:

     1. The Docker client contacted the Docker daemon.
     2. The Docker daemon pulled the "hello-world" image from the Docker Hub.

    There was no daemon. You confirmed that one step ago. What actually happened is that podman pulled the image itself, wrote it into your home directory, forked a container, and exited. The message comes from inside the image - it is a text file written by Docker, faithfully printed by Podman, describing an architecture that was not used.

    It is a good reminder for the rest of this path: an OCI image does not know or care which engine runs it, and plenty of container documentation is really Docker documentation.

    The storage location is the second thing to notice. graphroot is under /home/sysadmin/.local/share/containers/storage, not /var/lib/docker. You pulled an image without root and it went somewhere you own.

    bash Example session
    podman run --rm docker.io/library/hello-world 2>&1Trying to pull docker.io/library/hello-world:latest...Getting image source signaturesCopying blob sha256:4f55086f7dd096d48b0e49be066971a8ed996521c2e190aa21b2435a847198b4Copying config sha256:e2ac70e7319a02c5a477f5825259bd118b94e8b02c279c67afa63adab6d8685bWriting manifest to image destination Hello from Docker!This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.    (amd64) 3. The Docker daemon created a new container from that image which runs the    executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it    to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/podman info --format 'graphroot={{.Store.GraphRoot}} driver={{.Store.GraphDriverName}}'graphroot=/home/sysadmin/.local/share/containers/storage driver=overlay

    Expected resultThe pull, then the greeting, then a graphroot under your own home directory.

    Success conditionA container ran as your unprivileged user, with no daemon and no sudo.

  5. What else arrived, and which guide uses it

    Thirty-one packages is a lot for something that is "just a command". These are the ones worth knowing by name, because each becomes a subject later:

    • crun - the OCI runtime that actually creates the container. The thing that calls clone() and sets up the namespaces.
    • conmon - the per-container monitor. One of these per running container, holding its streams and reporting its exit. This is what a daemon would otherwise do.
    • netavark and aardvark-dns - the network backend and its DNS server. Podman 4 replaced CNI with these.
    • passt (providing pasta) and slirp4netns - userspace networking, which is how an unprivileged process gets a network at all.
    • uidmap - newuidmap and newgidmap, the setuid helpers that let you map a range of subordinate UIDs into a container.
    • buildah - podman build is a front end to this.
    • criu - checkpoint and restore, which is how a running container gets frozen to disk.

    One detail in the last command: netavark produces no path. It is not on your PATH because you never call it - Podman invokes it directly from /usr/lib/podman/. A helper the engine runs is not a command you run.

    And /etc/subuid already contains a range for your user. Ubuntu's useradd allocates 65,536 subordinate UIDs per account, which is why rootless worked in step 4 with no setup at all. On a distribution that does not do this, that step would have failed.

    bash Example session
    dpkg -l | grep -E 'ii +(podman|netavark|aardvark-dns|conmon|crun|containers-common|passt|golang-github-containers-common)' | awk '{print $2, $3}'aardvark-dns 1.16.0-3conmon 2.1.13+ds1-2crun 1.21-1ubuntu3golang-github-containers-common 0.66.0+ds2-3netavark 1.16.1-3.1passt 0.0~git20260120.386b5f5-1podman 5.7.0+ds2-3build1command -v podman conmon crun netavark pasta/usr/bin/podman/usr/bin/conmon/usr/bin/crun/usr/bin/pastacat /etc/subuidsysadmin:100000:65536

    Expected resultSeven package versions, four of five binaries resolved, and a subuid range for your user.

    Success conditionYou can name the runtime, the monitor, the network backend and the UID helper on your own machine.

Troubleshooting

Official sources