diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b405b47cb6be68df9ed8e7d7941244f2cc22f643..cab254a8cead5777e77d2180acf280bf8cdfdbab 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: node:16
+image: node:18
 cache:
   paths:
     - node_modules
diff --git a/Dockerfile b/Dockerfile
index a4eba23cfd2e33d53d8ed21c5b0c529bb034a520..831ee92da98a9c44ebee34cd9920c5db395bbf85 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # build stage
-FROM node:16 as build-stage
+FROM node:18 as build-stage
 WORKDIR /app
 HEALTHCHECK --interval=35s --timeout=4s CMD curl -f http://localhost || exit 1
 # RUN apk add yarn
@@ -11,7 +11,7 @@ RUN npm run build-only
 
 # production stage
 FROM nginx:stable-alpine as production-stage
-HEALTHCHECK --interval=35s --timeout=4s CMD curl --head -f http://localhost || exit 1
+HEALTHCHECK --interval=305s --timeout=4s CMD curl --head -f http://localhost || exit 1
 COPY --from=build-stage /app/dist /usr/share/nginx/html
 COPY --from=build-stage /app/src/assets/env.template.js /tmp
 COPY nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/package-lock.json b/package-lock.json
index deb819d45a851e4ac379e5281b1efa41693a6284..03f8850644a83ad975e37a422fac5b51b4c851d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,7 +31,7 @@
         "@esbuild-plugins/node-globals-polyfill": "~0.2.3",
         "@esbuild-plugins/node-modules-polyfill": "~0.2.2",
         "@rushstack/eslint-patch": "~1.2.0",
-        "@tsconfig/node16": "^16.1.1",
+        "@tsconfig/node18": "^18.2.1",
         "@types/bootstrap": "~5.2.6",
         "@types/dompurify": "~3.0.2",
         "@types/node": "^16.18.48",
@@ -2051,10 +2051,10 @@
         "node": ">=14.0.0"
       }
     },
-    "node_modules/@tsconfig/node16": {
-      "version": "16.1.1",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.1.tgz",
-      "integrity": "sha512-+pio93ejHN4nINX4pXqfnR/fPLRtJBaT4ORaa5RH0Oc1zoYmo2B2koG+M328CQhHKn1Wj6FcOxCDFXAot9NhvA==",
+    "node_modules/@tsconfig/node18": {
+      "version": "18.2.1",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.1.tgz",
+      "integrity": "sha512-RDDZFuofwkcKpl8Vpj5wFbY+H53xOtqK7ckEL1sXsbPwvKwDdjQf3LkHbtt9sxIHn9nWIEwkmCwBRZ6z5TKU2A==",
       "dev": true
     },
     "node_modules/@types/bootstrap": {
@@ -8099,10 +8099,10 @@
         "tslib": "^2.5.0"
       }
     },
