Skip to content

Pull and Run Containers

Brief Introduction into Podman

At the time of writing Docker is the most popular and most widely used container solution on the market. Due to licensing restrictions the use of Docker is often forbidden in a larger context if used without a proper license. In the following we are using Podman, an open-source alternative, instead of Docker, but most of the commands can be used with Podman and Docker alike. Please refer to the Setup section for Podman for installation instructions for your OS.

Verify the Podman installation

podman run hello-world

Output

Hello from Docker!
This message shows that your installation appears to be working correctly.

Basic Podman Commands

Now that everything is set up, it is time to issue the first podman commands. Let's pull our first image from Dockerhub. As a start we want to pull this Python image.

podman pull python

Output

Resolved "python" as an alias (/etc/containers/registries.conf.d/shortnames.conf)
Trying to pull docker.io/library/python:latest...
Getting image source signatures
Copying blob 7d973fba4a67 done   | 
Copying blob 27ee9a825048 done   | 
Copying blob cbc19164244e done   | 
Copying blob 00f78834a2fe done   | 
Copying blob 44fed46b68cf done   | 
Copying blob e57589e73783 done   | 
Copying blob 3d3e89965547 done   | 
Copying config 691aea61c5 done   | 
Writing manifest to image destination
691aea61c5aab0beca3e65c93e8cd94ce2c1beaf1ad83ffb545fbfc40cd34566

The podman pull command fetches the python image from a registry and saves it locally to our system. Use the command podman images to see a list of all images on your system.

podman images

Output

REPOSITORY                 TAG      IMAGE ID      CREATED       SIZE
docker.io/library/python   latest   691aea61c5aa  3 weeks ago   1.14 GB

Let's now run a container based on the python image. To do that we use the command podman run:

podman run python

Nothing really seemed to happen this time. This is not a bug. Many things were going on behind the scenes: When you call the command podman run, the podman client finds the image, loads up the container and runs a command inside the container. We ran podman run python without providing a command, and thus it directly exited again without producing output. Let's try again by providing a command to podman run.

podman run python python3 --version

Output

Python 3.14.7

We now ran the command python3 --version in the container and, as expected, the python version number was printed out to the terminal. Again, once the command finished the container exits. The general pattern for running commands in a container is podman run [options] image-name [command] [arguments].

Hopefully, you noticed that all of this happened pretty quickly. Imagine you needed to boot up a virtual machine, run the command and destroy the VM. That's the speed difference mentioned in the introduction. The podman ps command shows you all containers that are currently running.

podman ps

Output

CONTAINER ID      IMAGE      COMMAND      CREATED        STATUS         PORTS        NAMES

As expected, since no containers are running, we see a blank line. Use podman ps -a to get a more complete output:

podman ps -a

Output

CONTAINER ID  IMAGE                             COMMAND             CREATED         STATUS                     PORTS       NAMES
f948098b7b78  docker.io/library/python:latest   python3             2 minutes ago   Exited (0) 2 minutes ago               tender_satoshi
cfeaa5d91a64  docker.io/library/python:latest   python3 --version   2 seconds ago   Exited (0) 3 seconds ago               youthful_williamson

You might be wondering now if there is a way to run more than one command in a container. Invoking the run command with the -it flag will give you an interactive tty session in the container. Then you can run as many commands in the container as you want to, like in a normal bash or Python interpreter.

podman run -it python python3

Output

Python 3.14.7 (main, Aug 25 2026, 03:38:25) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
podman run -it python bash

Output

root@0e58f3ca7c2c:/#

Interestingly, we also get an interactive Python interpreter by only running the command podman run -it python. Keep that in mind, we will see later why this works in that case.

So far, we have had no access to files from the local filesystem inside the container. But you probably want to have exactly that. With Podman, you can easily mount folders inside a container. Supply the -v option when running the podman run command and as an argument pass the absolute path on the local filesystem on the left side and on the right side the absolute path inside the container. Split both paths with a :. You can use -v multiple times in one podman run command. The command below mounts the directory test from the users home-directory to the path /opt/test inside the container.

