diff --git a/scripts/ci/extends/template-build-linux.yml b/scripts/ci/extends/template-build-linux.yml
index 83e9964aaa4d4e3e2f50be92cf65937807dd5dc4..af4dbed25c515ec0ae1520fb74f205d0a7bfa8f1 100644
--- a/scripts/ci/extends/template-build-linux.yml
+++ b/scripts/ci/extends/template-build-linux.yml
@@ -38,11 +38,16 @@
             preset_postfix="-large"
           fi
 
-          if [[ "$OSTYPE" == "darwin"* ]]; then alias date=gdate ; fi
+          xvfb_run_cmd=""
+          if [[ "$OSTYPE" == "darwin"* ]]; then
+            alias date=gdate
+          else
+            xvfb_run_cmd="xvfb-run -a"
+          fi
 
           # -M Continuous does not run any tests -> -M Experimental --group Continuous
           # --test-dir has to be provided despite using a preset
-          ctest -M Experimental --group Continuous --test-dir ${build_dir_full} -T Test -T Submit --preset=${CMAKE_PRESET}${preset_postfix} --output-junit Tests/ctest.xml --stop-time `date -d "today + ${ctest_timeout} minutes" +'%H:%M:%S'` ${CTEST_ARGS}
+          ${xvfb_run_cmd} ctest -M Experimental --group Continuous --test-dir ${build_dir_full} -T Test -T Submit --preset=${CMAKE_PRESET}${preset_postfix} --output-junit Tests/ctest.xml --stop-time `date -d "today + ${ctest_timeout} minutes" +'%H:%M:%S'` ${CTEST_ARGS}
         fi
 
       else
diff --git a/scripts/ci/jobs/jupyter.yml b/scripts/ci/jobs/jupyter.yml
index 8e524fd3c6b983b6a9aa42b70a5e5ce29ce2f76f..d53ff673f7ff09c5b72ba0a677f88bfb10d259f2 100644
--- a/scripts/ci/jobs/jupyter.yml
+++ b/scripts/ci/jobs/jupyter.yml
@@ -11,7 +11,7 @@ test notebooks via wheel:
     # TODO:
     #  - better timeout
     #  - run in parallel
-    - find . -type f -iname '*.ipynb' | grep -vP '\.ipynb_checkpoints|\.ci-skip.ipynb$|_out|.venv|PhaseField' | PYVISTA_HEADLESS=1 xargs python Notebooks/testrunner.py --out _out --timeout 1200
+    - find . -type f -iname '*.ipynb' | grep -vP '\.ipynb_checkpoints|\.ci-skip.ipynb$|_out|.venv|PhaseField' | xargs xvfb-run -a python Notebooks/testrunner.py --out _out --timeout 1200
   artifacts:
     when: always
     paths:
diff --git a/scripts/cmake/PythonSetup.cmake b/scripts/cmake/PythonSetup.cmake
index 2eae0cfc73cee21781af7092355abc1de1d174de..8729dcbec891a5cd9944f8df892cd3d66ad10591 100644
--- a/scripts/cmake/PythonSetup.cmake
+++ b/scripts/cmake/PythonSetup.cmake
@@ -130,6 +130,16 @@ function(setup_venv)
             OUTPUT_VARIABLE _out
             ERROR_VARIABLE _err
         )
+        if(DEFINED ENV{CI_JOB_IMAGE})
+            # Install gmsh package without X11 dependencies in Docker CI builds
+            execute_process(
+                COMMAND
+                    ${LOCAL_VIRTUALENV_BIN_DIR}/pip install -i
+                    https://gmsh.info/python-packages-dev-nox --force-reinstall
+                    --no-cache-dir gmsh
+                WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+            )
+        endif()
         if(${_return_code} EQUAL 0)
             set(_OGS_PYTHON_PACKAGES_SHA1 "${_ogs_python_packages_sha1}"
                 CACHE INTERNAL ""
diff --git a/scripts/cmake/test/NotebookTest.cmake b/scripts/cmake/test/NotebookTest.cmake
index 0dd127c102bdf6f1c05d74c586861abdd7ec9ae4..2d52dfba410bb9769096d61601c7102884924ada 100644
--- a/scripts/cmake/test/NotebookTest.cmake
+++ b/scripts/cmake/test/NotebookTest.cmake
@@ -68,7 +68,6 @@ function(NotebookTest)
             )
             return()
         endif()
-        set(_pyvista_headless_env -E env PYVISTA_HEADLESS=1)
     endif()
 
     if("${_pyvista_check}" GREATER 0)
@@ -112,7 +111,7 @@ function(NotebookTest)
     add_test(
         NAME ${TEST_NAME}
         COMMAND
-            ${CMAKE_COMMAND} ${_pyvista_headless_env} ${CMAKE_COMMAND}
+            ${CMAKE_COMMAND} ${CMAKE_COMMAND}
             # TODO: only works if notebook is in a leaf directory
             # -DFILES_TO_DELETE=${Data_BINARY_DIR}/${NotebookTest_DIR}
             -DEXECUTABLE=${Python_EXECUTABLE} "-DEXECUTABLE_ARGS=${_exe_args}"