diff --git a/scripts/ci/extends/template-build-linux.yml b/scripts/ci/extends/template-build-linux.yml
index b1e248910382457dbaf3a677931488e2e959df35..a029ee7d95b88a9127a49607be138a439ec119b2 100644
--- a/scripts/ci/extends/template-build-linux.yml
+++ b/scripts/ci/extends/template-build-linux.yml
@@ -18,6 +18,25 @@
     # Activate .venv
     - test -f $build_dir_full/.venv/bin/activate && source $build_dir_full/.venv/bin/activate
     - |
+      function maybe_run_with_xvfb()
+      {
+          if [[ "$OSTYPE" == "darwin"* ]]; then
+            "$@"
+          elif command -v xvfb-run &> /dev/null ; then
+            local ctest_status_file="$build_dir_full/ctest_status"
+            rm -f "$ctest_status_file"
+            # status file and sh -c to workaround xvfb-run problems:
+            # /usr/bin/xvfb-run: line 186: kill: (<PID>) - No such process"
+            xvfb-run -a \
+                sh -c 'statf="$1"; shift; "$@"; echo "$?" >"$statf"' \
+                -- "$ctest_status_file" "$@" \
+                || true
+            [ 0 = "`cat "$ctest_status_file"`" ]
+          else
+            "$@"
+          fi
+      }
+
       if [[ -z "$TARGETS" ]]; then
 
         if [ "$BUILD_PACKAGE" = false ]; then
@@ -41,11 +60,8 @@
             preset_postfix="-large"
           fi
 
-          xvfb_run_cmd=""
           if [[ "$OSTYPE" == "darwin"* ]]; then
             alias date=gdate
-          elif command -v xvfb-run &> /dev/null ; then
-            xvfb_run_cmd="xvfb-run -a"
           fi
 
           ctest_arguments=""
@@ -68,7 +84,13 @@
           if [[ "$CI_MERGE_REQUEST_LABELS" =~ .*unit_tests.* ]]; then
             echo "Skipping ctests because of unit_tests-label."
           else
-            eval ${xvfb_run_cmd} ctest -M Experimental --group ${ctest_group} ${regex_argument} --test-dir ${build_dir_full} -T Start -T Test -T Submit ${ctest_arguments} --output-junit Tests/ctest.xml --stop-time `date -d "today + ${ctest_timeout} minutes" +'%H:%M:%S'`
+            maybe_run_with_xvfb ctest \
+                -M Experimental --group "${ctest_group}" "${regex_argument}" \
+                --test-dir "${build_dir_full}" -T Start -T Test -T Submit \
+                ${ctest_arguments} \
+                --output-junit Tests/ctest.xml \
+                --stop-time "`date -d "today + ${ctest_timeout} minutes" +'%H:%M:%S'`" \
+                --no-tests=error
           fi
         fi
 
diff --git a/scripts/ci/jobs/jupyter.yml b/scripts/ci/jobs/jupyter.yml
index d72753b02674a6607c4cd4645f488acd85aafe1f..d689cdb6e1c3c5d5e932bd7fe424ff4b3401a22a 100644
--- a/scripts/ci/jobs/jupyter.yml
+++ b/scripts/ci/jobs/jupyter.yml
@@ -11,7 +11,21 @@ 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' | xargs xvfb-run -a python Notebooks/testrunner.py --out _out
+    - |
+      status_file="`mktemp --tmpdir`"
+      echo 0 >"$status_file"  # success if no Notebooks are found
+      # status file and sh -c to workaround xvfb-run problems:
+      # /usr/bin/xvfb-run: line 186: kill: (<PID>) - No such process"
+      find . -type f -iname '*.ipynb' \
+          | grep -vP '\.ipynb_checkpoints|\.ci-skip\.ipynb$|_out|\.venv|PhaseField' \
+          | xargs xvfb-run -a \
+              sh -c 'statf="$1"; shift; "$@" || echo "$?" >"$statf"' \
+              -- "$status_file" \
+              python Notebooks/testrunner.py --out _out \
+          || true
+      status="`cat "$status_file"`"
+      rm "$status_file"
+      [ 0 = "$status" ]
   artifacts:
     when: always
     paths: