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.13
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.13
Resolved "python" as an alias (/etc/containers/registries.conf.d/shortnames.conf)
Trying to pull docker.io/library/python:3.13...
Getting image source signatures
Copying blob b619962ff558 skipped: already exists
Copying blob 48b8862a18fa skipped: already exists
Copying blob 2aee75fc41a4 skipped: already exists
Copying blob 3b1eb73e9939 skipped: already exists
Copying blob b1b8a0660a31 skipped: already exists
Copying blob 0c01110621e0 skipped: already exists
Copying blob c28d2e5e3fcb skipped: already exists
Copying config f7711bfba6 done |
Writing manifest to image destination
STEP 2/5: LABEL maintainer="support@hifis.net"
--> 300114353da3
STEP 3/5: RUN pip install --upgrade pip
Requirement already satisfied: pip in /usr/local/lib/python3.13/site-packages (25.1.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.
--> ba25f8ea21a3
STEP 4/5: RUN pip install ipython numpy
Collecting ipython
Downloading ipython-9.3.0-py3-none-any.whl.metadata (4.4 kB)
Collecting numpy
Downloading numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl.metadata (62 kB)
Collecting decorator (from ipython)
Downloading decorator-5.2.1-py3-none-any.whl.metadata (3.9 kB)
Collecting ipython-pygments-lexers (from ipython)
Downloading ipython_pygments_lexers-1.1.1-py3-none-any.whl.metadata (1.1 kB)
Collecting jedi>=0.16 (from ipython)
Downloading jedi-0.19.2-py2.py3-none-any.whl.metadata (22 kB)
Collecting matplotlib-inline (from ipython)
Downloading matplotlib_inline-0.1.7-py3-none-any.whl.metadata (3.9 kB)
Collecting pexpect>4.3 (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.51-py3-none-any.whl.metadata (6.4 kB)
Collecting pygments>=2.4.0 (from ipython)
Downloading pygments-2.19.1-py3-none-any.whl.metadata (2.5 kB)
Collecting stack_data (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.14.3-py3-none-any.whl.metadata (10 kB)
Collecting wcwidth (from prompt_toolkit<3.1.0,>=3.0.41->ipython)
Downloading wcwidth-0.2.13-py2.py3-none-any.whl.metadata (14 kB)
Collecting parso<0.9.0,>=0.8.4 (from jedi>=0.16->ipython)
Downloading parso-0.8.4-py2.py3-none-any.whl.metadata (7.7 kB)
Collecting ptyprocess>=0.5 (from pexpect>4.3->ipython)
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl.metadata (1.3 kB)
Collecting executing>=1.2.0 (from stack_data->ipython)
Downloading executing-2.2.0-py2.py3-none-any.whl.metadata (8.9 kB)
Collecting asttokens>=2.1.0 (from stack_data->ipython)
Downloading asttokens-3.0.0-py3-none-any.whl.metadata (4.7 kB)
Collecting pure-eval (from stack_data->ipython)
Downloading pure_eval-0.2.3-py3-none-any.whl.metadata (6.3 kB)
Downloading ipython-9.3.0-py3-none-any.whl (605 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 605.3/605.3 kB 22.3 MB/s eta 0:00:00
Downloading prompt_toolkit-3.0.51-py3-none-any.whl (387 kB)
Downloading numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl (16.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.6/16.6 MB 95.1 MB/s eta 0:00:00
Downloading jedi-0.19.2-py2.py3-none-any.whl (1.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 89.6 MB/s eta 0:00:00
Downloading parso-0.8.4-py2.py3-none-any.whl (103 kB)
Downloading pexpect-4.9.0-py2.py3-none-any.whl (63 kB)
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 kB)
Downloading pygments-2.19.1-py3-none-any.whl (1.2 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 103.5 MB/s eta 0:00:00
Downloading traitlets-5.14.3-py3-none-any.whl (85 kB)
Downloading decorator-5.2.1-py3-none-any.whl (9.2 kB)
Downloading ipython_pygments_lexers-1.1.1-py3-none-any.whl (8.1 kB)
Downloading matplotlib_inline-0.1.7-py3-none-any.whl (9.9 kB)
Downloading stack_data-0.6.3-py3-none-any.whl (24 kB)
Downloading asttokens-3.0.0-py3-none-any.whl (26 kB)
Downloading executing-2.2.0-py2.py3-none-any.whl (26 kB)
Downloading pure_eval-0.2.3-py3-none-any.whl (11 kB)
Downloading wcwidth-0.2.13-py2.py3-none-any.whl (34 kB)
Installing collected packages: wcwidth, pure-eval, ptyprocess, traitlets, pygments, prompt_toolkit, pexpect, parso, numpy, executing, decorator, asttokens, stack_data, matplotlib-inline, jedi, ipython-pygments-lexers, ipython
Successfully installed asttokens-3.0.0 decorator-5.2.1 executing-2.2.0 ipython-9.3.0 ipython-pygments-lexers-1.1.1 jedi-0.19.2 matplotlib-inline-0.1.7 numpy-2.3.0 parso-0.8.4 pexpect-4.9.0 prompt_toolkit-3.0.51 ptyprocess-0.7.0 pure-eval-0.2.3 pygments-2.19.1 stack_data-0.6.3 traitlets-5.14.3 wcwidth-0.2.13
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.
--> 03957dccac9c
STEP 5/5: ENTRYPOINT ["ipython"]
COMMIT my-ipython-image
--> 77378e65d4e3
Successfully tagged localhost/my-ipython-image:latest
77378e65d4e3752adc5c675afd270628120fbf4cb76b64a846cfaffa73bfd9f5
Let's try out the newly created image by running it.
podman run --rm -it my-ipython-image
Output
Python 3.13.4 (main, Jun 11 2025, 02:31:21) [GCC 12.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 9.3.0 -- An enhanced Interactive Python. Type '?' for help.
Tip: Use `ipython --help-all | less` to view all the IPython configuration options.
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
Dockerfile
must start with aFROM
instruction. - 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 6 of this course.
LABEL <key>=<value> <key>=<value> <key>=<value> ...
- The
LABEL
instruction adds metadata to the image. - A
LABEL
is a key-value pair. - This is typically used to provide information about e.g. the maintainer of an image.
RUN <command>
- The
RUN
instruction 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
ENTRYPOINT
allows 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.3.0
Let's build the image again and see what happens.
podman build -t my-ipython-image .
Output
STEP 1/5: FROM python:3.13
STEP 2/5: LABEL maintainer="support@hifis.net"
--> Using cache 300114353da39d0e338789b27654fa00eeb8d58b88290ba713b91ce79b32c0db
--> 300114353da3
STEP 3/5: RUN pip install --upgrade pip
--> Using cache ba25f8ea21a3398d89614a25ef28f6a2ea1cba73c7f4194451b6d670b765db68
--> ba25f8ea21a3
STEP 4/5: RUN pip install ipython numpy
--> Using cache 03957dccac9c6173ed0df3520d9718a3aeeaaeaf3f12b13a3e2f45f892f564f2
--> 03957dccac9c
STEP 5/5: ENTRYPOINT ["ipython"]
--> Using cache 77378e65d4e3752adc5c675afd270628120fbf4cb76b64a846cfaffa73bfd9f5
COMMIT my-ipython-image
--> 77378e65d4e3
Successfully tagged localhost/my-ipython-image:latest
77378e65d4e3752adc5c675afd270628120fbf4cb76b64a846cfaffa73bfd9f5
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.
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.13
. - Mark yourself as the maintainer of the image.
- Install
numpy
,scipy
,pandas
,scikit-learn
andjupyterlab
usingpip install
. - Create a custom user using the command
useradd -ms /bin/bash jupyter
. - Tell the image to automatically start as the
jupyter
user and to use the working directory/home/jupyter
. - Make sure the image starts with the command
jupyter lab --ip=0.0.0.0
by default.
Solution
- Create a
Dockerfile
with below content.
FROM python:3.13
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 2025-06-12 06:43:40.279 ServerApp] jupyter_lsp | extension was successfully linked.
[I 2025-06-12 06:43:40.282 ServerApp] jupyter_server_terminals | extension was successfully linked.
[I 2025-06-12 06:43:40.284 ServerApp] jupyterlab | extension was successfully linked.
[I 2025-06-12 06:43:40.284 ServerApp] Writing Jupyter server cookie secret to /home/jupyter/.local/share/jupyter/runtime/jupyter_cookie_secret
[I 2025-06-12 06:43:40.453 ServerApp] notebook_shim | extension was successfully linked.
[I 2025-06-12 06:43:40.461 ServerApp] notebook_shim | extension was successfully loaded.
[I 2025-06-12 06:43:40.462 ServerApp] jupyter_lsp | extension was successfully loaded.
[I 2025-06-12 06:43:40.463 ServerApp] jupyter_server_terminals | extension was successfully loaded.
[I 2025-06-12 06:43:40.463 LabApp] JupyterLab extension loaded from /usr/local/lib/python3.13/site-packages/jupyterlab
[I 2025-06-12 06:43:40.463 LabApp] JupyterLab application directory is /usr/local/share/jupyter/lab
[I 2025-06-12 06:43:40.464 LabApp] Extension Manager is 'pypi'.
[I 2025-06-12 06:43:40.488 ServerApp] jupyterlab | extension was successfully loaded.
[I 2025-06-12 06:43:40.488 ServerApp] Serving notebooks from local directory: /home/jupyter
[I 2025-06-12 06:43:40.488 ServerApp] Jupyter Server 2.16.0 is running at:
[I 2025-06-12 06:43:40.488 ServerApp] http://7909d0b301a5:8888/lab?token=9469bb0c7a7c55ae65a4f74e3613e39fc690ee03b556c09a
[I 2025-06-12 06:43:40.488 ServerApp] http://127.0.0.1:8888/lab?token=9469bb0c7a7c55ae65a4f74e3613e39fc690ee03b556c09a
[I 2025-06-12 06:43:40.488 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 2025-06-12 06:43:40.492 ServerApp] No web browser found: Error('could not locate runnable browser').
[C 2025-06-12 06:43:40.493 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://7909d0b301a5:8888/lab?token=9469bb0c7a7c55ae65a4f74e3613e39fc690ee03b556c09a
http://127.0.0.1:8888/lab?token=9469bb0c7a7c55ae65a4f74e3613e39fc690ee03b556c09a
[I 2025-06-12 06:43:40.500 ServerApp] Skipped non-installed server(s): bash-language-server, dockerfile-language-server-nodejs, javascript-typescript-langserver, jedi-language-server, julia-language-server, 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