Podman: How do I preserve or share my image?

This article is part of a series.

Vocabulary Overview

When talking about moving things it helps to be clear about what is being moved and to what kind of place.

Tags were what I needed clarified the most. One already showed up when doing a pull. It’s the :latest. latest is the default when nothing is specified as well.

podman pull docker.io/library/httpd:latest 

I suspected that like with git tags there’d be conventions around their usage so here is some reading:

Make Sure There’s an Image to Work With

So imagine needing to reinit the Mac’s podman VM.

podman machine stop
podman machine rm
podman machine init
podman machine start
podman images

On the start up there’d be no images! Including the new template! How to keep it around even if the podman VM gets wiped? How to share one once it’s ready?

First, recreate it:

# docker login #if needed
podman pull docker.io/library/httpd:latest # login required!
podman run -dt -p 8080:80/tcp docker.io/library/httpd
podman ps
podman exec -it {$CONTAINER_ID} bash
:/usr/local/apache2# > echo '<html><body><h1>Still works!</h1></body></html>' > htdocs/index.html
:/usr/local/apache2# > exit
curl localhost:8080

podman commit {$CONTAINER_ID} my-httpd-template
podman images
# podman image rm {$CONTAINER_ID} ## to delete

Option 1: Save to an archive

First option to preserve the image even if we delete podman machine again is to use the save command that takes an output on the host mac.

podman save --output httpd_template_image.tar localhost/my-httpd-template

saves a copy of the image (or images if desired) to an archive file called httpd_template_image.tar in the pwd using save

One could load it back into podman with load

podman load --input httpd_template_image.tar

Option 2: Export is for Containers

Export is similar to save, but for use on containers, not images. Using import takes that export archive of a container and will bring it in as an image.

So if we really wanted to we could have used it on our changed httpd container rather than commit the container to an new image.

## will create tar file in pwd
podman export --output changed_httpd_container.tar {$CONTAINER_ID}
podman import changed_httpd_container.tar imported-httpd-image

Option 3: podman push :dir / podman cp

If one just wanted to move the folder around the podman machine one could pretty easily get a copy using push just to another directory. push :dir is only for moving items within the same machine.

podman machine ssh
core@localhost:~ mkdir -p ~/container_tmp
# podman-machine-default has podman installed too! 
core@localhost:~ podman push my-httpd-template dir:/Users/$USER/container_tmp/my-httpd-template
core@localhost:~ ls container_tmp/

This DOES NOT get the files out of podman-machine-default. /Users/$USER/container_tmp/ is still on the VM. To move the files to the host machine use podman machine cp from the host mac.

# this moves the folder we just created with the push. 
podman machine cp podman-machine-default:/var/home/core/container_tmp/my-httpd-template ~/saved_containers/my-httpd-template

This is different than podman cp, which is for copying to, from and between containers. That page also provides alternatives to itself for moving files around containers.

Option 4: Using push to a remote registry.

Rather than just keeping a hand-tooled local backup, the more usual would be to push useful images up to a registry. I am going to show two. Docker Hub and Digital Ocean.

Docker Hub

If you’ve followed along, you have a login solution because it was required to get the image, but see the end of this section for more info.

One can create a new repository Docker Hub’s repository home page:

Notice I am saying repository. The registry is docker hub. Each project gets a repository for all its versions.

Instead of creating one from the web-gui, I pulled out the CLI directions.

docker tag local-image:tagname new-repo:tagname
docker push new-repo:tagname

This translates to podman as:

podman tag localhost/my-httpd-template docker.io/$DOCKER_USERNAME/my-httpd-template:v1
podman images #optional. see the new image (it's still only local)
podman push docker.io/$DOCKER_USERNAME/my-httpd-template:v1

This exact repository can be seen for now at:

A reminder about logging in:

Option A (docker login):

docker login

Option B (podman login):

podman login docker.io
Username: $DOCKER_USERNAME
Password: 

Note: if the expected password for a service doesn’t work, try an API key.

Digital Ocean

Create a container registry. There is a free version that allows for 1 and only 1 repository, but that’s okay for this. (affiliate link with some free credits if useful.)

The steps walk through the process of installing THEIR cli and link to how to create an API token as needed. DO NOT LOOSE IT. (Passwords, BitWarden, LastPass, 1Password, Encrypted Note, Keychain Secure Note… something.)

As a note on Digital Ocean, I used to unreservedly recommend them because they put so much work into high quality documentation that covered topics in general, not just how to do things on their platform. It was a real differentiator. Made me willing to pay a premium to support the public good they provided. Back in 2023 they laid off a bunch of those folks, changed their docs home page and blog topics to be more DO specific and leaned into AI… so ¯\_ (ツ)_/¯. At least they still seems to have straight forward pricing models that prevent surprises.

