diff --git a/backend/app.py b/backend/app.py
index 01436ce6374ecb4a190e2243a9a6c6c6027c6bd9..06db7d65e6edd9947b356375b2b59e360a79b891 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -14,6 +14,7 @@ from web import web
 from areas import users
 from areas import apps
 from areas import auth
+from areas import resources
 from areas import roles
 from areas import tags
 from cliapp import cliapp
diff --git a/backend/areas/resources/__init__.py b/backend/areas/resources/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7318f9e29ab05497740653b77334ad4c34c85a5b
--- /dev/null
+++ b/backend/areas/resources/__init__.py
@@ -0,0 +1,2 @@
+from .resources import *
+from .resources_service import *
diff --git a/backend/areas/resources/resources.py b/backend/areas/resources/resources.py
new file mode 100644
index 0000000000000000000000000000000000000000..c44b4010698c3426644f5a198b2713c22fbfc6ef
--- /dev/null
+++ b/backend/areas/resources/resources.py
@@ -0,0 +1,18 @@
+from flask import jsonify, request
+from flask_cors import cross_origin
+from flask_expects_json import expects_json
+from flask_jwt_extended import get_jwt, jwt_required
+
+from areas import api_v1
+from helpers.auth_guard import admin_required
+
+from .resources_service import ResourcesService
+
+
+@api_v1.route("/resources", methods=["GET"])
+@jwt_required()
+@cross_origin()
+@admin_required()
+def get_resources():
+    res = ResourcesService.get_resources()
+    return jsonify(res)
diff --git a/backend/areas/resources/resources_service.py b/backend/areas/resources/resources_service.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b5238d641e4e5bab2db1dfbbc9c928ede3b0126
--- /dev/null
+++ b/backend/areas/resources/resources_service.py
@@ -0,0 +1,104 @@
+# from config import KRATOS_ADMIN_URL
+
+# from database import db
+
+# from kubernetes.client import CustomObjectsApi
+# import re
+import requests
+
+from flask import current_app
+# import helpers.kubernetes
+
+
+class ResourcesService:
+    @classmethod
+    def get_resources(cls):
+        # custom = CustomObjectsApi()
+        # raw_nodes = custom.list_cluster_custom_object('metrics.k8s.io', 'v1beta1', 'nodes')
+        # raw_pods = custom.list_cluster_custom_object('metrics.k8s.io', 'v1beta1', 'pods')
+        # nodes = []
+        # for node in raw_nodes["items"]:
+        #     nodes.append({
+        #         "name": node["metadata"]["name"],
+        #         "cpu_raw": node["usage"]["cpu"],
+        #         "cpu": cls.parse_cpu(node["usage"]["cpu"]),
+        #         "memory_raw": node["usage"]["memory"],
+        #         "memory_used": cls.parse_memory(node["usage"]["memory"]),
+        #     })
+        cores = cls.get_prometheus('machine_cpu_cores')
+        return {
+            # Number of cores in use. So average usage times number of cores.
+            "cpu": cores * cls.get_prometheus('1 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[8m])))', 'float'),
+            "cpu_total": cores,
+        #         "memory_raw": node["usage"]["memory"],
+        #         "memory_used": cls.parse_memory(node["usage"]["memory"]),
+            "memory_total": cls.get_prometheus('machine_memory_bytes'),
+            "memory_available": cls.get_prometheus('node_memory_MemAvailable_bytes'),
+            "disk_free": cls.get_prometheus('node_filesystem_free_bytes{mountpoint="/"}'),
+            "disk_total": cls.get_prometheus('node_filesystem_size_bytes{mountpoint="/"}'),
+        }
+
+    # @staticmethod
+    # def parse_cpu(s):
+    #     result = re.match(r"^(\d+)([mun]?)$", s)
+    #     if result is None:
+    #         raise Exception("cpu data does not match known patterns")
+    #     number = result.group(1)
+    #     suffix = result.group(2)
+    #     multipliers = {"": 1, "m": 1e-3, "u": 1e-6, "n": 1e-9}
+    #     return (int(number) * multipliers[suffix])
+
+    # @staticmethod
+    # def parse_memory(s):
+    #     result = re.match(r"^(\d+)(|Ki|Mi|Gi)$", s)
+    #     if result is None:
+    #         raise Exception("memory data does not match known patterns")
+    #     number = result.group(1)
+    #     suffix = result.group(2)
+    #     multipliers = {"": 1, "Ki": 1024, "Mi": 1024*1024, "Gi": 1024*1024*1024}
+    #     return (int(number) * multipliers[suffix])
+
+    @staticmethod
+    def get_prometheus(query, cast='int'):
+        try:
+            params = {
+                "query": query,
+            }
+            result = requests.get("http://kube-prometheus-stack-prometheus:9090/api/v1/query", params=params)
+            current_app.logger.info(query)
+            current_app.logger.info(result.json())
+            value = result.json()["data"]["result"][0]["value"][1]
+        except AttributeError:
+            return None
+        if cast == 'float':
+            converted = float(value)
+        else:
+            converted = int(value)
+        return converted
+
+  # "pods": {
+  #   "apiVersion": "metrics.k8s.io/v1beta1",
+  #   "items": [
+  #     {
+  #       "containers": [
+  #         {
+  #           "name": "traffic-manager",
+  #           "usage": {
+  #             "cpu": "2839360n",
+  #             "memory": "24696Ki"
+  #           }
+  #         }
+  #       ],
+  #       "metadata": {
+  #         "creationTimestamp": "2023-11-30T15:10:10Z",
+  #         "labels": {
+  #           "app": "traffic-manager",
+  #           "pod-template-hash": "5cd7cc7fd6",
+  #           "telepresence": "manager"
+  #         },
+  #         "name": "traffic-manager-5cd7cc7fd6-mp7td",
+  #         "namespace": "ambassador"
+  #       },
+  #       "timestamp": "2023-11-30T15:10:00Z",
+  #       "window": "12.942s"
+  #     },
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7d2d7e1131480e90e50a7d6a249285a1bdce83bb..e9b064f527150ea91098e49e638a920a3544bd97 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,7 +4,7 @@ import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
 import { Toaster } from 'react-hot-toast';
 
 import { useAuth } from 'src/services/auth';
