A Containerized Installation of openEMS Using Apptainer

Setting up openEMS in a containerized environment using apptainer has some nice benefits. Dependencies are easily fulfilled and coupling to the host system is minimized. Adding the possibility to start a Jupyter Python kernel in the container from the host makes integration into the host system almost seamless.

Misc
Published

August 4, 2025

Creating a Containerized openEMS Installation

Apptainer is an open-source container platform for HPC and scientific workflows, formerly called Singularity. It runs containers in user space without root privileges, making it ideal for multi-user clusters and secure cloud environments. Its single-file .sif format simplifies sharing and ensures reproducibility, while daemonless execution avoids privilege risks.

openEMS is an open-source FDTD solver for 3D electromagnetic simulations such as antennas, waveguides, and scattering problems. It uses a Yee-cell discretization with PML boundaries and can be controlled via Python scripts or Jupyter notebooks. Together, these tools enable portable, secure, and scriptable scientific computing workflows.

Following and slightly adjusting the build instructions given by https://docs.openems.de/install.html#linux we will setup an Apptainer .def file that will allow us to automatically build the container. We will use Ubuntu LTS 22.04, so we have to tweak the libvtk. The python3-ipykernel package will later allow us to run a Jupyter kernel within the container.

openems.def
# openEMS
# Michael Wichmann, 2025

Bootstrap: docker
From: ubuntu:22.04

%post
    # For debugging: Keep the donwloaded *.deb packages. Bind apt-cache
    # externally. Use something like:
    #   $ apptainer build --bind ~/apt-cache:/var/cache/apt/archives openems.sif openems.def
    # Uncomment the following two lines in order to ensure that
    # -APT keeps downloaded packages and
    # -apt-cache directory is not removed by docker-clean script.
    echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
    rm -f /etc/apt/apt.conf.d/docker-clean

    # Dependencies
    export DEBIAN_FRONTEND=noninteractive
    echo "tzdata tzdata/Areas select Etc" | debconf-set-selections
    echo "tzdata tzdata/Zones/Etc select UTC" | debconf-set-selections
    apt-get -y update && \
    apt-get -y install --no-install-recommends \
        build-essential cmake git libhdf5-dev libvtk9-dev libboost-all-dev libcgal-dev libtinyxml-dev \
        qtbase5-dev libvtk9-qt-dev \
        octave liboctave-dev \
        python3-numpy python3-matplotlib cython3 python3-h5py \
        paraview \
        gengetopt help2man groff pod2pdf bison flex libhpdf-dev libtool \
        python3-pip \
        python3-ipykernel \
    && rm -rf /var/lib/apt/lists/*

    # Create symlink python pointing to python3
    ln -sf /usr/bin/python3 /usr/bin/python

    # Clone into openEMS repository and build
    git clone --recursive https://github.com/thliebig/openEMS-Project.git
    cd openEMS-Project
    ./update_openEMS.sh /opt/openEMS --python --with-CTB --with-hyp2mat
    # --with-MPI --> build error

After running the command

apptainer build openems.sif openems.def

we are left with a 1.1 GB .sif file that encapsulates a working installation of openEMS.

In case we need to do a lot of rebuilding and tweaking it can get annoying to download all the required .deb packages time and time again. Depending on your bandwidth you might be inclined to keep a local apt package cache and bind that to your container. Make sure to uncomment the two lines at the beginning of the %post section, otherwise your cache is wiped during the container build.

apptainer build --bind ~/apt-cache:/var/cache/apt/archives openems.sif openems.def

After completion we can log into our container with

apptainer shell openems.sif

For useful command line options like --writable-tmpfs and --fakeroot consult the Apptainer documentation.

Registering a Jupyter Kernel

Running the following command will bring us close to our goal of being able to start a Jupyter kernel within our container.

apptainer exec openems.sif python3 -m ipykernel install \
  --user --name openems_kernel \
  --display-name "openEMS (Apptainer)"

But we are not quite there yet. Inspecting the generated kernelspec at .local/share/jupyter/kernels/openems_kernel yields the following:

kernel.json
 {
  "argv": [
   "/usr/bin/python3",
   "-m",
   "ipykernel_launcher",
   "-f",
   "{connection_file}"
  ],
  "display_name": "openEMS (Apptainer)",
  "language": "python",
  "metadata": {
   "debugger": false
  }
 }

In order to actually invoke the container we need to add one line at the beginning of the argv list with the full path to our .sif file and the apptainer exec command. Also successfully connecting to the containerized jupyter kernel might require shared access to the /run/user/{uid} directory. So finally we arrive at the following kernelspec file:

kernel.json
 {
  "argv": [
   "/usr/bin/apptainer", "exec",
   "--bind", "/run/user/1000:/run/user/1000",
   "/home/michael/opus/sandbox/openems/openems.sif",
   "/usr/bin/python3",
   "-m",
   "ipykernel_launcher",
   "-f",
   "{connection_file}"
  ],
  "display_name": "Python / openEMS (Apptainer)",
  "language": "python",
  "metadata": {
   "debugger": false
  }
 }

The following screenshot shows the final result: a Jupyter notebook running on the host, connected to our ‘openEMS’ kernel in the container running the MSL notch filter tutorial.

openEMS kernel running in apptainer