Creating a Container
When starting to work with containers you will soon notice that existing images may not always satisfy your needs. In these situations you want to create your own custom image.
Images are defined by a text file called Dockerfile.
Dockerfiles contain the instructions for Docker / Podman how to create a custom image as the basis for containers.
Let's build and run our first image
We start by creating a text file called Dockerfile in the folder ~/using-containers-in-science/.
cd ~
mkdir using-containers-in-science
cd using-containers-in-science
nano Dockerfile
Now, we add the content below into the Dockerfile:
FROM python:3.14
LABEL maintainer="support@hifis.net"
RUN pip install --upgrade pip
RUN pip install ipython numpy
ENTRYPOINT ["ipython"]
After that we can save and leave the editor (In the case of nano: Ctrl+O then Ctrl+X).
Congratulations, it is that simple.
The image can be built using the podman build command as shown below.
Note that to build a custom image, you have to be in the folder containing the Dockerfile.
The latter is implicitly used as the input for the build, and you have to specify the name of the image to be built.
podman build -t my-ipython-image .
Output
STEP 1/5: FROM python:3.14
Resolved "python" as an alias (/etc/containers/registries.conf.d/shortnames.conf)
Trying to pull docker.io/library/python:3.14...
Getting image source signatures
Copying blob 7d973fba4a67 done |
Copying blob 44fed46b68cf done |
Copying blob 00f78834a2fe done |
Copying blob e57589e73783 done |
Copying blob cbc19164244e done |
Copying blob 27ee9a825048 done |
Copying blob 3d3e89965547 done |
Copying config 691aea61c5 done |
Writing manifest to image destination
STEP 2/5: LABEL maintainer="support@hifis.net"
--> a0b5af546850
STEP 3/5: RUN pip install --upgrade pip
Requirement already satisfied: pip in ./usr/local/lib/python3.14/site-packages (26.2.1)
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
--> da68df006fcc
STEP 4/5: RUN pip install ipython numpy
Collecting ipython
Downloading ipython-9.17.1-py3-none-any.whl.metadata (4.6 kB)
Collecting numpy
Downloading numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
Collecting ipython-pygments-lexers>=1.0.0 (from ipython)
Downloading ipython_pygments_lexers-1.1.1-py3-none-any.whl.metadata (1.1 kB)
Collecting jedi>=0.18.2 (from ipython)
Downloading jedi-0.20.0-py2.py3-none-any.whl.metadata (23 kB)
Collecting matplotlib-inline>=0.1.6 (from ipython)
Downloading matplotlib_inline-0.2.2-py3-none-any.whl.metadata (2.4 kB)
Collecting pexpect>4.6 (from ipython)
Downloading pexpect-4.9.0-py2.py3-none-any.whl.metadata (2.5 kB)
Collecting prompt_toolkit<3.1.0,>=3.0.41 (from ipython)
Downloading prompt_toolkit-3.0.53-py3-none-any.whl.metadata (6.4 kB)
Collecting psutil>=7 (from ipython)
Downloading psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl.metadata (22 kB)
Collecting pygments>=2.14.0 (from ipython)
Downloading pygments-2.21.0-py3-none-any.whl.metadata (2.5 kB)
Collecting stack_data>=0.6.0 (from ipython)
Downloading stack_data-0.6.3-py3-none-any.whl.metadata (18 kB)
Collecting traitlets>=5.13.0 (from ipython)
Downloading traitlets-5.16.1-py3-none-any.whl.metadata (10 kB)
Collecting wcwidth>=0.1.4 (from prompt_toolkit<3.1.0,>=3.0.41->ipython)
Downloading wcwidth-0.8.4-py3-none-any.whl.metadata (23 kB)
Collecting parso<0.9.0,>=0.8.6 (from jedi>=0.18.2->ipython)
Downloading parso-0.8.7-py2.py3-none-any.whl.metadata (8.2 kB)
Collecting ptyprocess>=0.5 (from pexpect>4.6->ipython)
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl.metadata (1.3 kB)
Collecting executing>=1.2.0 (from stack_data>=0.6.0->ipython)
Downloading executing-2.2.1-py2.py3-none-any.whl.metadata (8.9 kB)
Collecting asttokens>=2.1.0 (from stack_data>=0.6.0->ipython)
Downloading asttokens-3.0.2-py3-none-any.whl.metadata (5.7 kB)
Collecting pure-eval (from stack_data>=0.6.0->ipython)
Downloading pure_eval-0.2.4-py3-none-any.whl.metadata (6.4 kB)
Downloading ipython-9.17.1-py3-none-any.whl (639 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 639.0/639.0 kB 5.0 MB/s 0:00:00
Downloading prompt_toolkit-3.0.53-py3-none-any.whl (392 kB)
Downloading numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.7 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.7/16.7 MB 7.5 MB/s 0:00:02
Downloading ipython_pygments_lexers-1.1.1-py3-none-any.whl (8.1 kB)
Downloading jedi-0.20.0-py2.py3-none-any.whl (4.9 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.9/4.9 MB 6.2 MB/s 0:00:00
Downloading parso-0.8.7-py2.py3-none-any.whl (107 kB)
Downloading matplotlib_inline-0.2.2-py3-none-any.whl (9.5 kB)
Downloading pexpect-4.9.0-py2.py3-none-any.whl (63 kB)
Downloading psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl (155 kB)
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 kB)
Downloading pygments-2.21.0-py3-none-any.whl (1.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.3/1.3 MB 6.7 MB/s 0:00:00
Downloading stack_data-0.6.3-py3-none-any.whl (24 kB)
Downloading asttokens-3.0.2-py3-none-any.whl (28 kB)
Downloading executing-2.2.1-py2.py3-none-any.whl (28 kB)
Downloading traitlets-5.16.1-py3-none-any.whl (86 kB)
Downloading wcwidth-0.8.4-py3-none-any.whl (299 kB)
Downloading pure_eval-0.2.4-py3-none-any.whl (11 kB)
Installing collected packages: pure-eval, ptyprocess, wcwidth, traitlets, pygments, psutil, pexpect, parso, numpy, executing, asttokens, stack_data, prompt_toolkit, matplotlib-inline, jedi, ipython-pygments-lexers, ipython
Successfully installed asttokens-3.0.2 executing-2.2.1 ipython-9.17.1 ipython-pygments-lexers-1.1.1 jedi-0.20.0 matplotlib-inline-0.2.2 numpy-2.5.3 parso-0.8.7 pexpect-4.9.0 prompt_toolkit-3.0.53 psutil-7.2.2 ptyprocess-0.7.0 pure-eval-0.2.4 pygments-2.21.0 stack_data-0.6.3 traitlets-5.16.1 wcwidth-0.8.4
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
--> e20da0dde771
STEP 5/5: ENTRYPOINT ["ipython"]
COMMIT my-ipython-image
--> 875ec93d3f51
Successfully tagged localhost/my-ipython-image:latest
875ec93d3f51902f3a212081439fee6c19dffd17d0999d5430b0d3827c3ee0e9
Let's try out the newly created image by running it.
podman run --rm -it my-ipython-image
Output
Python 3.14.7 (main, Aug 25 2026, 03:38:25) [GCC 14.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 9.17.1 -- An enhanced Interactive Python. Type '?' for help.
Tip: You can use `%hist` to view history, see the options with `%history?`
In [1]:
We end up in an IPython shell allowing us to interact like in an IPython shell installed in the usual manner.
Once we exit the shell, the container also stops running.
Let's see how this works by disassembling the Dockerfile.
Disassembling the Dockerfile
The Dockerfile used above contains four different types of instructions:
FROM <image>- Sets the base image for the instructions below.
- Each valid
Dockerfilemust start with aFROMinstruction. - The image can be any valid image, e.g. from public registries.
- Please note: Choose a trusted base image for your images.
- We'll cover that topic in more detail in lesson 7 of this course.
LABEL <key>=<value> <key>=<value> <key>=<value> ...- The
LABELinstruction adds metadata to the image. - A
LABELis a key-value pair. - This is typically used to provide information about e.g. the maintainer of an image.
RUN <command>- The
RUNinstruction executes any command on top of the current image. (We will cover this in a minute.) - The resulting image will be used as the base for the next step in the
Dockerfile. ENTRYPOINT ["executable", "param1", "param2"]- An
ENTRYPOINTallows you to configure a container that runs as an executable. - Command line arguments to
podman run <image>will be appended after all elements in the exec formENTRYPOINT.
Example
podman run --rm -it my-ipython-image --version
Will give us the version number of IPython.
This is equivalent to executing ipython --version, locally.
9.17.1
Let's build the image again and see what happens.
podman build -t my-ipython-image .
Output
STEP 1/5: FROM python:3.14
STEP 2/5: LABEL maintainer="support@hifis.net"
--> Using cache a0b5af546850eacca2ab1983941cb9252171f6f54cb9d8abb613cdd7e3922595
--> a0b5af546850
STEP 3/5: RUN pip install --upgrade pip
--> Using cache da68df006fccb3b63078ddc09fa0bb6b5f731c9375654043702029c776da0c6f
--> da68df006fcc
STEP 4/5: RUN pip install ipython numpy
--> Using cache e20da0dde771ac3522fc1cd1ecc336fb1f23f3cc5769419599c21d3506760817
--> e20da0dde771
STEP 5/5: ENTRYPOINT ["ipython"]
--> Using cache 875ec93d3f51902f3a212081439fee6c19dffd17d0999d5430b0d3827c3ee0e9
COMMIT my-ipython-image
--> 875ec93d3f51
Successfully tagged localhost/my-ipython-image:latest
875ec93d3f51902f3a212081439fee6c19dffd17d0999d5430b0d3827c3ee0e9
This time, the output is much shorter than in our initial run of the podman build command.
In each of the steps it is claimed to have used the cache.
As each instruction is executed, Podman looks for an existing image in its cache that has already been created in the same manner.
If there is such an image, Podman will re-use that image instead of creating a duplicate.
If you do not want Podman to use its cache, provide the --no-cache=true option to the podman build command.
Container Layers
As we have seen before, e.g. by pulling a container image, a container image is made of layers
and a container build adds additional layers on top of the base container image.
Basically, each step in the Dockerfile adds another layer to the container image.
To understand the layering in containers better, let's look more closely at the types
of layers and their purpose.
There are two types of layers:
- Image Layers (read-only)
- Container Layer (writable)
Image Layers
Image layers are static snapshots representing filesystem changes from build steps. There are base layers and intermediate layers in image layers. Unless the image is a base image beginning from scratch, the base layers are the foundation and provide the layers of the base image used such as operating systems like Ubuntu, Debian, etc. Intermediate layers are incremental changes piled on top. These layers are immutable and can be shared between images.
Container Layer
When a container starts, a thin, writable layer is added on top of the image layers. All changes made inside the running container, such as new, modified or deleted files, are stored in this layer. The image itself stays unchanged. When the container is removed, its writable layer is also removed. Data that needs to be persisted should therefore be written to a volume, which is stored outside the container layer.
Benefits
- Identical base layers are stored once and shared, which saves storage.
- Unchanged steps are processed by the layer cache, which skips re-work.
- Only the thin writable layer needs to be created at start-up, which leads to quicker start-ups.
Task: Create and Run a Data Science Image
Task Description
Your goal in this exercise is to create your own custom data science image as follows:
- Build your image on top of the latest Python image of release series
3.14. - Mark yourself as the maintainer of the image.
- Install
numpy,scipy,pandas,scikit-learnandjupyterlabusingpip install. - Create a custom user using the command
useradd -ms /bin/bash jupyter. - Tell the image to automatically start as the
jupyteruser and to use the working directory/home/jupyter. - Make sure the image starts with the command
jupyter lab --ip=0.0.0.0by default.
Solution
- Create a
Dockerfilewith below content.
FROM python:3.14
RUN pip install ipython jupyterlab numpy pandas scikit-learn
# Create a custom user under which the application runs
RUN useradd -ms /bin/bash jupyter
# Use this user by default for all subsequent operations
USER jupyter
# Default to start the container in the home directory of the jupyter user
WORKDIR /home/jupyter
# Publish port 8888 to the outside, for documentation purpose
EXPOSE 8888
ENTRYPOINT ["jupyter", "lab", "--ip=0.0.0.0"]
- Build the image.
podman build -t my-datascience-image .
- Run the image and bind port 8888.
podman run -p 8888:8888 -it --rm my-datascience-image
This yields an output as shown below. (Details may vary)
Output
[I 2026-09-18 13:00:58.519 ServerApp] jupyter_lsp | extension was successfully linked.
[I 2026-09-18 13:00:58.521 ServerApp] jupyter_server_terminals | extension was successfully linked.
[I 2026-09-18 13:00:58.523 ServerApp] jupyterlab | extension was successfully linked.
[I 2026-09-18 13:00:58.523 ServerApp] Writing Jupyter server cookie secret to /home/jupyter/.local/share/jupyter/runtime/jupyter_cookie_secret
[I 2026-09-18 13:00:58.546 ServerApp] notebook_shim | extension was successfully linked.
[I 2026-09-18 13:00:58.556 ServerApp] notebook_shim | extension was successfully loaded.
[I 2026-09-18 13:00:58.557 ServerApp] jupyter_lsp | extension was successfully loaded.
[I 2026-09-18 13:00:58.558 ServerApp] jupyter_server_terminals | extension was successfully loaded.
[I 2026-09-18 13:00:58.558 LabApp] JupyterLab extension loaded from /usr/local/lib/python3.14/site-packages/jupyterlab
[I 2026-09-18 13:00:58.558 LabApp] JupyterLab application directory is /usr/local/share/jupyter/lab
[I 2026-09-18 13:00:58.559 LabApp] Extension Manager is 'pypi'.
[I 2026-09-18 13:00:58.574 ServerApp] jupyterlab | extension was successfully loaded.
[I 2026-09-18 13:00:58.574 ServerApp] Serving notebooks from local directory: /home/jupyter
[I 2026-09-18 13:00:58.574 ServerApp] Jupyter Server 2.21.1 is running at:
[I 2026-09-18 13:00:58.574 ServerApp] http://0.0.0.0:8888/lab?token=5ae8ef366ff74158b7a84b57d4649d7df4f48e877df299ce
[I 2026-09-18 13:00:58.575 ServerApp] http://127.0.0.1:8888/lab?token=5ae8ef366ff74158b7a84b57d4649d7df4f48e877df299ce
[I 2026-09-18 13:00:58.575 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 2026-09-18 13:00:58.580 ServerApp] No web browser found: Error('could not locate runnable browser').
[C 2026-09-18 13:00:58.580 ServerApp]
To access the server, open this file in a browser:
file:/home/jupyter/.local/share/jupyter/runtime/jpserver-1-open.html
Or copy and paste one of these URLs:
http://3f15985fc66a:8888/lab?token=5ae8ef366ff74158b7a84b57d4649d7df4f48e877df299ce
http://127.0.0.1:8888/lab?token=5ae8ef366ff74158b7a84b57d4649d7df4f48e877df299ce
The server is listening on all interfaces, so any hostname or IP of this machine will work.
[I 2026-09-18 13:00:58.591 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