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

  1. Image Layers (read-only)
  2. 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:

  1. Build your image on top of the latest Python image of release series 3.14.
  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.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