diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 507f3e7..3aa5762 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -120,7 +120,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [15.x]
+        node-version: [18.x]
 
     steps:
       - uses: actions/checkout@v2
diff --git a/Dockerfile b/Dockerfile
index c134c50..3925343 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 ### Build UI ###
-FROM --platform=${BUILDPLATFORM:-linux/amd64} node:15 as ui
+FROM --platform=${BUILDPLATFORM:-linux/amd64} node:18 as ui
 
 ARG REACT_APP_API_ENDPOINT
 ENV REACT_APP_API_ENDPOINT=${REACT_APP_API_ENDPOINT}
@@ -29,10 +29,10 @@ RUN npm ci --omit=dev --ignore-scripts
 
 # Build frontend
 COPY ui /app
-RUN npm run build -- --public-url $UI_PUBLIC_URL
+RUN npm run build -- --base=$UI_PUBLIC_URL
 
 ### Build API ###
-FROM --platform=${BUILDPLATFORM:-linux/amd64} debian:bookworm AS api
+FROM --platform=${BUILDPLATFORM:-linux/amd64} ubuntu:22.10 AS api
 ARG TARGETPLATFORM
 
 COPY docker/install_build_dependencies.sh /tmp/
@@ -67,7 +67,7 @@ COPY api /app
 RUN go build -v -o photoview .
 
 ### Copy api and ui to production environment ###
-FROM debian:bookworm
+FROM ubuntu:22.10
 ARG TARGETPLATFORM
 WORKDIR /app
 
@@ -75,7 +75,7 @@ COPY api/data /app/data
 
 RUN apt update \
   # Required dependencies
-  && apt install -y curl gpg libdlib19 ffmpeg exiftool libheif1
+  && apt install -y curl gpg libdlib19.1 ffmpeg exiftool libheif1
 
 # Install Darktable if building for a supported architecture
 RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
@@ -87,7 +87,7 @@ RUN apt purge -y gpg \
   && apt clean \
   && rm -rf /var/lib/apt/lists/*
 
-COPY --from=ui /app/build /ui
+COPY --from=ui /app/dist /ui
 COPY --from=api /app/photoview /app/photoview
 
 ENV PHOTOVIEW_LISTEN_IP 127.0.0.1
diff --git a/ui/package-lock.json b/ui/package-lock.json
index ec19579..e47ca4e 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -56,6 +56,7 @@
         "@vitest/ui": "^0.17.1",
         "apollo": "2.34.0",
         "apollo-language-server": "1.26.9",
+        "c8": "^7.11.3",
         "eslint": "^8.19.0",
         "eslint-config-prettier": "^8.5.0",
         "husky": "^8.0.1",
@@ -7596,6 +7597,46 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/c8": {
+      "version": "7.11.3",
+      "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz",
+      "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==",
+      "dev": true,
+      "dependencies": {
+        "@bcoe/v8-coverage": "^0.2.3",
+        "@istanbuljs/schema": "^0.1.3",
+        "find-up": "^5.0.0",
+        "foreground-child": "^2.0.0",
+        "istanbul-lib-coverage": "^3.2.0",
+        "istanbul-lib-report": "^3.0.0",
+        "istanbul-reports": "^3.1.4",
+        "rimraf": "^3.0.2",
+        "test-exclude": "^6.0.0",
+        "v8-to-istanbul": "^9.0.0",
+        "yargs": "^16.2.0",
+        "yargs-parser": "^20.2.9"
+      },
+      "bin": {
+        "c8": "bin/c8.js"
+      },
+      "engines": {
+        "node": ">=10.12.0"
+      }
+    },
+    "node_modules/c8/node_modules/v8-to-istanbul": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
+      "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.12",
+        "@types/istanbul-lib-coverage": "^2.0.1",
+        "convert-source-map": "^1.6.0"
+      },
+      "engines": {
+        "node": ">=10.12.0"
+      }
+    },
     "node_modules/call-bind": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -11582,6 +11623,19 @@
         "is-callable": "^1.1.3"
       }
     },
+    "node_modules/foreground-child": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+      "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+      "dev": true,
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/fork-ts-checker-webpack-plugin": {
       "version": "6.5.2",
       "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz",
@@ -30526,6 +30580,39 @@
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
       "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
     },
+    "c8": {
+      "version": "7.11.3",
+      "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz",
+      "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==",
+      "dev": true,
+      "requires": {
+        "@bcoe/v8-coverage": "^0.2.3",
+        "@istanbuljs/schema": "^0.1.3",
+        "find-up": "^5.0.0",
+        "foreground-child": "^2.0.0",
+        "istanbul-lib-coverage": "^3.2.0",
+        "istanbul-lib-report": "^3.0.0",
+        "istanbul-reports": "^3.1.4",
+        "rimraf": "^3.0.2",
+        "test-exclude": "^6.0.0",
+        "v8-to-istanbul": "^9.0.0",
+        "yargs": "^16.2.0",
+        "yargs-parser": "^20.2.9"
+      },
+      "dependencies": {
+        "v8-to-istanbul": {
+          "version": "9.0.1",
+          "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
+          "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==",
+          "dev": true,
+          "requires": {
+            "@jridgewell/trace-mapping": "^0.3.12",
+            "@types/istanbul-lib-coverage": "^2.0.1",
+            "convert-source-map": "^1.6.0"
+          }
+        }
+      }
+    },
     "call-bind": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -33414,6 +33501,16 @@
         "is-callable": "^1.1.3"
       }
     },
+    "foreground-child": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+      "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
     "fork-ts-checker-webpack-plugin": {
       "version": "6.5.2",
       "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz",
diff --git a/ui/package.json b/ui/package.json
index c2ba980..f707418 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -13,7 +13,7 @@
     "build": "vite build",
     "lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
     "test": "vitest",
-    "test:ci": "CI=true vitest --verbose --ci --coverage",
+    "test:ci": "CI=true vitest --reporter verbose --run --coverage",
     "genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts && prettier --write */**/__generated__/*.ts",
     "extractTranslations": "i18next -c i18next-parser.config.js",
     "prepare": "(cd .. && ./ui/node_modules/.bin/husky install)"
@@ -66,6 +66,7 @@
     "@vitest/ui": "^0.17.1",
     "apollo": "2.34.0",
     "apollo-language-server": "1.26.9",
+    "c8": "^7.11.3",
     "eslint": "^8.19.0",
     "eslint-config-prettier": "^8.5.0",
     "husky": "^8.0.1",