-import { Dashboard, Users, Login, Apps, AppSingle } from './modules';
+import { Dashboard, Users, Login, Apps, AppSingle, ResourcesDashboard } from './modules';
 import { Layout } from './components';
 import { LoginCallback } from './modules/login/LoginCallback';
 
@@ -49,6 +49,9 @@ function App() {
                 <Route path=":slug" element={<AppSingle />} />
                 <Route index element={<Apps />} />
               </Route>
+              <Route path="/resources" element={<ProtectedRoute />}>
+                <Route index element={<ResourcesDashboard />} />
+              </Route>
               <Route path="*" element={<Navigate to="/dashboard" />} />
             </Routes>
           </Layout>
diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx
index 76619801ba39cc4cc70ff5c60a95037e48253d38..b9227c3c39e6e447d852d82e8a794503f00f7718 100644
--- a/frontend/src/components/Header/Header.tsx
+++ b/frontend/src/components/Header/Header.tsx
@@ -16,6 +16,7 @@ const navigation = [
   { name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
   { name: 'Users', to: '/users', requiresAdmin: true },
   { name: 'Apps', to: '/apps', requiresAdmin: true },
+  { name: 'System resources', to: '/resources', requiresAdmin: true },
 ];
 
 function classNames(...classes: any[]) {
diff --git a/frontend/src/modules/index.ts b/frontend/src/modules/index.ts
index 389ad968f3a1a8123bda1cfb8b4d671ec770de1a..662dbe7ccbe3135317706cd2b35aab1db2bcdf54 100644
--- a/frontend/src/modules/index.ts
+++ b/frontend/src/modules/index.ts
@@ -1,4 +1,5 @@
 export { Login } from './login';
 export { Dashboard } from './dashboard';
 export { Apps, AppSingle } from './apps';
+export { ResourcesDashboard } from './resources';
 export { Users } from './users';
diff --git a/frontend/src/modules/resources/Resources.tsx b/frontend/src/modules/resources/Resources.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2414e92fc64f62cd44acac7e4932ad1c8a75d939
--- /dev/null
+++ b/frontend/src/modules/resources/Resources.tsx
@@ -0,0 +1,57 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+/**
+ * This page shows system information, in particular resource usage (cpu,
+ * memory, disk).
+ *
+ * This page is only available for admin users.
+ */
+import React, { useEffect } from 'react';
+import { ResourceCard } from './components/ResourceCard';
+import { useResources } from 'src/services/resources';
+
+const metrics = [
+  {
+    id: 'memory',
+    title: 'Memory',
+  },
+  {
+    id: 'cpu',
+    title: 'CPU',
+  },
+  {
+    id: 'disk',
+    title: 'Disk',
+  },
+];
+
+export const ResourcesDashboard: React.FC = () => {
+  const { resources, loadResources } = useResources();
+  window.console.log(resources);
+
+  useEffect(() => {
+    loadResources();
+  }, []);
+
+  return (
+    <div className="relative">
+      <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
+        <div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
+          <h1 className="text-3xl leading-6 font-bold text-gray-900">System resources</h1>
+        </div>
+        <div className="flex flex-col">
+          <div className="-my-2 sm:-mx-6 lg:-mx-8">
+            <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
+              <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
+                <div className=" mt-5 grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-3 mb-10">
+                  {metrics.map((metric) => (
+                    <ResourceCard key={metric.id} metric={metric} resources={resources} />
+                  ))}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/frontend/src/modules/resources/components/ResourceCard/ResourceCard.tsx b/frontend/src/modules/resources/components/ResourceCard/ResourceCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f966504b4dddd4a962256f6634f842abe7142ca3
--- /dev/null
+++ b/frontend/src/modules/resources/components/ResourceCard/ResourceCard.tsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import GaugeComponent from 'react-gauge-component';
+
+import { Resources } from 'src/services/resources';
+
+type ResourceCardProps = {
+  metric: any;
+  resources: Resources;
+};
+
+const format = (id: string) => {
+  switch (id) {
+    case 'disk':
+      return (s: number) => `${s.toFixed(1)} GB`;
+    case 'memory':
+      return (s: number) => `${s.toFixed(1)} GB`;
+    case 'cpu':
+      return (s: number) => `${s.toFixed(0)}%`;
+  }
+};
+const formatTick = (id: string) => {
+  switch (id) {
+    case 'disk':
+      return (s: number) => `${s.toFixed(1)}`;
+    case 'memory':
+      return (s: number) => `${s.toFixed(1)}`;
+    case 'cpu':
+      return (s: number) => `${s.toFixed(0)}%`;
+  }
+};
+
+const getValue = (id: string, resources: Resources) => {
+  switch (id) {
+    case 'disk':
+      return resources.disk_used;
+    case 'memory':
+      return resources.memory_used;
+    case 'cpu':
+      return resources.cpu;
+  }
+};
+
+const getMax = (id: string, resources: Resources) => {
+  switch (id) {
+    case 'disk':
+      return resources.disk_total;
+    case 'memory':
+      return resources.memory_total;
+    case 'cpu':
+      return 100;
+    default:
+      return 100;
+  }
+};
+
+export const ResourceCard = ({ metric, resources }: ResourceCardProps) => {
+  const max = getMax(metric.id, resources);
+  return (
+    <>
+      <div
+        className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0 flex flex-col justify-between relative"
+        key={metric.id}
+      >
+        <div className="px-4 pt-4 pb-2">
+          <div className="mr-4 flex items-center">
+            <div className="flex flex-col justify-between gap-1 items-center">
+              <h2 className="text-xl leading-8 font-bold">{metric.title}</h2>
+              <GaugeComponent
+                value={getValue(metric.id, resources)}
+                marginInPercent={{
+                  top: 0.03,
+                  bottom: 0.03,
+                  left: 0.15,
+                  right: 0.15,
+                }}
+                maxValue={max}
+                arc={{
+                  subArcs: [
+                    { limit: max * 0.7, color: 'green' },
+                    { limit: max * 0.85, color: 'orange' },
+                    { limit: max, color: 'red' },
+                  ],
+                }}
+                labels={{
+                  valueLabel: {
+                    formatTextValue: format(metric.id),
+                    style: {
+                      fill: 'black',
+                      textShadow: 'none',
+                    },
+                  },
+                  tickLabels: {
+                    defaultTickValueConfig: {
+                      formatTextValue: formatTick(metric.id),
+                    },
+                  },
+                }}
+                style={{
+                  width: '350px',
+                }}
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </>
+  );
+};
diff --git a/frontend/src/modules/resources/components/ResourceCard/index.ts b/frontend/src/modules/resources/components/ResourceCard/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8332706100a64d41465628548e552ad9ea418e2
--- /dev/null
+++ b/frontend/src/modules/resources/components/ResourceCard/index.ts
@@ -0,0 +1 @@
+export { ResourceCard } from './ResourceCard';
diff --git a/frontend/src/modules/resources/components/index.ts b/frontend/src/modules/resources/components/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8332706100a64d41465628548e552ad9ea418e2
--- /dev/null
+++ b/frontend/src/modules/resources/components/index.ts
@@ -0,0 +1 @@
+export { ResourceCard } from './ResourceCard';
diff --git a/frontend/src/modules/resources/index.ts b/frontend/src/modules/resources/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5d5067f20dd08ee09a09c15ed5d49078d3a7e43b
--- /dev/null
+++ b/frontend/src/modules/resources/index.ts
@@ -0,0 +1 @@
+export { ResourcesDashboard } from './Resources';
diff --git a/frontend/src/redux/store.ts b/frontend/src/redux/store.ts
index 598fcfdef1f26db0068e9f1574b4974698c81ff3..06dcc4ea9272d084f8a5ea2f8d8c0d0f4952c18c 100644
--- a/frontend/src/redux/store.ts
+++ b/frontend/src/redux/store.ts
@@ -7,6 +7,7 @@ import { reducer as authReducer } from 'src/services/auth';
 
 import usersReducer from 'src/services/users/redux/reducers';
 import appsReducer from 'src/services/apps/redux/reducers';
+import resourcesReducer from 'src/services/resources/redux/reducers';
 import sysInfoReducer from 'src/services/sysInfo/redux/reducers';
 import { State } from './types';
 
@@ -20,6 +21,7 @@ const appReducer = combineReducers<State>({
   auth: authReducer,
   users: usersReducer,
   apps: appsReducer,
+  resources: resourcesReducer,
   sysInfo: sysInfoReducer,
 });
 
diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts
index c958fc6c056f9db5cc008cc317e3feff611327d9..ea751c19a151585a6092888b59a7c7c2a14b2921 100644
--- a/frontend/src/redux/types.ts
+++ b/frontend/src/redux/types.ts
@@ -3,6 +3,7 @@ import { Store } from 'redux';
 import { AuthState } from 'src/services/auth/redux';
 import { UsersState } from 'src/services/users/redux';
 import { AppsState } from 'src/services/apps/redux';
+import { ResourcesState } from 'src/services/resources/redux';
 import { SysInfoState } from 'src/services/sysInfo/redux/types';
 
 export interface AppStore extends Store, State {}
@@ -11,5 +12,6 @@ export interface State {
   auth: AuthState;
   users: UsersState;
   apps: AppsState;
+  resources: ResourcesState;
   sysInfo: SysInfoState;
 }
diff --git a/frontend/src/services/resources/hooks/index.ts b/frontend/src/services/resources/hooks/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb7960da006eedcdb09a67196ffaec07ebc97b7e
--- /dev/null
+++ b/frontend/src/services/resources/hooks/index.ts
@@ -0,0 +1 @@
+export { useResources } from './use-resources';
diff --git a/frontend/src/services/resources/hooks/use-resources.ts b/frontend/src/services/resources/hooks/use-resources.ts
new file mode 100644
index 0000000000000000000000000000000000000000..96490973e501b2bfc8c79a0cc59f8c473b891cff
--- /dev/null
+++ b/frontend/src/services/resources/hooks/use-resources.ts
@@ -0,0 +1,17 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchResources } from '../redux';
+import { getResources } from '../redux/selectors';
+
+export function useResources() {
+  const dispatch = useDispatch();
+  const resources = useSelector(getResources);
+
+  function loadResources() {
+    return dispatch(fetchResources());
+  }
+
+  return {
+    resources,
+    loadResources,
+  };
+}
diff --git a/frontend/src/services/resources/index.ts b/frontend/src/services/resources/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a2f15d5e4727b05904516d0cde4f09d66d313dd7
--- /dev/null
+++ b/frontend/src/services/resources/index.ts
@@ -0,0 +1,3 @@
+export * from './types';
+export { reducer } from './redux';
+export { useResources } from './hooks';
diff --git a/frontend/src/services/resources/redux/actions.ts b/frontend/src/services/resources/redux/actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c38ef283a3566691fc482b383acabc21be3eef51
--- /dev/null
+++ b/frontend/src/services/resources/redux/actions.ts
@@ -0,0 +1,23 @@
+import { Dispatch } from 'redux';
+import { performApiCall } from 'src/services/api';
+import { transformResources } from '../transformations';
+
+export enum ResourcesActionTypes {
+  FETCH_RESOURCES = 'resources/fetch_resources',
+}
+
+export const fetchResources = () => async (dispatch: Dispatch<any>) => {
+  try {
+    const { data } = await performApiCall({
+      path: '/resources',
+      method: 'GET',
+    });
+
+    dispatch({
+      type: ResourcesActionTypes.FETCH_RESOURCES,
+      payload: transformResources(data),
+    });
+  } catch (err) {
+    console.error(err);
+  }
+};
diff --git a/frontend/src/services/resources/redux/index.ts b/frontend/src/services/resources/redux/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8f7fcabc47f606bb62be3218b6c4514ffbffb50
--- /dev/null
+++ b/frontend/src/services/resources/redux/index.ts
@@ -0,0 +1,4 @@
+export * from './actions';
+export { default as reducer } from './reducers';
+export { getResources } from './selectors';
+export * from './types';
diff --git a/frontend/src/services/resources/redux/reducers.ts b/frontend/src/services/resources/redux/reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..069898e3baa23dd74a54218b2109593aef4356d1
--- /dev/null
+++ b/frontend/src/services/resources/redux/reducers.ts
@@ -0,0 +1,19 @@
+import { ResourcesActionTypes } from './actions';
+
+const initialResourcesState: any = {
+  resources: {},
+};
+
+const resourcesReducer = (state: any = initialResourcesState, action: any) => {
+  switch (action.type) {
+    case ResourcesActionTypes.FETCH_RESOURCES:
+      return {
+        ...state,
+        resources: action.payload,
+      };
+    default:
+      return state;
+  }
+};
+
+export default resourcesReducer;
diff --git a/frontend/src/services/resources/redux/selectors.ts b/frontend/src/services/resources/redux/selectors.ts
new file mode 100644
index 0000000000000000000000000000000000000000..196bb950fa8409e8e69ea7f29dbdbee6acd866d4
--- /dev/null
+++ b/frontend/src/services/resources/redux/selectors.ts
@@ -0,0 +1,3 @@
+import { State } from 'src/redux';
+
+export const getResources = (state: State) => state.resources.resources;
diff --git a/frontend/src/services/resources/redux/types.ts b/frontend/src/services/resources/redux/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3ea8625048afdfece4e9a7680a8ee1f570558ece
--- /dev/null
+++ b/frontend/src/services/resources/redux/types.ts
@@ -0,0 +1,5 @@
+import { Resources } from '../types';
+
+export interface ResourcesState {
+  resources: Resources;
+}
diff --git a/frontend/src/services/resources/transformations.ts b/frontend/src/services/resources/transformations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..498167297f5d10268dddc7381d22aafb841ecbf0
--- /dev/null
+++ b/frontend/src/services/resources/transformations.ts
@@ -0,0 +1,11 @@
+import { Resources } from './types';
+
+export const transformResources = (response: any): Resources => {
+  return {
+    cpu: (response.cpu / response.cpu_total) * 100,
+    memory_used: (response.memory_total - response.memory_available) / 1e9,
+    memory_total: response.memory_total / 1e9,
+    disk_used: (response.disk_total - response.disk_free) / 1e9,
+    disk_total: response.disk_total / 1e9,
+  };
+};
diff --git a/frontend/src/services/resources/types.ts b/frontend/src/services/resources/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce5991cf55a2ac425ae52e9894351e605d30aab6
--- /dev/null
+++ b/frontend/src/services/resources/types.ts
@@ -0,0 +1,7 @@
+export interface Resources {
+  cpu: number;
+  memory_used: number;
+  memory_total: number;
+  disk_used: number;
+  disk_total: number;
+}