Overview

There are two competing utilities for building and running containers, Podman and Docker. They are available for Windows, Linux, and Macs, and from the end users point of view, they are completely equivalent when it comes to executing commands; i.e. they can be considered aliases of one another. On *nix-based systems, alias docker=podman will work if only Podman is installed when using basic commands for building images or running containers.

🛈
Because we feel that Podman is the better tool technologically, this primer will only reference Podman; however, because Docker and Podman are conceptually and semantically equivalent, wherever podman is mentioned in this document or the code, docker can be safely substituted with no change in meaning or outcome.

Prerequisites

This primer assumes the reader is familiar with the basic concepts behind OCI images and containers. If not, we suggest reading Part I of this series first to cover those topics before continuing.

Common Subcommands

The following are a sampling of the most commonly used Podman subcommands. Many are shorthand for podman image <command> or podman container <command>. Each subcommand listed below links to the official documentation for complete information.

build

Build an image.

# build an image tagged as some-tag for image repository some-image-repo
# assumes a file named Dockerfile in the current directory
podman build -t some-image-repo:some-tag .
# build an image tagged as some-tag for image repository some-image-repo
# using a file named my-dockerfile.base-image in the current directory
podman build --tag some-image-repo:some-tag --file my-dockerfile.base-image
🛈
While the OCI specification actually denotes Containerfiles to be the default, Docker isn’t fully OCI compliant as of the writing of this primer. Because most publicly available projects still use Dockerfiles, for convenience and historical purposes these primers will only refer to Dockerfiles.

help

Help about any subcommand.

# print a list of Podman's subcommands and options available with every subcommand
podman help
# get help with the container ls subcommands
podman help container ls

history

Print the history of an image, which includes information on each layer in the image, its size, and how it was created.

# print the history of the python:alpine image
podman history python:alpine
  • images - List all images stored locally. Shorthand for podman image list.

    podman images
  • inspect - Print the manifest of an image.

    # print the manifest of the python:alpine image
    python inspect python:alpine

kill

Forcibly kill the main process in one or more running containers. Less graceful than stopping a container.

# kill container_a and container_b
podman kill container_a container_b
  • ps - Lists all running containers. Shorthand for podman container list.

    podman ps

pull

Pull an image from an image registry.

# pull the busybox image
podman pull busybox

+

🛈

Docker Hub uses aliases for many of the common, official images it hosts; e.g.

  • busybox - the "Swiss Army knife of Embedded Linux"

  • alpine - minimalist image based on musl libc and busybox.

push

Push an image to an image registry.

# Push the image myimage:dev to Docker Hub under the account "username"
podman push myimage:dev docker.io/username/myimage:dev
podman ps

rm

Delete a container.

# delete my_container if it's already stopped
podman rm my_container
# force delete my_container
podman rm -f my_container
# delete all containers
podman rm --all

rmi

Delete a local image. Only works if no containers based on the image exist. Short for podman image rm.

# delete the python:apline image
podman rmi python:apline
# delete all locally stored images
podman rmi --all

run

Run a container. Run is shorthand for podman create and podman start.

# Launches a container based on the busybox image with an interactive
# shell terminal
podman run -i -t busybox /bin/sh

stop

Stop the main process in one or more running containers. A more graceful request than kill.

# stop container_a and container_b
podman stop container_a container_b

tag

Optionally rename and/or tag an image. If no tag follows the ":", defaults to "latest".

# Completely rename the image
podman mylocalimage:some-tag myorg/some-other-name:some-other-tag
# Renames the latest some-image-name to some-other-image-name:latest
podman some-image-name some-other-image-name
# Tag an image by its id to some-other-image-name:v1.0
podman 05455a08881e some-other-image-name:v1.0
podman inspect alpine

Fundamental differences between Docker and Podman

Docker is a daemon in *nix based systems, and a service in Windows. This means it runs in the background all the time, and it runs with root or administrator privileges.

Podman is binary utility. This means it runs only on demand, and can be run with an unprivileged user.

Besides being more efficient with system resources, this makes Podman more secure. Running anything with root/admin privileges is by definition less secure.

While Docker is a singular tool, Podman exists within a suite of utilities that are maintained by the Containers community on GitHub. Two of these utilities that are worth mentioning here are Skopeo and Buildah.

Buildah

For the most part, builds can be executed through Docker and Podman, but Buildah exists in case more complicated builds of OCI images are required than what Podman/Docker can provide through Dockerfiles. Buildah can also build Dockerfiles. Complex image builds without a Dockerfile are outside the scope of this primer, and application developers should never need this functionality, but we mention it here for completeness.

Skopeo

Skopeo exists to help manage images in remote registries; e.g. copy from one remote registry to another, remote image tagging, or deleting from a remote registry. Podman only recently added the capability of copying and tagging remote images via podman scp, but only skopeo can delete a remote image. Copying, tagging, and deleting remote images are more DevOps than application developer concerns and therefore out of scope for this primer, but we mention it here for completeness.

Installing Podman or Docker

The following lab was created and tested in Fedora Linux in a Bash terminal with or Podman and Git already installed. The lab will work if you would prefer to use Docker. If needed, follow the links in this paragraph for installation instructions of any of the mentioned utilities.

Windows users can use the Windows Subsystem for Linux, available to install on all Windows 10+ versions for free.

As a reminder, if you install Docker, podman and docker commands are completely interchangeable, especially for the purposes of this lab.

Practical Lab (~15 mins)

The following is a brief lab designed as an introduction to using the Podman CLI and demonstrating some OCI images and concepts. The lab was tested under Fedora 39, which uses SELinux. It should work on other OS’es with other security modules enabled.

💡
All text with a dollar sign or hash prompt and gray background is meant to be typed by the user in a BASH compatible *nix shell. Just click through and launch the terminal. Windows users running can use the Window Subsystem for Linux, run a full update at the Bash terminal and install Podman if it isn’t already installed. Alternative, Windows users can use a Windows terminal if Docker or Podman is installed, and while the labs are not tested or designed for use in Windows terminals, they should work.

Local terminal prompt: $some_command #
Container terminal prompt: / # some_command

The results of the command will have a similar gray background format and be prefaced with the label "Output:"; e.g.

Output:

some_command output

Open a new terminal:

  1. List your current set of images.

    $ podman images
    💡
    Podman has tab completion enabled for subcommands, command options, names of images on the local system, and the names of containers it has created.

    Output:

    REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

    It should be empty if your using Podman for the first time.

  2. Pull the alpine image.

    $ podman pull alpine

    Output:

    Resolved "alpine" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
    Trying to pull docker.io/library/alpine:latest...
    Getting image source signatures
    Copying blob 4abcf2066143 done   |
    Copying config 05455a0888 done   |
    Writing manifest to image destination
    05455a08881ea9cf0e752bc48e61bbd71a34c029bb13df01e40e3e70e0d007bd

    Notice that output resolves "alpine" as an alias.

  3. List your images again to show alpine is now locally stored.

    $ podman images

    Output:

    REPOSITORY                TAG         IMAGE ID      CREATED       SIZE
    docker.io/library/alpine  latest      05455a08881e  2 months ago  7.67 MB

    Note the tag is the default, latest, since we didn’t specify a tag.

  4. Pull the Python image based on the alpine image.

    $ podman pull python:alpine

    Output:

    Resolved "python" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
    Trying to pull docker.io/library/python:alpine...
    Getting image source signatures
    Copying blob b62713ed4827 done   |
    Copying blob 3a6cecfe7003 done   |
    Copying blob c3cdf40b8bda done   |
    Copying blob 4abcf2066143 skipped: already exists
    Copying blob 60d2faee92e7 done   |
    Copying config f44387b482 done   |
    Writing manifest to image destination
    f44387b482817f41bdac1892c45711adaedb3a7dd381844cdc3f360e66314d7a

    Note that python also resolves to an alias on Docker Hub. Also notice one of the layers was skipped, which represents the alpine layer that we just pulled. Podman caches layers for efficiency.

  5. List your images again to show both images are now locally stored.

    $ podman images

    Output:

    REPOSITORY                TAG         IMAGE ID      CREATED       SIZE
    docker.io/library/python  alpine      f44387b48281  3 days ago    60.2 MB
    docker.io/library/alpine  latest      05455a08881e  2 months ago  7.67 MB
  6. Inspect the local alpine image.

    $ podman inspect alpine

    Output:

    [
        {
              "Id": "05455a08881ea9cf0e752bc48e61bbd71a34c029bb13df01e40e3e70e0d007bd",
              "Digest": "sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b",
              "RepoTags": [
                  "docker.io/library/alpine:latest"
              ],
              "RepoDigests": [
                  "docker.io/library/alpine@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0",
                  "docker.io/library/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b"
              ],
              "Parent": "",
              "Comment": "",
              "Created": "2024-01-27T00:30:48.743965523Z",
              "Config": {
                  "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
                  ],
                  "Cmd": [
                        "/bin/sh"
                  ]
              },
              "Version": "20.10.23",
              "Author": "",
              "Architecture": "amd64",
              "Os": "linux",
              "Size": 7671366,
              "VirtualSize": 7671366,
              "GraphDriver": {
                  "Name": "overlay",
                  "Data": {
                        "UpperDir": "/home/hippyod/.local/share/containers/storage/overlay/d4fc045c9e3a848011de66f34b81f052d4f2c15a17bb196d637e526349601820/diff",
                        "WorkDir": "/home/hippyod/.local/share/containers/storage/overlay/d4fc045c9e3a848011de66f34b81f052d4f2c15a17bb196d637e526349601820/work"
                  }
              },
              "RootFS": {
                  "Type": "layers",
                  "Layers": [
                        "sha256:d4fc045c9e3a848011de66f34b81f052d4f2c15a17bb196d637e526349601820"
                  ]
              },
              "Labels": null,
              "Annotations": {},
              "ManifestType": "application/vnd.docker.distribution.manifest.v2+json",
              "User": "",
              "History": [
                  {
                        "created": "2024-01-27T00:30:48.624602109Z",
                        "created_by": "/bin/sh -c #(nop) ADD file:37a76ec18f9887751cd8473744917d08b7431fc4085097bb6a09d81b41775473 in / "
                  },
                  {
                        "created": "2024-01-27T00:30:48.743965523Z",
                        "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
                        "empty_layer": true
                  }
              ],
              "NamesHistory": [
                  "docker.io/library/alpine:latest"
              ]
        }
    ]

    If you read through the information above, you can get information on the image digest, tags, and the "Config.Cmd" section explains that the command that is run when the container is launched is sh.

  7. List your running containers.

    $ podman ps

    Output:

    CONTAINER ID  IMAGE       COMMAND     CREATED     STATUS      PORTS       NAMES

    Again, it should be empty like the initial list of images above if your using Podman for the first time, or simply have no running containers.

  8. Launch a alpine container with an interactive shell and use it.

    $ podman run --interactive --tty --name my_first_container alpine

    Output:

    / #

    You should now be in an interactive shell running inside the alpine container. This bypasses the original command that would have been run so we can interact with the image directly. Running an interactive shell in this manner can be useful during development for debugging purposes.

  9. List everything in the current directory.

    / # ls -al

    Output:

    total 12
    dr-xr-xr-x    1 root     root            40 Apr 12 16:46 .
    dr-xr-xr-x    1 root     root            40 Apr 12 16:46 ..
    drwxr-xr-x    2 root     root          4096 Jan 26 17:53 bin
    drwxr-xr-x    5 root     root           360 Apr 12 16:46 dev
    drwxr-xr-x    1 root     root            25 Apr 12 16:46 etc
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 home
    drwxr-xr-x    7 root     root          4096 Jan 26 17:53 lib
    drwxr-xr-x    5 root     root            44 Jan 26 17:53 media
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 mnt
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 opt
    dr-xr-xr-x  710 nobody   nobody           0 Apr 12 16:46 proc
    drwx------    1 root     root            26 Apr 12 16:50 root
    drwxr-xr-x    1 root     root            42 Apr 12 16:46 run
    drwxr-xr-x    2 root     root          4096 Jan 26 17:53 sbin
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 srv
    dr-xr-xr-x   13 nobody   nobody           0 Apr  5 15:23 sys
    drwxrwxrwt    2 root     root             6 Jan 26 17:53 tmp
    drwxr-xr-x    7 root     root            66 Jan 26 17:53 usr
    drwxr-xr-x   12 root     root           137 Jan 26 17:53 var

    Since alpine is a simple Linux container, as expected the output is the contents of the root directory of a typical Linux system.

  10. Create a file in the container filesystem, and list everything in the directory again.

    / # touch my_first_container.txt
    / # ls -al

    Output:

    total 12
    dr-xr-xr-x    1 root     root            70 Apr 12 23:15 .
    dr-xr-xr-x    1 root     root            70 Apr 12 23:15 ..
    drwxr-xr-x    2 root     root          4096 Jan 26 17:53 bin
    drwxr-xr-x    5 root     root           360 Apr 12 16:46 dev
    drwxr-xr-x    1 root     root            25 Apr 12 16:46 etc
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 home
    drwxr-xr-x    7 root     root          4096 Jan 26 17:53 lib
    drwxr-xr-x    5 root     root            44 Jan 26 17:53 media
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 mnt
    -rw-r--r--    1 root     root             0 Apr 12 23:15 my_first_container.txt
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 opt
    dr-xr-xr-x  707 nobody   nobody           0 Apr 12 16:46 proc
    drwx------    1 root     root            26 Apr 12 16:50 root
    drwxr-xr-x    1 root     root            42 Apr 12 16:46 run
    drwxr-xr-x    2 root     root          4096 Jan 26 17:53 sbin
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 srv
    dr-xr-xr-x   13 nobody   nobody           0 Apr  5 15:23 sys
    drwxrwxrwt    2 root     root             6 Jan 26 17:53 tmp
    drwxr-xr-x    7 root     root            66 Jan 26 17:53 usr
    drwxr-xr-x   12 root     root           137 Jan 26 17:53 var
  11. In a SECOND, separate terminal on your host system and outside my_first_container, create a greetings.txt file.

    $ echo -n "Howdy" > greeting.txt
  12. Run a second container with an interactive shell, and list everything in the current directory. The --mount options is there to mount the greetings.txt file into the new container.

    🛈
    The --security-opt label=disable is for systems using SELinux.
    $ podman run --interactive --tty \
      --mount type=bind,source=$(pwd)/greeting.txt,target=/greeting.txt \
      --security-opt label=disable \
      --name my_second_container alpine
    / # ls -al

    Output:

    total 12
    dr-xr-xr-x    1 root     root            71 Apr 12 23:20 .
    dr-xr-xr-x    1 root     root            71 Apr 12 23:20 ..
    drwxr-xr-x    2 root     root          4096 Jan 26 17:53 bin
    drwxr-xr-x    5 root     root           360 Apr 12 23:20 dev
    drwxr-xr-x    1 root     root            25 Apr 12 23:20 etc
    -rw-r--r--    1 root     root             0 Apr 12 23:20 greeting.txt
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 home
    drwxr-xr-x    7 root     root          4096 Jan 26 17:53 lib
    drwxr-xr-x    5 root     root            44 Jan 26 17:53 media
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 mnt
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 opt
    dr-xr-xr-x  725 nobody   nobody           0 Apr 12 23:20 proc
    drwx------    1 root     root            26 Apr 12 23:20 root
    drwxr-xr-x    1 root     root            42 Apr 12 23:20 run
    drwxr-xr-x    2 root     root          4096 Jan 26 17:53 sbin
    drwxr-xr-x    2 root     root             6 Jan 26 17:53 srv
    dr-xr-xr-x   13 nobody   nobody           0 Apr  5 15:23 sys
    drwxrwxrwt    2 root     root             6 Jan 26 17:53 tmp
    drwxr-xr-x    7 root     root            66 Jan 26 17:53 usr
    drwxr-xr-x   12 root     root           137 Jan 26 17:53 var

    There is no file called my_first_container.txt, and you can see the greeting.txt file, which was mounted in the root filesystem on the container from the host filesystem. This demonstrates how each container runs as if in it’s own, individual host, and has it’s own, separate filesystem.

  13. Print the contents of the file in the container.

    / # cat greeting.txt

    Output:

    Howdy
  14. Add some text to the file in the container, and print greeting.txt again.

    / # echo -n ", y'all" >> greetings.txt
    / # cat greeting.txt

    Output:

    Howdy, y'all
  15. In a THIRD, separate terminal on your host system and outside my_second_container, print greeting.txt.

    $ cat greeting.txt

    Output:

    Howdy, y'all

    This demonstrates that greeting.txt is not a copy in the container, but is actually the file on the host system.

    💡
    Mounting files into a container is how applications can keep their configurations external to the container. This is one way "Build once, deploy many" is able to be realized across many deployment environments, and change over time without necessarily needing to rebuild the image.
  16. On the host terminal, list the running containers.

    $ podman ps

    Output:

    CONTAINER ID  IMAGE                            COMMAND     CREATED        STATUS        PORTS       NAMES
    61b3a1045161  docker.io/library/alpine:latest  /bin/sh     5 minutes ago  Up 5 minutes              my_first_container
    92a7acea77c5  docker.io/library/alpine:latest  /bin/sh     2 minutes ago  Up 2 minutes              my_second_container

    This demonstrates that both containers are running on your local system, and each running from a shell launched from sh.

  17. Kill both running containers.

    $ podman kill my_first_container my_second_container

    Output:

    my_first_container
    my_second_container
    🛈
    If you look in your previous two terminals, you should now see both have returned to your local command prompt since both containers aren’t running anymore.
  18. List the containers again to show they are not running anymore.

    $ podman ps

    Output:

    CONTAINER ID  IMAGE                             COMMAND     CREATED        STATUS        PORTS       NAMES
  19. List containers that aren’t running to show they’re still on the system.

    $ podman ps -a

    Output:

    CONTAINER ID  IMAGE                            COMMAND     CREATED         STATUS                       PORTS       NAMES
    33808400cc0c  docker.io/library/alpine:latest  /bin/sh     14 minutes ago  Exited (137) 38 seconds ago              my_first_container
    db00ffdbd9ff  docker.io/library/alpine:latest  /bin/sh     7  minutes ago  Exited (137) 38 seconds ago              my_second_container
  20. Cleanup the system by removing the unused containers.

    $ podman rm my_first_container my_second_container my_third_container

    Output:

    my_first_container
    my_second_container
  21. Finish cleanup of the system by removing the unused images.

    $ podman rmi python:alpine alpine

    Output:

    Untagged: docker.io/library/python:alpine
    Untagged: docker.io/library/alpine:latest
    Deleted: f44387b482817f41bdac1892c45711adaedb3a7dd381844cdc3f360e66314d7a
    Deleted: 05455a08881ea9cf0e752bc48e61bbd71a34c029bb13df01e40e3e70e0d007bd
    💡
    We could typed podman rm --all and podman rmi --all, but blanket removal of everything might accidentally remove more than you expected, so best to avoid unless you’re absolutely sure.

The above output in the final two steps confirms all lab containers and images have been removed from the local system.

This concludes the lab.

Next Steps

Part III of this series will cover writing Dockerfiles for building your own images.

Comments