-    "@tsconfig/node16": {
-      "version": "16.1.1",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-16.1.1.tgz",
-      "integrity": "sha512-+pio93ejHN4nINX4pXqfnR/fPLRtJBaT4ORaa5RH0Oc1zoYmo2B2koG+M328CQhHKn1Wj6FcOxCDFXAot9NhvA==",
+    "@tsconfig/node18": {
+      "version": "18.2.1",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.1.tgz",
+      "integrity": "sha512-RDDZFuofwkcKpl8Vpj5wFbY+H53xOtqK7ckEL1sXsbPwvKwDdjQf3LkHbtt9sxIHn9nWIEwkmCwBRZ6z5TKU2A==",
       "dev": true
     },
     "@types/bootstrap": {
diff --git a/package.json b/package.json
index 41c9d17529e470fab6593c1a7cff452aaa8d6d6c..41e4a72dcd15f579ef85f2d0676dd54e63b19bfd 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
     "@esbuild-plugins/node-globals-polyfill": "~0.2.3",
     "@esbuild-plugins/node-modules-polyfill": "~0.2.2",
     "@rushstack/eslint-patch": "~1.2.0",
-    "@tsconfig/node16": "^16.1.1",
+    "@tsconfig/node18": "^18.2.1",
     "@types/bootstrap": "~5.2.6",
     "@types/dompurify": "~3.0.2",
     "@types/node": "^16.18.48",
diff --git a/src/components/workflows/modals/ArbitraryWorkflowModal.vue b/src/components/workflows/modals/ArbitraryWorkflowModal.vue
index 87952e20e32a58277dbf9a1ea052a93b4fc885a7..9abbaf2783fa0d6868361493de778f1114a70f86 100644
--- a/src/components/workflows/modals/ArbitraryWorkflowModal.vue
+++ b/src/components/workflows/modals/ArbitraryWorkflowModal.vue
@@ -1,20 +1,22 @@
 <script setup lang="ts">
 import BootstrapModal from "@/components/modals/BootstrapModal.vue";
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
-import { computed, onMounted, reactive, ref } from "vue";
+import { computed, onMounted, reactive, ref, watch } from "vue";
 import { useRouter } from "vue-router";
 import {
   GitRepository,
   requiredRepositoryFiles,
   determineGitIcon,
 } from "@/utils/GitRepository";
-import { Modal } from "bootstrap";
+import { Collapse, Modal } from "bootstrap";
 
 const props = defineProps<{
   modalID: string;
 }>();
 
 let createWorkflowModal: Modal | null = null;
+let privateRepositoryCollapse: Collapse | null = null;
+let tokenHelpCollapse: Collapse | null = null;
 const arbitraryWorkflowForm = ref<HTMLFormElement | undefined>(undefined);
 const workflowRepositoryElement = ref<HTMLInputElement | undefined>(undefined);
 const router = useRouter();
@@ -27,6 +29,14 @@ const workflow = reactive<{
   git_commit_hash: "",
 });
 
+const repositoryCredentials = reactive<{
+  token: string;
+  privateRepo: boolean;
+}>({
+  token: "",
+  privateRepo: false,
+});
+
 const formState = reactive<{
   loading: boolean;
   checkRepoLoading: boolean;
@@ -43,8 +53,21 @@ const formState = reactive<{
   unsupportedRepository: false,
 });
 
+watch(
+  () => repositoryCredentials.privateRepo,
+  (show) => {
+    if (show) {
+      privateRepositoryCollapse?.show();
+    } else {
+      privateRepositoryCollapse?.hide();
+      tokenHelpCollapse?.hide();
+    }
+  },
+);
+
 function modalClosed() {
   formState.validated = false;
+  tokenHelpCollapse?.hide();
 }
 
 function viewWorkflow() {
@@ -54,6 +77,10 @@ function viewWorkflow() {
     query: {
       repository: encodeURI(workflow.repository_url),
       commit_hash: workflow.git_commit_hash,
+      token:
+        repositoryCredentials.token.length > 0
+          ? encodeURIComponent(repositoryCredentials.token)
+          : undefined,
     },
   });
 }
@@ -68,6 +95,9 @@ function checkRepository() {
       const repo = GitRepository.buildRepository(
         workflow.repository_url,
         workflow.git_commit_hash,
+        repositoryCredentials.privateRepo
+          ? repositoryCredentials.token
+          : undefined,
       );
       repo
         .checkFilesExist(requiredRepositoryFiles, true)
@@ -93,12 +123,19 @@ function checkRepository() {
     }
   }
 }
+
 const gitIcon = computed<string>(() =>
   determineGitIcon(workflow.repository_url),
 );
 
 onMounted(() => {
   createWorkflowModal = new Modal("#" + props.modalID);
+  privateRepositoryCollapse = new Collapse("#privateRepositoryCollapse", {
+    toggle: false,
+  });
+  tokenHelpCollapse = new Collapse("#tokenHelpCollapse", {
+    toggle: false,
+  });
 });
 </script>
 
@@ -176,6 +213,73 @@ onMounted(() => {
             </li>
           </ul>
         </div>
+        <div class="mb-3">
+          <div class="form-check fs-5">
+            <input
+              class="form-check-input"
+              type="checkbox"
+              v-model="repositoryCredentials.privateRepo"
+              id="privateRepositoryCheckbox"
+              @change="formState.allowUpload = false"
+              aria-controls="#privateRepositoryCollapse"
+            />
+            <label class="form-check-label" for="privateRepositoryCheckbox">
+              Private Git Repository
+            </label>
+          </div>
+          <div class="collapse" id="privateRepositoryCollapse">
+            <label for="tokenUsernameInput" class="form-label">Token</label>
+            <div class="input-group">
+              <div class="input-group-text">
+                <font-awesome-icon icon="fa-solid fa-key" />
+              </div>
+              <input
+                type="password"
+                class="form-control"
+                id="repositoryTokenInput"
+                v-model="repositoryCredentials.token"
+                @change="formState.allowUpload = false"
+                :required="repositoryCredentials.privateRepo"
+                aria-controls="#tokenHelpCollapse"
+              />
+              <div
+                class="input-group-text cursor-pointer hover-info"
+                @click="tokenHelpCollapse?.toggle()"
+              >
+                <font-awesome-icon icon="fa-solid fa-circle-question" />
+              </div>
+            </div>
+            <div class="collapse" id="tokenHelpCollapse">
+              <div class="card card-body mt-3">
+                <h5>GitHub</h5>
+                <p>
+                  For private GitHub repositories, CloWM needs a Personal Access
+                  Token (classic) with the scope <code>repo</code>.<br />
+                  Read this
+                  <a
+                    target="_blank"
+                    href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic"
+                    >Tutorial</a
+                  >
+                  on how to create such a token.
+                </p>
+                <h5>GitLab</h5>
+                <p>
+                  For private GitLab repositories, CloWM needs a Project Access
+                  Token with the <code>read_api</code> scope and at least
+                  <code>Reporter</code> role.<br />
+                  Read this
+                  <a
+                    target="_blank"
+                    href="https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html#create-a-project-access-token"
+                    >Tutorial</a
+                  >
+                  on how to create such a token.
+                </p>
+              </div>
+            </div>
+          </div>
+        </div>
       </form>
     </template>
     <template v-slot:footer>
@@ -215,4 +319,8 @@ onMounted(() => {
   </bootstrap-modal>
 </template>
 
-<style scoped></style>
+<style scoped>
+.hover-info:hover {
+  color: var(--bs-info) !important;
+}
+</style>
diff --git a/src/router/index.ts b/src/router/index.ts
index 8abd1c20ea0b19fd0cc8c0381e86aeb4e2e7fcc2..cd6d25646eb4b2ff556af43ba1b667106b4e973a 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -60,6 +60,7 @@ const router = createRouter({
           props: (route) => ({
             repository: route.query.repository,
             commit_hash: route.query.commit_hash,
+            token: route.query.token,
           }),
         },
         {
diff --git a/src/utils/GitRepository.ts b/src/utils/GitRepository.ts
index 6baffa13c27556bb42e11c22f2d9756f35ea6188..68004e6c06e19000f3710ff2db33f8430c200301 100644
--- a/src/utils/GitRepository.ts
+++ b/src/utils/GitRepository.ts
@@ -57,7 +57,7 @@ export abstract class GitRepository {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   async downloadFile(filepath: string): Promise<any> {
     try {
-      return await axios.get(await this.downloadFileUrl(filepath));
+      return await this.httpClient.get(await this.downloadFileUrl(filepath));
     } catch (e) {
       return "";
     }
diff --git a/src/views/workflows/ArbitraryWorkflowView.vue b/src/views/workflows/ArbitraryWorkflowView.vue
index a30b0ca50e3989dec5b44c79a4b15d4d10c07c2c..cc1c87b0c28732dd69e14989523e1f7c01230f64 100644
--- a/src/views/workflows/ArbitraryWorkflowView.vue
+++ b/src/views/workflows/ArbitraryWorkflowView.vue
@@ -11,6 +11,7 @@ import { Toast } from "bootstrap";
 const props = defineProps<{
   repository?: string;
   commit_hash?: string;
+  token?: string;
 }>();
 
 const router = useRouter();
@@ -37,12 +38,13 @@ const workflowExecutionState = reactive<{
 const showDocumentation = ref<boolean>(true);
 let errorToast: Toast | null = null;
 
-function downloadVersionFiles(repository: string, commit_hash: string) {
+function downloadVersionFiles(
+  repository: string,
+  commit_hash: string,
+  token?: string,
+) {
   workflowState.loading = true;
-  const repo = GitRepository.buildRepository(repository, commit_hash);
-  //const descriptionPromise = repo.downloadFile().then((response) => {
-  // versionState.descriptionMarkdown = response.data;
-  //});
+  const repo = GitRepository.buildRepository(repository, commit_hash, token);
   Promise.all(
     requiredRepositoryFiles.map((file) =>
       repo.downloadFile(file).then((response) => {
@@ -78,6 +80,7 @@ function startWorkflow(
       parameters: parameters,
       report_output_bucket: report_output_bucket,
       repository_url: props.repository,
+      token: props.token,
     })
       .then(() => {
         router.push({
@@ -100,7 +103,7 @@ function startWorkflow(
 onMounted(() => {
   errorToast = new Toast("#arbitraryWorkflowExecutionViewErrorToast");
   if (props.commit_hash && props.repository) {
-    downloadVersionFiles(props.repository, props.commit_hash);
+    downloadVersionFiles(props.repository, props.commit_hash, props.token);
   }
 });
 </script>
diff --git a/tsconfig.json b/tsconfig.json
index 4f9228fddf23ba7b1325ff0a1b6ee7c54790b20e..32d333fdaaf1d55c8b026efa8c95a87163d171f1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
 {
   "extends": [
-    "@tsconfig/node16/tsconfig.json",
+    "@tsconfig/node18/tsconfig.json",
     "@vue/tsconfig/tsconfig.json"
   ],
   "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],