podman run -v $HOME/test:/opt/test --rm -it alpine:latest sh

Output

Resolved "alpine" as an alias (/etc/containers/registries.conf.d/shortnames.conf)
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob e2de96513ba9 done   | 
Copying config 320994c3b9 done   | 
Writing manifest to image destination
/ #
/ # cd /opt/test
/ # touch foo
/ # exit
$ cd $HOME/test
$ ls

Output

foo

Package Permissions within the Container

Because of permissions and user IDs, it might happen, that you cannot access the files, that were written by the application within Podman. Podman uses the root-user if not specified differently somewhere else, which means, for example, that files, directories, and packages that were written or installed within the container would have user and group root as the default. This may result in an access denied error message if you try to access those artifacts with a non-root user. To avoid that, you have to pass you local user ID to Podman:

-u $(id -u ${USER}):$(id -g ${USER})
podman run --rm -it \
       -u $(id -u ${USER}):$(id -g ${USER}) \
       -v $HOME/test:/opt/test \
       alpine:latest \
       sh

What about the --rm option though? You might have already noticed that we can still see leftover containers from previous runs that even exited by running podman ps -a. Throughout this lesson you will run podman run multiple times. This will leave containers which in turn will occupy disk space. It is necessary to regularly delete old containers from the disk. To do that you can run the podman container rm command. Copy the container IDs from the output of podman ps -a and paste them alongside the command.

podman container rm bcca3121881c 0c60bdbb6a82 36a2fc0eed60 543117f36209

Output

36a2fc0eed60
543117f36209
0c60bdbb6a82
bcca3121881c

This can be a repetitive task. You can delete a bunch of containers in one go as shown in the following command:

podman container prune

This command deletes all containers that are not running.

One last useful thing: Combine the podman run command with the option --rm as was shown before. This will automatically delete the container once it exits from it. But be careful, this is only useful, if you want to run the container only once. It is definitely gone afterwards and will have to be re-created if you wish to run it again.

Volumes

We have already seen how to mount folders from the host to the container (these are called bind mounts). Volumes are the only way to have persistent data within a container and the reason why so many containers (like databases) use them. Because often it is not needed to access the files from the host system, we can let Podman manage this persistent storage.

podman volume ls

With this command we can list all existing volumes.

podman volume create test_volume

Here we created a volume called test_volume.

podman volume ls

Output

DRIVER    VOLUME NAME
local     test_volume

Volumes are particularly useful because they are OS independent and fully managed by Podman. Some additional advantages of volumes:

  • Volumes are easier to back up or migrate than bind mounts.
  • Volumes can be more safely shared among multiple containers.

In the end, volumes are names for folders on the host (like / or ~). We can see their location on the host, if we inspect a volume.

podman volume inspect test_volume

Output

[
    {
        "Name": "test_volume",
        "Driver": "local",
        "Mountpoint": "/home/XXX/.local/share/containers/storage/volumes/test_volume/_data",
        "CreatedAt": "2026-09-18T10:38:17.115256528+02:00",
        "Labels": {},
        "Scope": "local",
        "Options": {},
        "MountCount": 0,
        "NeedsCopyUp": true,
        "NeedsChown": true,
        "LockNumber": 0
    }
]

We can mount a volume like a folder from a host system by using its name on the left side of the : in -v argument.

podman run -it \
       -v test_volume:/test \
       alpine:latest \
       touch /test/foo

After a volume is unmounted, we can use the following command to remove it.

podman volume rm test_volume

Note: All containers associated with a volume need to be removed beforehand.

Port Mapping

Network ports are used to keep different communication streams separate on a single computer. For instance, multiple web applications can coexist by communicating with the outside world via different ports.

By default, containers are isolated and their ports are not mapped to the host's network. To allow a containerized application to be accessible from the outside, you must map the container's internal ports to the host's network.

This process is known as port mapping and is configured using the -p flag with the following syntax:

<host ip address>:<host port>:<container port>

Please note that the parts <host ip address> and <host port> are optional, in particular, by omitting the host ip address the port binds on all host network interfaces, making the service in the container reachable from other machines on the network (and potentially from the internet).

Examples

Map container port 80 to port 8080 on the host's localhost (127.0.0.1):

-p 127.0.0.1:8080:80

Map container port 80 to port 8080 on all network interfaces:

-p 8080:80

Map container port 80 to an arbitrary host port on all network interfaces (use podman port <container> or podman ps (column PORTS) to see which host port was assigned):

-p 80

This will be exemplified in the next exercise.

Task: Run your own image

Task Description

Your goal in this exercise is to run a Jupyter notebook inside a container. You need to be able to open the Jupyter notebook in the browser of the host system and run Python commands inside. Delete the created container afterwards and make sure, that there are no leftover containers.

  1. Find the right image called docker.io/jupyterhub/singleuser (~300 MB) from Dockerhub and pull it.
  2. Create a folder called jupyter on your local system.
  3. Run a Jupyter container based on the image you just pulled. Bind the port 8888 to localhost. Mount the folder created locally into the container. Note the hints below.
  4. Open the Jupyter notebook in the browser of your host system by opening the link that appears in the terminal: http://127.0.0.1:8888/lab?token=...
  5. Run a simple calculation.
  6. Stop the container and delete the container image.

Hints: Use the option -p 127.0.0.1:8888:8888 to bind the Jupyter notebook port to your host system, while the option -v $(pwd):/home/jovyan mounts the current folder into the container. Please also read the section about user-related configurations.

Solution

We use the image jupyterhub/singleuser from Dockerhub. Start the container, using the podman run command.

podman run --name jupyter \
    -p 127.0.0.1:8888:8888 \
    -v $(pwd):/home/jovyan \
    docker.io/jupyterhub/singleuser:latest
Output
Trying to pull docker.io/jupyterhub/singleuser:latest...
Getting image source signatures
Copying blob 3fd12b2ef46e done   | 
Copying blob 27e93c2ac0bb done   | 
Copying blob c3c3c766d9f2 done   | 
Copying blob 73847a9ca5f4 done   | 
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob b36045e39013 done   | 
Copying blob 9348e61d8af6 done   | 
Copying blob da86a5e67510 done   | 
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 4fdc7d25df22 done   | 
Copying blob d6a6bd03b6fc done   | 
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 52fadb4dd14b done   | 
Copying blob 11b06a3a6d85 done   | 
Copying blob 9dbadd403517 done   | 
Copying blob 9b95725fdb06 done   | 
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 339181aa16d6 done   | 
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 9ffaecabb18d done   | 
Copying blob 7adc0d39c83a done   | 
Copying blob 088c90ab7dc8 done   | 
Copying blob f8655b2a71bf done   | 
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 3c722b0a39aa done   | 
Copying config e3c6fa6cbe done   | 
Writing manifest to image destination
INFO[2026-09-18 09:00:14] Entered start.sh with args: start-notebook.py
INFO[2026-09-18 09:00:14] Running hooks in: /usr/local/bin/start-notebook.d as uid: 1000 gid: 100
INFO[2026-09-18 09:00:14] Done running hooks in: /usr/local/bin/start-notebook.d
INFO[2026-09-18 09:00:14] Running hooks in: /usr/local/bin/before-notebook.d as uid: 1000 gid: 100
INFO[2026-09-18 09:00:14] Sourcing shell script: /usr/local/bin/before-notebook.d/10activate-conda-env.sh
INFO[2026-09-18 09:00:15] Done running hooks in: /usr/local/bin/before-notebook.d
INFO[2026-09-18 09:00:15] Executing the command: start-notebook.py
Executing: jupyter lab
[W 2026-09-18 09:00:16.154 ServerApp] A `_jupyter_server_extension_points` function was not found in nbclassic. Instead, a `_jupyter_server_extension_paths` function was found and will be used for now. This function name will be deprecated in future releases of Jupyter Server.
[I 2026-09-18 09:00:16.155 ServerApp] jupyter_lsp | extension was successfully linked.
[I 2026-09-18 09:00:16.157 ServerApp] jupyter_server_terminals | extension was successfully linked.
[I 2026-09-18 09:00:16.159 ServerApp] jupyterlab | extension was successfully linked.
[I 2026-09-18 09:00:16.160 ServerApp] nbclassic | extension was successfully linked.
[I 2026-09-18 09:00:16.162 ServerApp] notebook | extension was successfully linked.
[I 2026-09-18 09:00:16.183 ServerApp] notebook_shim | extension was successfully linked.
[W 2026-09-18 09:00:16.192 ServerApp] WARNING: The Jupyter server is listening on all IP addresses and not using encryption. This is not recommended.
[I 2026-09-18 09:00:16.192 ServerApp] notebook_shim | extension was successfully loaded.
[I 2026-09-18 09:00:16.193 ServerApp] jupyter_lsp | extension was successfully loaded.
[I 2026-09-18 09:00:16.194 ServerApp] jupyter_server_terminals | extension was successfully loaded.
[I 2026-09-18 09:00:16.195 LabApp] JupyterLab extension loaded from /opt/conda/lib/python3.13/site-packages/jupyterlab
[I 2026-09-18 09:00:16.195 LabApp] JupyterLab application directory is /opt/conda/share/jupyter/lab
[I 2026-09-18 09:00:16.195 LabApp] Extension Manager is 'pypi'.
[I 2026-09-18 09:00:16.222 ServerApp] jupyterlab | extension was successfully loaded.
[I 2026-09-18 09:00:16.224 ServerApp] nbclassic | extension was successfully loaded.
[I 2026-09-18 09:00:16.226 ServerApp] notebook | extension was successfully loaded.
[I 2026-09-18 09:00:16.226 ServerApp] Serving notebooks from local directory: /home/jovyan
[I 2026-09-18 09:00:16.226 ServerApp] Jupyter Server 2.21.0 is running at:
[I 2026-09-18 09:00:16.226 ServerApp] http://localhost:8888/lab?token=119ba939d7986065c355b107a662f5ff75e96ebcf55035ae
[I 2026-09-18 09:00:16.226 ServerApp]     http://127.0.0.1:8888/lab?token=119ba939d7986065c355b107a662f5ff75e96ebcf55035ae
[I 2026-09-18 09:00:16.226 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 2026-09-18 09:00:16.228 ServerApp] 

    To access the server, open this file in a browser:
        file:///home/jovyan/.local/share/jupyter/runtime/jpserver-2-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/lab?token=119ba939d7986065c355b107a662f5ff75e96ebcf55035ae
        http://127.0.0.1:8888/lab?token=119ba939d7986065c355b107a662f5ff75e96ebcf55035ae
[I 2026-09-18 09:00:16.239 ServerApp] Skipped non-installed server(s): basedpyright, bash-language-server, dockerfile-language-server-nodejs, javascript-typescript-langserver, jedi-language-server, julia-language-server, pyrefly, pyright, python-language-server, python-lsp-server, r-languageserver, sql-language-server, texlab, typescript-language-server, unified-language-server, vscode-css-languageserver-bin, vscode-html-languageserver-bin, vscode-json-languageserver-bin, yaml-language-server
[W 2026-09-18 09:00:29.211 LabApp] Could not determine jupyterlab build status without nodejs

When your container is running you can open your browser at http://127.0.0.1:8888/lab?token=.... After you closed the session you can remove it, using podman rm [name].

podman container rm jupyter

Output

jupyter
podman ps -a

Output

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES