Skip to content

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 854e2aed8deb done   | 
Copying blob 155ad54a8b28 done   | 
Copying blob 1d281e50d3e4 done   | 
Copying blob 447713e77b4f done   | 
Copying blob 8031108f3cda done   | 
Copying blob 21754c21aa78 done   | 
Copying blob 5e9ad5aa09b4 done   | 
Copying config 36d17f72f4 done   | 
Writing manifest to image destination
STEP 2/5: LABEL maintainer="support@hifis.net"
--> bd00d88e93de
STEP 3/5: RUN pip install --upgrade pip
Requirement already satisfied: pip in /usr/local/lib/python3.13/site-packages (24.3.1)
Collecting pip
  Downloading pip-25.0.1-py3-none-any.whl.metadata (3.7 kB)
Downloading pip-25.0.1-py3-none-any.whl (1.8 MB)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 34.2 MB/s eta 0:00:00
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 24.3.1
    Uninstalling pip-24.3.1:
      Successfully uninstalled pip-24.3.1
Successfully installed pip-25.0.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.
--> 7305cd261b54
STEP 4/5: RUN pip install ipython numpy
Collecting ipython
  Downloading ipython-9.0.2-py3-none-any.whl.metadata (4.3 kB)
Collecting numpy
  Downloading numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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.50-py3-none-any.whl.metadata (6.6 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 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 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 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.0.2-py3-none-any.whl (600 kB)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 600.5/600.5 kB 19.4 MB/s eta 0:00:00
Downloading numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.1 MB)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.1/16.1 MB 73.2 MB/s eta 0:00:00
Downloading jedi-0.19.2-py2.py3-none-any.whl (1.6 MB)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 108.6 MB/s eta 0:00:00
Downloading pexpect-4.9.0-py2.py3-none-any.whl (63 kB)
Downloading prompt_toolkit-3.0.50-py3-none-any.whl (387 kB)
Downloading pygments-2.19.1-py3-none-any.whl (1.2 MB)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 97.4 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 parso-0.8.4-py2.py3-none-any.whl (103 kB)
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 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.0.2 ipython-pygments-lexers-1.1.1 jedi-0.19.2 matplotlib-inline-0.1.7 numpy-2.2.3 parso-0.8.4 pexpect-4.9.0 prompt_toolkit-3.0.50 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.
--> 458fc0ae1680
STEP 5/5: ENTRYPOINT ["ipython"]
COMMIT my-ipython-image
--> c444f1041486
Successfully tagged localhost/my-ipython-image:latest
c444f1041486eb1b458b4b0dbc7b84b94e45fa9191f1d0e6b168b508be1eeb03

Let's try out the newly created image by running it.

podman run --rm -it my-ipython-image

Output

Python 3.13.2 (main, Feb 25 2025, 05:25:21) [GCC 12.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 9.0.2 -- An enhanced Interactive Python. Type '?' for help.
Tip: You can find how to type a unicode symbol by back completing it `\Ⅷ<tab>` will expand to `\ROMAN NUMERAL EIGHT`.

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 a FROM 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 form ENTRYPOINT.

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.0.2

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 bd00d88e93de4242f005998474c2304c900a5f6fc04c299988b150ee449f2a53
--> bd00d88e93de
STEP 3/5: RUN pip install --upgrade pip
--> Using cache 7305cd261b5408536a4e004384179f8450a6bc6ae10890f93783f00c5ed8cbf0
--> 7305cd261b54
STEP 4/5: RUN pip install ipython numpy
--> Using cache 458fc0ae16809c5749109f19472304a8c807f009a2020491547f3a7a30e55fdc
--> 458fc0ae1680
STEP 5/5: ENTRYPOINT ["ipython"]
--> Using cache c444f1041486eb1b458b4b0dbc7b84b94e45fa9191f1d0e6b168b508be1eeb03
COMMIT my-ipython-image
--> c444f1041486
Successfully tagged localhost/my-ipython-image:latest
c444f1041486eb1b458b4b0dbc7b84b94e45fa9191f1d0e6b168b508be1eeb03

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:

  1. Build your image on top of the latest Python image of release series 3.13.
  2. Mark yourself as the maintainer of the image.
  3. Install numpy, scipy, pandas, scikit-learn and jupyterlab using pip install.
  4. Create a custom user using the command useradd -ms /bin/bash jupyter.
  5. Tell the image to automatically start as the jupyter user and to use the working directory /home/jupyter.
  6. Make sure the image starts with the command jupyter lab --ip=0.0.0.0 by default.

Hint: Use the instructions USER and WORKDIR for task 5.

When having built the image, make sure to test it by running it and opening jupyter in your browser. You should be able to execute any command now, e.g.

import numpy as np
np.__config__.show()
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-03-10 14:56:05.867 ServerApp] jupyter_lsp | extension was successfully linked.
[I 2025-03-10 14:56:05.869 ServerApp] jupyter_server_terminals | extension was successfully linked.
[I 2025-03-10 14:56:05.871 ServerApp] jupyterlab | extension was successfully linked.
[I 2025-03-10 14:56:05.872 ServerApp] Writing Jupyter server cookie secret to /home/jupyter/.local/share/jupyter/runtime/jupyter_cookie_secret
[I 2025-03-10 14:56:06.033 ServerApp] notebook_shim | extension was successfully linked.
[I 2025-03-10 14:56:06.043 ServerApp] notebook_shim | extension was successfully loaded.
[I 2025-03-10 14:56:06.045 ServerApp] jupyter_lsp | extension was successfully loaded.
[I 2025-03-10 14:56:06.045 ServerApp] jupyter_server_terminals | extension was successfully loaded.
[I 2025-03-10 14:56:06.046 LabApp] JupyterLab extension loaded from /usr/local/lib/python3.13/site-packages/jupyterlab
[I 2025-03-10 14:56:06.046 LabApp] JupyterLab application directory is /usr/local/share/jupyter/lab
[I 2025-03-10 14:56:06.046 LabApp] Extension Manager is 'pypi'.
[I 2025-03-10 14:56:06.071 ServerApp] jupyterlab | extension was successfully loaded.
[I 2025-03-10 14:56:06.072 ServerApp] Serving notebooks from local directory: /home/jupyter
[I 2025-03-10 14:56:06.072 ServerApp] Jupyter Server 2.15.0 is running at:
[I 2025-03-10 14:56:06.072 ServerApp] http://6226a8c33d72:8888/lab?token=582cebbd9586aecceeba6ddd9791c6247fcbd22051fcd228
[I 2025-03-10 14:56:06.072 ServerApp]     http://127.0.0.1:8888/lab?token=582cebbd9586aecceeba6ddd9791c6247fcbd22051fcd228
[I 2025-03-10 14:56:06.072 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 2025-03-10 14:56:06.076 ServerApp] No web browser found: Error('could not locate runnable browser').
[C 2025-03-10 14:56:06.076 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://6226a8c33d72:8888/lab?token=582cebbd9586aecceeba6ddd9791c6247fcbd22051fcd228
        http://127.0.0.1:8888/lab?token=582cebbd9586aecceeba6ddd9791c6247fcbd22051fcd228
[I 2025-03-10 14:56:06.087 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