Using the Podman/Docker CLI
The App Devs Primer on Images and Containers, Part II
Comment •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
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. |
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
# 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.
Related utilities
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:
-
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.
-
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.
-
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.
-
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.
-
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
-
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
. -
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.
-
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.
-
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.
-
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
-
In a SECOND, separate terminal on your host system and outside my_first_container, create a greetings.txt file.
$ echo -n "Howdy" > greeting.txt
-
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. -
Print the contents of the file in the container.
/ # cat greeting.txt
Output:
Howdy
-
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
-
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. -
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
. -
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. -
List the containers again to show they are not running anymore.
$ podman ps
Output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
-
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
-
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
-
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
andpodman 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.