Logging in

One can either login through the digital ocean CLI, registering the API key with Docker directly, or the podman login process.

## Option A
doctl registry login
## Option B
docker login registry.digitalocean.com
#> Username: <registered-email>
#> Password: <paste-api-token>
## Option C
podman login registry.digitalocean.com
#> Username: <registered-email>
#> Password: <paste-api-token>

push to Digital Ocean

# LOGIN FIRST
podman tag localhost/my-httpd-template registry.digitalocean.com/$DO_REGISTRY_NAME/my-httpd-template:v1
podman images #optional. see the new image (it's still only local)
podman push registry.digitalocean.com/$DO_REGISTRY_NAME/my-httpd-template:v1

This one is PRIVATE, so I can’t provide a link. It uses 45.65 MB / 500 MB of my space. YIKES. That’s a lot for one wee little web page. But its storing the whole server profile, so it won’t get much bigger than that as more content gets added.

Option 5: A local registry

Sometimes one wants a registry on ones own machine or for the local network. For testing, for privacy, for control, for bandwidth. Many reasons. The examples I found were for Linux machines, and there are a couple of gotcha’s porting them to Mac.

The TL;DR

## Make the storage folder
mkdir -p ~/web_tools/registry
## pull and run the registry image from docker using that folder
## configured to use the folder we want and the port we want. 
podman run -d --name my_registry -p 5050:5000 -v /Users/$USER/web_tools/registry:/var/lib/registry --restart=always registry:latest
## tag the image over to the new registry port
podman tag localhost/my-httpd-template localhost:5050/my-httpd-template
## see that no files are there yet
ls ~/web_tools/registry
## push to the registry
podman push localhost:5050/my-httpd-template --tls-verify=false 
## see the files are there now.
ls ~/web_tools/registry/docker/registry/v2/repositories/my-httpd-template

## Remove all the local versions
## See all the current images, some of mine had the same ID
## b/c of how I pushed to the remotes
podman images
## remove all the images with my-httpd-template in the name
podman image rm localhost:5050/my-httpd-template
podman image rm localhost/my-httpd-template
podman image rm -f $MY_DUPLICATED_ID
## should see no my-httpd-template
podman images

## make is so don't have to use --tls-verify=false every time and so its contents will appear in searches
## see the search without the registry
podman search my-httpd-template
podman machine ssh
## On the VM change the registries.conf in the local folder. 
## WARNING: this command will overwrite anything that is currently in it. 
core@localhost:~ echo -e 'unqualified-search-registries = ["localhost:5050", "docker.io", "ghrc.io", "quay.io", "registry.fedoraproject.org"]\n\n[[registry]]\nlocation="localhost:5050"\ninsecure=true' > $HOME/.config/containers/registries.conf
core@localhost:~ exit

## see the search with our local registry in the results
podman search my-httpd-template

## and pull from the local!
podman pull localhost:5050/my-httpd-template
## the image is back
podman images

When trying to figure out what all those commands should be there are four questions have to be answered:

Where will the files be stored?

The the files question can be easy to answer. I’m going to create a folder on the mac at /Users/$USER to start.

mkdir -p ~/web_tools/registry

The expected location for registry files is /var/lib/registry, and one could create that inside podman machine and set up a volume to use that instead, but for me the point is to have the registry saved to my user on the host mac.

Where is it going to be served to?

All the examples use port 5000 on both the host and the container. But on a Mac that’s typically in use (along with 7000) by the Control Center. To free it up one needs to turn off Continuity, AirPlay… things I like using.

## See whose on your port
lsof -n -P -i :5000
## Dig further:
lsof -i
sudo skywalkctl flow -n -P $PID_CONTROL_CENTER
sudo skywalkctl netstat -a

So I am going to use a different port in this example. The nice thing is the container will still be able to use the default port (5000). This does mean when using the registry we will sometimes need to specify the port for software that would otherwise look for the default.

What tool is going to serve it?

The answer is to have podman run an existing registry image.

But how to give podman access to the folder and the port? We’re going to need to use some flags.

volume

[[SOURCE-VOLUME|HOST-DIR:]CONTAINER-DIR[:OPTIONS]]

How will podman know about the folder? Because it will be mapped to a volume.

One can create a volume in a separate step or at the time of container creation. At the time of container creation means the run command will handle all the appropriate configuration.

If one calls the command to run (and also pull if not already on the machine) a busybox image in one step:

podman run -dit --volume src:/dest busybox

and no volume called src already exists podman will create it on the podman vm at

/var/home/core/.local/share/containers/storage/volumes/src

The /dest part is where it maps to on the container’s file structure.

podman ps #to get the container id if didn't nab it. 
podman exec $BUSYBOX_CONTAINER_ID ls -ld /dest
podman exec $BUSYBOX_CONTAINER_ID touch hello.txt
podman machine ssh
core@localhost:~$ ls -la /var/home/core/.local/share/containers/storage/volumes/

NOTE: busybox can be used in the run command without docker.io/busybox because docker.io is one of the unqualified hosts in both the default registries.conf and the one updated in the last post. more

To do something a little more useful, where the source folder is accessible by the mac user, one can either set up a volume ahead of time or just pass in an actual path to an existing directory. While the folder for the SOURCE side must exist before hand, the one on the DESTINATION side does not. If it doesn’t exist it will be created as part of the container’s start up.

NOTE: the following commands will open an interactive shell immediately to help with testing file creation (-it without the d). The pwd will be /, not /var/home/core/ like with podman machine ssh.

mkdir -p ~/my_demo_volume_a
touch ~/my_demo_volume_a/waiting_to_be_seen.txt
podman run -it --rm --volume ~/my_demo_volume_a:/dest busybox
# / # cd /dest
# / # ls
# / # touch hello_a.txt
# / # exit

Or the long way. See discussion on this github issue

mkdir -p ~/my_demo_volume_b
## Look at how much has to be specified!!!
podman volume create --driver local --opt 'type=none' --opt "device=/Users/$USER/my_demo_volume_b" --opt 'o=bind' my_fancy_volume
podman run -it --rm --volume my_fancy_volume:/dest busybox

The volumes created by run where the source is a directory path do not show up in podman volume ls, only named ones do.

There is an alternate syntax for options with a colon after the destination parameter followed by comma separated options. A common one to see is z or Z. They seem to largely handle permission issues. Try letting run handle your situation before mucking about with them.

publish

[[ip:][hostPort]:]containerPort[/protocol]

In a run command, -p isn’t actually short for “port” but publish.

The registry will need to be available on the network since that’s what all the tooling expects. The publish option can map a single port or a range of ports. If a container has a lot of ports to expose, --publish-all, -P will map random host ports as needed. One can see the ports being used with podman ports.

my-httpd-template needed this option itself!

podman run -dt -p 8080:80/tcp localhost/my-httpd-template

Alternatives might have looked like:

# will only respond on 127.0.0.1, not all IPs (double check etc/hosts)
podman run -dt -p localhost:8080:80/tcp localhost/my-httpd-template
# to get a random available port. 
podman run -dt -p localhost::80/tcp localhost/my-httpd-template
# uses tcp by default
podman run -dt -p 8080:80 localhost/my-httpd-template
# use udp, scpt is the other option. To use more than one, -p for each.
podman run -dt -p 8080:80/udp localhost/my-httpd-template

restart

--restart=policy

From the docs, valid policy values are:

Our registry will use “always”, but keep in mind “Restart policy does not take effect if a container is stopped via the podman kill or podman stop commands.” (also from doc link.)

privileged

Older examples for creating a registry use --privileged. I am just going to pull the explanation from the run documentation:

Give extended privileges to this container. The default is false.

By default, Podman containers are unprivileged (=false) and cannot, for example, modify parts of the operating system. This is because by default a container is only allowed limited access to devices. A “privileged” container is given the same access to devices as the user launching the container, with the exception of virtual consoles (/dev/tty\d+) when running in systemd mode (–systemd=always).

A privileged container turns off the security features that isolate the container from the host. Dropped Capabilities, limited devices, read-only mount points, Apparmor/SELinux separation, and Seccomp filters are all disabled. Due to the disabled security features, the privileged field should almost never be set as containers can easily break out of confinement.

Containers running in a user namespace (e.g., rootless containers) cannot have more privileges than the user that launched them.

Newer examples leave this off, I suspect because narrower privileging around volumes seems to have improved in the intervening years. I am going to leave it off too, but wanted to mention it in case someone finds there way here after seeing an older example.

How will podman know how to find our registry?

To let podman know about the registry, update the $HOME/.config/containers/registries.conf from the last post to add it to the unqualified-search-registries and to indicate that it isn’t secure.

unqualified-search-registries = ["localhost:5050", "docker.io", "ghrc.io", "quay.io", "registry.fedoraproject.org"]

[[registry]]
location="localhost:5050"
insecure=true'

Summary

So these are 5 options on how to get images out of the podman VM on the mac and out to somewhere that will survive if it’s nuked.

The next post will be about improving our image, and maybe writing one from scratch.

This article is part of a series.