diff --git a/dbrepo-authentication-service/Dockerfile b/dbrepo-authentication-service/Dockerfile
index d5aae517441875eae082a8f1a4fffd5fe33dcf43..a44ea5df157b59f694ff1040cb3595ebd394faa6 100644
--- a/dbrepo-authentication-service/Dockerfile
+++ b/dbrepo-authentication-service/Dockerfile
@@ -5,13 +5,14 @@ MAINTAINER Martin Weise <martin.weise@tuwien.ac.at>
 # Enable health and metrics support
 ENV KC_HEALTH_ENABLED=true
 ENV KC_METRICS_ENABLED=true
-ENV KC_FEATURES=update-email
 
 # Configure a database vendor
 ENV KC_DB=mariadb
 
 WORKDIR /opt/keycloak
 
+COPY ./server.keystore ./conf/server.keystore
+
 RUN /opt/keycloak/bin/kc.sh build
 
 ###### SECOND STAGE ######
diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json
index e2fc0ac59053bfaf65b27253d44479db4b5aa4ef..040ad7b26115e80bacaf40a769c0f9876233f0ba 100644
--- a/dbrepo-authentication-service/dbrepo-realm.json
+++ b/dbrepo-authentication-service/dbrepo-realm.json
@@ -55,6 +55,17 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "9bb4a8dc-28e0-4645-b62f-cc94425f0cb0",
+      "name" : "default-maintenance-handling",
+      "description" : "${default-maintenance-handling}",
+      "composite" : true,
+      "composites" : {
+        "realm" : [ "create-maintenance-message", "find-maintenance-message", "update-maintenance-message", "delete-maintenance-message", "list-maintenance-messages" ]
+      },
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "5136d7a3-e3f0-4585-bacd-15cb8a56095c",
       "name" : "escalated-container-handling",
@@ -198,6 +209,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "f4116230-8642-4bb7-bbc8-db9c5c07b558",
+      "name" : "create-maintenance-message",
+      "description" : "${create-maintenance-message}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "973f0999-cc70-4b28-9f43-979c470bea8e",
       "name" : "default-data-steward-roles",
@@ -249,6 +268,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "272a79a7-e282-4261-8f7d-5d5d1364243a",
+      "name" : "update-maintenance-message",
+      "description" : "${update-maintenance-message}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "64c16bfb-2015-48ad-a23f-637ff24419cb",
       "name" : "default-query-handling",
@@ -268,6 +295,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "c047d521-cec3-4444-86c4-aef098489b7b",
+      "name" : "delete-maintenance-message",
+      "description" : "${delete-maintenance-message}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "e14ab76b-1c24-484d-ae2d-478b8457edea",
       "name" : "list-licenses",
@@ -435,7 +470,7 @@
       "description" : "${default-developer-roles}",
       "composite" : true,
       "composites" : {
-        "realm" : [ "escalated-query-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ]
+        "realm" : [ "escalated-query-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "default-maintenance-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ]
       },
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
@@ -504,6 +539,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "d6e38368-b40f-423b-82e4-e8aa595237c9",
+      "name" : "find-maintenance-message",
+      "description" : "${find-maintenance-message}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "fd1cc463-3e67-49d9-81b8-2cd90c1daa9c",
       "name" : "check-database-access",
@@ -539,6 +582,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "09f7bdb0-296f-46c8-a3a3-8f9254fb17e4",
+      "name" : "list-maintenance-messages",
+      "description" : "${list-maintenance-messages}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "fe3bc45c-61c2-4ece-bcaf-d410dc7de501",
       "name" : "update-database-access",
@@ -909,7 +960,7 @@
   "otpPolicyLookAheadWindow" : 1,
   "otpPolicyPeriod" : 30,
   "otpPolicyCodeReusable" : false,
-  "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName" ],
+  "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ],
   "webAuthnPolicyRpEntityName" : "keycloak",
   "webAuthnPolicySignatureAlgorithms" : [ "ES256" ],
   "webAuthnPolicyRpId" : "",
@@ -1932,7 +1983,7 @@
       "subType" : "authenticated",
       "subComponents" : { },
       "config" : {
-        "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ]
+        "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper" ]
       }
     }, {
       "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979",
@@ -1941,7 +1992,7 @@
       "subType" : "anonymous",
       "subComponents" : { },
       "config" : {
-        "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper" ]
+        "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper" ]
       }
     } ],
     "org.keycloak.keys.KeyProvider" : [ {
@@ -1993,7 +2044,7 @@
   "internationalizationEnabled" : false,
   "supportedLocales" : [ ],
   "authenticationFlows" : [ {
-    "id" : "7e7d6810-5b6c-4ec6-865c-5f0b62ec56d7",
+    "id" : "a50c392e-a870-484c-b7c4-7c85e886ee46",
     "alias" : "Account verification options",
     "description" : "Method with which to verity the existing account",
     "providerId" : "basic-flow",
@@ -2015,7 +2066,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "6d972ab3-0618-4971-b44a-0fc0d11c7280",
+    "id" : "3f384243-bf93-4535-8811-b23526f12963",
     "alias" : "Authentication Options",
     "description" : "Authentication options.",
     "providerId" : "basic-flow",
@@ -2044,7 +2095,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "821a14e0-ef26-4b07-b716-fa34393eda56",
+    "id" : "478c7d4f-86f2-42eb-91db-4bc03e20e416",
     "alias" : "Browser - Conditional OTP",
     "description" : "Flow to determine if the OTP is required for the authentication",
     "providerId" : "basic-flow",
@@ -2066,7 +2117,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "e70eadbd-4c39-4cfd-86ac-e50acc753b1b",
+    "id" : "cca43646-a74e-4439-b029-3c0a11cd3693",
     "alias" : "Direct Grant - Conditional OTP",
     "description" : "Flow to determine if the OTP is required for the authentication",
     "providerId" : "basic-flow",
@@ -2088,7 +2139,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "4e35af97-acf4-4ca8-bc81-0477c1adfb6d",
+    "id" : "815189fe-ef5f-4865-be03-1e4b5dd9eafe",
     "alias" : "First broker login - Conditional OTP",
     "description" : "Flow to determine if the OTP is required for the authentication",
     "providerId" : "basic-flow",
@@ -2110,7 +2161,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "2e0bd063-274a-4aab-a5f0-038a0bca5b98",
+    "id" : "b72709d3-246d-4057-a82f-1a6e721bdd0c",
     "alias" : "Handle Existing Account",
     "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
     "providerId" : "basic-flow",
@@ -2132,7 +2183,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "6a20fab2-44bb-4451-b29a-6fb7e14a52ce",
+    "id" : "191c4793-7fdd-449a-8218-70d047d882b7",
     "alias" : "Reset - Conditional OTP",
     "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
     "providerId" : "basic-flow",
@@ -2154,7 +2205,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "159d7398-74a7-4f60-a3fd-eb2df46f5ce7",
+    "id" : "afd9a108-753a-4012-a65f-69d149d8313a",
     "alias" : "User creation or linking",
     "description" : "Flow for the existing/non-existing user alternatives",
     "providerId" : "basic-flow",
@@ -2177,7 +2228,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "85a66c55-4665-4ba0-bec9-7254eb8e5895",
+    "id" : "95c1347f-8248-4506-8ac7-77da64331658",
     "alias" : "Verify Existing Account by Re-authentication",
     "description" : "Reauthentication of existing account",
     "providerId" : "basic-flow",
@@ -2199,7 +2250,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "c002e6da-2397-4fae-8d48-1eec3719ca15",
+    "id" : "00aa962b-69e7-4032-99d6-33d2e15689bf",
     "alias" : "browser",
     "description" : "browser based authentication",
     "providerId" : "basic-flow",
@@ -2235,7 +2286,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "a03631cf-2fea-4a12-a35c-8137023503bd",
+    "id" : "b7efe33a-b00a-4830-b362-739101e1a437",
     "alias" : "clients",
     "description" : "Base authentication for clients",
     "providerId" : "client-flow",
@@ -2271,7 +2322,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "a89940e4-bf4d-4a04-8fdf-dcf775336b20",
+    "id" : "39a89fb2-74b0-44bb-877c-a2f75044f5e1",
     "alias" : "direct grant",
     "description" : "OpenID Connect Resource Owner Grant",
     "providerId" : "basic-flow",
@@ -2300,7 +2351,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "2dc2582b-be6f-4d9a-b545-b2c0e79a3581",
+    "id" : "c0b9af60-9bb2-42e2-815d-c03144b4773c",
     "alias" : "docker auth",
     "description" : "Used by Docker clients to authenticate against the IDP",
     "providerId" : "basic-flow",
@@ -2315,7 +2366,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "09e56692-226f-4384-85e0-e33463cdb226",
+    "id" : "50e548b3-60a1-4511-838a-894462d7437a",
     "alias" : "first broker login",
     "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
     "providerId" : "basic-flow",
@@ -2338,7 +2389,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "1439c900-92e0-4230-a1a7-ae82c3b8ddc9",
+    "id" : "260eb8b4-8889-41e4-b3d4-ce8adf15474c",
     "alias" : "forms",
     "description" : "Username, password, otp and other auth forms.",
     "providerId" : "basic-flow",
@@ -2360,7 +2411,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "4cc3bb1b-e85d-447e-b50e-1afbe107bafe",
+    "id" : "d7db5234-08e2-44ed-8708-5f4e906fe8a5",
     "alias" : "http challenge",
     "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
     "providerId" : "basic-flow",
@@ -2382,7 +2433,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "04c49d80-30e4-4a37-b1c7-4d18c1b6a7f1",
+    "id" : "73ada652-053b-42f3-be1f-af907cdd3151",
     "alias" : "registration",
     "description" : "registration flow",
     "providerId" : "basic-flow",
@@ -2398,7 +2449,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "85abb75a-0774-4b2d-8a71-2a92b0cfb639",
+    "id" : "ef0d04cc-a30d-4cdc-8e62-26075e256f41",
     "alias" : "registration form",
     "description" : "registration form",
     "providerId" : "form-flow",
@@ -2434,7 +2485,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "948f68c1-015b-4349-a56f-6ee177d558ce",
+    "id" : "c6db550e-4542-4474-8e3d-955fb50b1a05",
     "alias" : "reset credentials",
     "description" : "Reset credentials for a user if they forgot their password or something",
     "providerId" : "basic-flow",
@@ -2470,7 +2521,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "6046d416-4a88-4af6-b440-9fbc87fba478",
+    "id" : "9ba81baf-f271-44df-b712-1a58af78c27c",
     "alias" : "saml ecp",
     "description" : "SAML ECP Profile Authentication Flow",
     "providerId" : "basic-flow",
@@ -2486,13 +2537,13 @@
     } ]
   } ],
   "authenticatorConfig" : [ {
-    "id" : "3c91aefc-127f-4722-8375-72e8434d6266",
+    "id" : "d70d482f-3f61-4514-918f-9634c6dc7200",
     "alias" : "create unique user config",
     "config" : {
       "require.password.update.after.registration" : "false"
     }
   }, {
-    "id" : "1041c583-4682-44a8-b61b-9712bd4987c4",
+    "id" : "b5e8a2b3-a407-42f4-bc5d-c82c2c77c8d1",
     "alias" : "review profile config",
     "config" : {
       "update.profile.on.first.login" : "missing"
diff --git a/dbrepo-authentication-service/generate-keystore.sh b/dbrepo-authentication-service/generate-keystore.sh
new file mode 100644
index 0000000000000000000000000000000000000000..8b68c44a1febcac8c308a8c443a4a41f5ea21d2f
--- /dev/null
+++ b/dbrepo-authentication-service/generate-keystore.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+keytool -genkey -alias server -keyalg RSA -keypass password -storepass password -keystore server.keystore
\ No newline at end of file
diff --git a/dbrepo-authentication-service/server.keystore b/dbrepo-authentication-service/server.keystore
new file mode 100644
index 0000000000000000000000000000000000000000..9dcd5051210b5bd2f945a8325610684c8e0029a8
Binary files /dev/null and b/dbrepo-authentication-service/server.keystore differ
diff --git a/dbrepo-container-service/pom.xml b/dbrepo-container-service/pom.xml
index 286be07560852c79f76480948c12b3e0bd97b218..e780d3f864a797d6915f27cae7bfbb714b0ef321 100644
--- a/dbrepo-container-service/pom.xml
+++ b/dbrepo-container-service/pom.xml
@@ -188,6 +188,7 @@
                         <exclude>at/tuwien/exception/**/*</exclude>
                         <exclude>at/tuwien/config/**/*</exclude>
                         <exclude>at/tuwien/handlers/**/*</exclude>
+                        <exclude>at/tuwien/auth/**/*</exclude>
                         <exclude>**/DbrepoContainerManagingApplication.class</exclude>
                     </excludes>
                 </configuration>
diff --git a/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java b/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java
index 8f69d140c8386a5f0f6ca1df4b392c5610b7c98e..2619f4537a9dfdcc3d5561b50bff63a94eff5d96 100644
--- a/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java
+++ b/dbrepo-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java
@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.*;
 
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotNull;
+
 import java.security.Principal;
 import java.util.List;
 import java.util.Optional;
@@ -152,12 +153,18 @@ public class ContainerEndpoint {
                             mediaType = "application/json",
                             schema = @Schema(implementation = ApiErrorDto.class))}),
     })
-    public ResponseEntity<ContainerDto> findById(@NotNull @PathVariable("id") Long containerId) throws DockerClientException,
-            ContainerNotFoundException, ContainerNotRunningException {
+    public ResponseEntity<ContainerDto> findById(@NotNull @PathVariable("id") Long containerId)
+            throws DockerClientException, ContainerNotFoundException {
         log.debug("endpoint find container, id={}", containerId);
-        final Container container = containerService.inspect(containerId);
-        final ContainerDto dto = containerMapper.containerToContainerDto(container);
-        dto.setState(ContainerStateDto.RUNNING);
+        ContainerDto dto;
+        try {
+            dto = containerService.inspect(containerId);
+        } catch (ContainerNotRunningException e) {
+            /* ignore */
+            dto = containerMapper.containerToContainerDto(containerService.find(containerId));
+            dto.setRunning(false);
+            dto.setState(ContainerStateDto.EXITED);
+        }
         log.trace("find container resulted in container {}", dto);
         return ResponseEntity.ok()
                 .body(dto);
diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java
index c49b7ea08ee1de453788526a3017c11816cdd277..328f77a6fc2ad0dbdc8a74336e908725e4b53e17 100644
--- a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java
+++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointIntegrationTest.java
@@ -2,13 +2,19 @@ package at.tuwien.endpoint;
 
 import at.tuwien.BaseUnitTest;
 import at.tuwien.api.container.*;
+import at.tuwien.config.DockerConfig;
 import at.tuwien.config.ReadyConfig;
 import at.tuwien.endpoints.ContainerEndpoint;
 import at.tuwien.entities.container.Container;
 import at.tuwien.exception.*;
+import at.tuwien.repository.jpa.ContainerRepository;
+import at.tuwien.repository.jpa.ImageRepository;
+import at.tuwien.repository.jpa.RealmRepository;
 import at.tuwien.repository.jpa.UserRepository;
 import at.tuwien.service.impl.ContainerServiceImpl;
 import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,7 +32,8 @@ import java.util.List;
 import java.util.Optional;
 
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
 
 @Log4j2
 @ExtendWith(SpringExtension.class)
@@ -36,330 +43,90 @@ public class ContainerEndpointIntegrationTest extends BaseUnitTest {
     @MockBean
     private ReadyConfig readyConfig;
 
-    @MockBean
-    private ContainerServiceImpl containerService;
-
-    @MockBean
-    private UserRepository userRepository;
-
     @Autowired
-    private ContainerEndpoint containerEndpoint;
-
-    @Test
-    @WithAnonymousUser
-    public void findById_anonymous_succeeds() throws DockerClientException, ContainerNotFoundException,
-            ContainerNotRunningException {
-
-        /* test */
-        findById_generic(CONTAINER_1_ID, CONTAINER_1);
-    }
-
-    @Test
-    @WithMockUser(username = USER_1_USERNAME, authorities = {"find-container"})
-    public void findById_hasRole_succeeds() throws DockerClientException, ContainerNotFoundException,
-            ContainerNotRunningException {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_1_USERNAME))
-                .thenReturn(Optional.of(USER_1));
-
-        /* test */
-        findById_generic(CONTAINER_1_ID, CONTAINER_1);
-    }
-
-    @Test
-    @WithMockUser(username = USER_4_USERNAME)
-    public void findById_noRole_succeeds() throws DockerClientException, ContainerNotFoundException,
-            ContainerNotRunningException {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_4_USERNAME))
-                .thenReturn(Optional.of(USER_4));
-
-        /* test */
-        findById_generic(CONTAINER_1_ID, CONTAINER_1);
-    }
-
-    @Test
-    @WithAnonymousUser
-    public void delete_anonymous_fails() {
-
-        /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            delete_generic(CONTAINER_1_ID, CONTAINER_1, null);
-        });
-    }
-
-    @Test
-    @WithMockUser(username = USER_3_USERNAME)
-    public void delete_noRole_fails() {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_4_USERNAME))
-                .thenReturn(Optional.of(USER_4));
+    private RealmRepository realmRepository;
 
-        /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL);
-        });
-    }
-
-    @Test
-    @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-container"})
-    public void delete_hasRole_succeeds() throws ContainerStillRunningException, ContainerAlreadyRemovedException,
-            ContainerNotFoundException, DockerClientException {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_2_USERNAME))
-                .thenReturn(Optional.of(USER_2));
-
-        /* test */
-        delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL);
-    }
-
-    @Test
-    @WithAnonymousUser
-    public void findAll_anonymous_succeeds() {
-
-        /* test */
-        findAll_generic(null, null);
-    }
-
-    @Test
-    @WithMockUser(username = USER_1_USERNAME, authorities = {"find-containers"})
-    public void findAll_hasRole_succeeds() {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_1_USERNAME))
-                .thenReturn(Optional.of(USER_1));
-
-        /* test */
-        findAll_generic(USER_1_PRINCIPAL, null);
-    }
-
-    @Test
-    @WithMockUser(username = USER_4_USERNAME)
-    public void findAll_noRole_succeeds() {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_4_USERNAME))
-                .thenReturn(Optional.of(USER_4));
-
-        /* test */
-        findAll_generic(USER_4_PRINCIPAL, null);
-    }
-
-    @Test
-    @WithAnonymousUser
-    public void create_anonymous_fails() {
-        final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder()
-                .name(CONTAINER_1_NAME)
-                .repository(IMAGE_1_REPOSITORY)
-                .tag(IMAGE_1_TAG)
-                .build();
+    @Autowired
+    private ImageRepository imageRepository;
 
-        /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            create_generic(request, null);
-        });
-    }
+    @Autowired
+    private ContainerRepository containerRepository;
 
-    @Test
-    @WithMockUser(username = USER_1_USERNAME, authorities = {"create-container"})
-    public void create_hasRole_succeeds() throws UserNotFoundException, DockerClientException,
-            ContainerAlreadyExistsException, ImageNotFoundException {
-        final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder()
-                .name(CONTAINER_1_NAME)
-                .repository(IMAGE_1_REPOSITORY)
-                .tag(IMAGE_1_TAG)
-                .build();
+    @Autowired
+    private UserRepository userRepository;
 
-        /* mock */
-        when(userRepository.findByUsername(USER_1_USERNAME))
-                .thenReturn(Optional.of(USER_1));
+    @Autowired
+    private ContainerEndpoint containerEndpoint;
 
-        /* test */
-        create_generic(request, USER_1_PRINCIPAL);
+    @BeforeEach
+    public void beforeEach() {
+        afterEach();
+        /* networks */
+        DockerConfig.createAllNetworks();
+        /* metadata database */
+        realmRepository.save(REALM_DBREPO);
+        userRepository.save(USER_1_SIMPLE);
+        userRepository.save(USER_2_SIMPLE);
+        userRepository.save(USER_3_SIMPLE);
+        imageRepository.save(IMAGE_1);
     }
 
-    @Test
-    @WithMockUser(username = USER_4_USERNAME)
-    public void create_noRole_fails() {
-        final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder()
-                .name(CONTAINER_1_NAME)
-                .repository(IMAGE_1_REPOSITORY)
-                .tag(IMAGE_1_TAG)
-                .build();
-
-        /* mock */
-        when(userRepository.findByUsername(USER_4_USERNAME))
-                .thenReturn(Optional.of(USER_4));
-
-        /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            create_generic(request, USER_4_PRINCIPAL);
-        });
+    @AfterEach
+    public void afterEach() {
+        DockerConfig.removeAllContainers();
+        DockerConfig.removeAllNetworks();
     }
 
     @Test
     @WithAnonymousUser
-    public void modify_anonymous_fails() {
-
-        /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, null);
-        });
-    }
-
-    @Test
-    @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-container-state"})
-    public void modify_hasRole_succeeds() throws ContainerAlreadyRunningException,
-            ContainerAlreadyStoppedException, ContainerNotFoundException, UserNotFoundException, NotAllowedException,
-            DockerClientException {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_1_USERNAME))
-                .thenReturn(Optional.of(USER_1));
-
-        /* test */
-        modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_1_PRINCIPAL);
-    }
-
-    @Test
-    @WithMockUser(username = USER_4_USERNAME)
-    public void modify_noRole_fails() {
+    public void findAll_anonymousNoLimit_succeeds() throws InterruptedException {
 
         /* mock */
-        when(userRepository.findByUsername(USER_4_USERNAME))
-                .thenReturn(Optional.of(USER_4));
+        DockerConfig.createContainer(null, CONTAINER_1_SIMPLE, CONTAINER_1_ENV);
+        DockerConfig.startContainer(CONTAINER_1_SIMPLE);
+        containerRepository.save(CONTAINER_1_SIMPLE);
 
         /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL);
-        });
-    }
-
-    @Test
-    @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-foreign-container-state"})
-    public void modify_hasRoleForeign_succeeds() throws UserNotFoundException, ContainerAlreadyRunningException,
-            NotAllowedException, ContainerAlreadyStoppedException, ContainerNotFoundException, DockerClientException {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_2_USERNAME))
-                .thenReturn(Optional.of(USER_2));
-
-        /* test */
-        modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL);
-    }
-
-    @Test
-    @WithMockUser(username = USER_4_USERNAME)
-    public void modify_noRoleForeign_fails() {
-
-        /* mock */
-        when(userRepository.findByUsername(USER_4_USERNAME))
-                .thenReturn(Optional.of(USER_4));
-
-        /* test */
-        assertThrows(AccessDeniedException.class, () -> {
-            modify_generic(ContainerActionTypeDto.STOP, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL);
-        });
-    }
-
-    /* ################################################################################################### */
-    /* ## GENERIC TEST CASES                                                                            ## */
-    /* ################################################################################################### */
-
-    public void findById_generic(Long containerId, Container container) throws DockerClientException,
-            ContainerNotFoundException, ContainerNotRunningException {
-
-        /* mock */
-        when(containerService.find(containerId))
-                .thenReturn(container);
-        when(containerService.inspect(containerId))
-                .thenReturn(container);
-
-        /* test */
-        final ResponseEntity<ContainerDto> response = containerEndpoint.findById(containerId);
-        assertEquals(HttpStatus.OK, response.getStatusCode());
-        assertNotNull(response.getBody());
-        final ContainerDto dto = response.getBody();
-        assertEquals(ContainerStateDto.RUNNING, dto.getState());
-    }
-
-    public void delete_generic(Long containerId, Container container, Principal principal) throws ContainerNotFoundException,
-            ContainerStillRunningException, ContainerAlreadyRemovedException, DockerClientException {
-
-        /* mock */
-        when(containerService.find(containerId))
-                .thenReturn(container);
-        doNothing()
-                .when(containerService)
-                .remove(CONTAINER_1_ID);
-
-        /* test */
-        final ResponseEntity<?> response = containerEndpoint.delete(containerId, principal);
-        assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
-        assertNull(response.getBody());
-    }
-
-    public void findAll_generic(Principal principal, Integer limit) {
-
-        /* mock */
-        when(containerService.getAll(limit))
-                .thenReturn(List.of(CONTAINER_1, CONTAINER_2));
-
-        /* test */
-        final ResponseEntity<List<ContainerBriefDto>> response = containerEndpoint.findAll(principal, limit);
+        final ResponseEntity<List<ContainerBriefDto>> response = containerEndpoint.findAll(null, null);
         assertEquals(HttpStatus.OK, response.getStatusCode());
         assertNotNull(response.getBody());
         final List<ContainerBriefDto> body = response.getBody();
-        assertEquals(2, body.size());
-        final ContainerBriefDto container1 = body.get(0);
-        assertEquals(CONTAINER_1_ID, container1.getId());
-        assertEquals(CONTAINER_1_NAME, container1.getName());
-        assertEquals(CONTAINER_1_INTERNALNAME, container1.getInternalName());
-        final ContainerBriefDto container2 = body.get(1);
-        assertEquals(CONTAINER_2_ID, container2.getId());
-        assertEquals(CONTAINER_2_NAME, container2.getName());
-        assertEquals(CONTAINER_2_INTERNALNAME, container2.getInternalName());
+        assertEquals(1, body.size());
+        final ContainerBriefDto container0 = body.get(0);
+        assertTrue(container0.getRunning());
     }
 
-    public void create_generic(ContainerCreateRequestDto data, Principal principal) throws UserNotFoundException,
-            DockerClientException, ContainerAlreadyExistsException, ImageNotFoundException {
+    @Test
+    @WithAnonymousUser
+    public void findById_anonymousNotRunning_succeeds() throws DockerClientException, ContainerNotFoundException {
 
         /* mock */
-        when(containerService.create(data, principal))
-                .thenReturn(CONTAINER_1);
+        DockerConfig.createContainer(null, CONTAINER_1_SIMPLE, CONTAINER_1_ENV);
+        containerRepository.save(CONTAINER_1_SIMPLE);
 
         /* test */
-        final ResponseEntity<ContainerBriefDto> response = containerEndpoint.create(data, principal);
-        assertEquals(HttpStatus.CREATED, response.getStatusCode());
+        final ResponseEntity<ContainerDto> response = containerEndpoint.findById(CONTAINER_1_ID);
+        assertEquals(HttpStatus.OK, response.getStatusCode());
         assertNotNull(response.getBody());
+        final ContainerDto body = response.getBody();
+        assertFalse(body.getRunning());
+        assertEquals(ContainerStateDto.EXITED, body.getState());
     }
 
-    public void modify_generic(ContainerActionTypeDto data, Long containerId, Container container, Principal principal)
-            throws ContainerAlreadyRunningException, ContainerNotFoundException, ContainerAlreadyStoppedException,
-            UserNotFoundException, NotAllowedException, DockerClientException {
+    @Test
+    @WithMockUser(username = USER_3_USERNAME, authorities = {"modify-container-state"})
+    public void modify_foreign_fails() {
         final ContainerChangeDto request = ContainerChangeDto.builder()
-                .action(data)
+                .action(ContainerActionTypeDto.STOP)
                 .build();
 
         /* mock */
-        when(containerService.find(containerId))
-                .thenReturn(container);
-        if (data.equals(ContainerActionTypeDto.START)) {
-            when(containerService.start(containerId))
-                    .thenReturn(container);
-        } else if (data.equals(ContainerActionTypeDto.STOP)) {
-            when(containerService.stop(containerId))
-                    .thenReturn(container);
-        }
+        containerRepository.save(CONTAINER_1_SIMPLE);
 
         /* test */
-        final ResponseEntity<ContainerBriefDto> response = containerEndpoint.modify(containerId, request, principal);
-        assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
-        assertNotNull(response.getBody());
+        assertThrows(NotAllowedException.class, () -> {
+            containerEndpoint.modify(CONTAINER_1_ID, request, USER_3_PRINCIPAL);
+        });
     }
 
 }
diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointUnitTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointUnitTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..921fc436a34e0b74e6199a76419471a6f7233d3f
--- /dev/null
+++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ContainerEndpointUnitTest.java
@@ -0,0 +1,364 @@
+package at.tuwien.endpoint;
+
+import at.tuwien.BaseUnitTest;
+import at.tuwien.api.container.*;
+import at.tuwien.config.ReadyConfig;
+import at.tuwien.endpoints.ContainerEndpoint;
+import at.tuwien.entities.container.Container;
+import at.tuwien.exception.*;
+import at.tuwien.repository.jpa.UserRepository;
+import at.tuwien.service.impl.ContainerServiceImpl;
+import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@Log4j2
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+public class ContainerEndpointUnitTest extends BaseUnitTest {
+
+    @MockBean
+    private ReadyConfig readyConfig;
+
+    @MockBean
+    private ContainerServiceImpl containerService;
+
+    @MockBean
+    private UserRepository userRepository;
+
+    @Autowired
+    private ContainerEndpoint containerEndpoint;
+
+    @Test
+    @WithAnonymousUser
+    public void findById_anonymous_succeeds() throws DockerClientException, ContainerNotFoundException,
+            ContainerNotRunningException {
+
+        /* test */
+        findById_generic(CONTAINER_1_ID, CONTAINER_1_HASH, CONTAINER_1, CONTAINER_1_DTO);
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"find-container"})
+    public void findById_hasRole_succeeds() throws DockerClientException, ContainerNotFoundException,
+            ContainerNotRunningException {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_1_USERNAME))
+                .thenReturn(Optional.of(USER_1));
+
+        /* test */
+        findById_generic(CONTAINER_1_ID, CONTAINER_1_HASH, CONTAINER_1, CONTAINER_1_DTO);
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void findById_noRole_succeeds() throws DockerClientException, ContainerNotFoundException,
+            ContainerNotRunningException {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_4_USERNAME))
+                .thenReturn(Optional.of(USER_4));
+
+        /* test */
+        findById_generic(CONTAINER_1_ID, CONTAINER_1_HASH, CONTAINER_1, CONTAINER_1_DTO);
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void delete_anonymous_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            delete_generic(CONTAINER_1_ID, CONTAINER_1, null);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_3_USERNAME)
+    public void delete_noRole_fails() {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_4_USERNAME))
+                .thenReturn(Optional.of(USER_4));
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-container"})
+    public void delete_hasRole_succeeds() throws ContainerStillRunningException, ContainerAlreadyRemovedException,
+            ContainerNotFoundException {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_2_USERNAME))
+                .thenReturn(Optional.of(USER_2));
+
+        /* test */
+        delete_generic(CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL);
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void findAll_anonymous_succeeds() {
+
+        /* test */
+        findAll_generic(null, null);
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"find-containers"})
+    public void findAll_hasRole_succeeds() {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_1_USERNAME))
+                .thenReturn(Optional.of(USER_1));
+
+        /* test */
+        findAll_generic(USER_1_PRINCIPAL, null);
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void findAll_noRole_succeeds() {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_4_USERNAME))
+                .thenReturn(Optional.of(USER_4));
+
+        /* test */
+        findAll_generic(USER_4_PRINCIPAL, null);
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void create_anonymous_fails() {
+        final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder()
+                .name(CONTAINER_1_NAME)
+                .repository(IMAGE_1_REPOSITORY)
+                .tag(IMAGE_1_TAG)
+                .build();
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            create_generic(request, null);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"create-container"})
+    public void create_hasRole_succeeds() throws UserNotFoundException, DockerClientException,
+            ContainerAlreadyExistsException, ImageNotFoundException {
+        final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder()
+                .name(CONTAINER_1_NAME)
+                .repository(IMAGE_1_REPOSITORY)
+                .tag(IMAGE_1_TAG)
+                .build();
+
+        /* mock */
+        when(userRepository.findByUsername(USER_1_USERNAME))
+                .thenReturn(Optional.of(USER_1));
+
+        /* test */
+        create_generic(request, USER_1_PRINCIPAL);
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void create_noRole_fails() {
+        final ContainerCreateRequestDto request = ContainerCreateRequestDto.builder()
+                .name(CONTAINER_1_NAME)
+                .repository(IMAGE_1_REPOSITORY)
+                .tag(IMAGE_1_TAG)
+                .build();
+
+        /* mock */
+        when(userRepository.findByUsername(USER_4_USERNAME))
+                .thenReturn(Optional.of(USER_4));
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            create_generic(request, USER_4_PRINCIPAL);
+        });
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void modify_anonymous_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, null);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-container-state"})
+    public void modify_hasRole_succeeds() throws ContainerAlreadyRunningException,
+            ContainerAlreadyStoppedException, ContainerNotFoundException, UserNotFoundException, NotAllowedException {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_1_USERNAME))
+                .thenReturn(Optional.of(USER_1));
+
+        /* test */
+        modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_1_PRINCIPAL);
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void modify_noRole_fails() {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_4_USERNAME))
+                .thenReturn(Optional.of(USER_4));
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-foreign-container-state"})
+    public void modify_hasRoleForeign_succeeds() throws UserNotFoundException, ContainerAlreadyRunningException,
+            NotAllowedException, ContainerAlreadyStoppedException, ContainerNotFoundException {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_2_USERNAME))
+                .thenReturn(Optional.of(USER_2));
+
+        /* test */
+        modify_generic(ContainerActionTypeDto.START, CONTAINER_1_ID, CONTAINER_1, USER_2_PRINCIPAL);
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void modify_noRoleForeign_fails() {
+
+        /* mock */
+        when(userRepository.findByUsername(USER_4_USERNAME))
+                .thenReturn(Optional.of(USER_4));
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            modify_generic(ContainerActionTypeDto.STOP, CONTAINER_1_ID, CONTAINER_1, USER_4_PRINCIPAL);
+        });
+    }
+
+    /* ################################################################################################### */
+    /* ## GENERIC TEST CASES                                                                            ## */
+    /* ################################################################################################### */
+
+    public void findById_generic(Long containerId, String containerHash, Container container, ContainerDto containerDto)
+            throws DockerClientException, ContainerNotFoundException, ContainerNotRunningException {
+
+        /* mock */
+        when(containerService.find(containerId))
+                .thenReturn(container);
+        when(containerService.inspect(containerId))
+                .thenReturn(containerDto);
+
+        /* test */
+        final ResponseEntity<ContainerDto> response = containerEndpoint.findById(containerId);
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertNotNull(response.getBody());
+        final ContainerDto dto = response.getBody();
+        assertEquals(ContainerStateDto.RUNNING, dto.getState());
+    }
+
+    public void delete_generic(Long containerId, Container container, Principal principal) throws ContainerNotFoundException,
+            ContainerStillRunningException, ContainerAlreadyRemovedException {
+
+        /* mock */
+        when(containerService.find(containerId))
+                .thenReturn(container);
+        doNothing()
+                .when(containerService)
+                .remove(CONTAINER_1_ID);
+
+        /* test */
+        final ResponseEntity<?> response = containerEndpoint.delete(containerId, principal);
+        assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
+        assertNull(response.getBody());
+    }
+
+    public void findAll_generic(Principal principal, Integer limit) {
+
+        /* mock */
+        when(containerService.getAll(limit))
+                .thenReturn(List.of(CONTAINER_1, CONTAINER_2));
+
+        /* test */
+        final ResponseEntity<List<ContainerBriefDto>> response = containerEndpoint.findAll(principal, limit);
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertNotNull(response.getBody());
+        final List<ContainerBriefDto> body = response.getBody();
+        assertEquals(2, body.size());
+        final ContainerBriefDto container1 = body.get(0);
+        assertEquals(CONTAINER_1_ID, container1.getId());
+        assertEquals(CONTAINER_1_NAME, container1.getName());
+        assertEquals(CONTAINER_1_INTERNALNAME, container1.getInternalName());
+        final ContainerBriefDto container2 = body.get(1);
+        assertEquals(CONTAINER_2_ID, container2.getId());
+        assertEquals(CONTAINER_2_NAME, container2.getName());
+        assertEquals(CONTAINER_2_INTERNALNAME, container2.getInternalName());
+    }
+
+    public void create_generic(ContainerCreateRequestDto data, Principal principal) throws UserNotFoundException,
+            DockerClientException, ContainerAlreadyExistsException, ImageNotFoundException {
+
+        /* mock */
+        when(containerService.create(data, principal))
+                .thenReturn(CONTAINER_1);
+
+        /* test */
+        final ResponseEntity<ContainerBriefDto> response = containerEndpoint.create(data, principal);
+        assertEquals(HttpStatus.CREATED, response.getStatusCode());
+        assertNotNull(response.getBody());
+    }
+
+    public void modify_generic(ContainerActionTypeDto data, Long containerId, Container container, Principal principal)
+            throws ContainerAlreadyRunningException, ContainerNotFoundException, ContainerAlreadyStoppedException,
+            UserNotFoundException, NotAllowedException {
+        final ContainerChangeDto request = ContainerChangeDto.builder()
+                .action(data)
+                .build();
+
+        /* mock */
+        when(containerService.find(containerId))
+                .thenReturn(container);
+        if (data.equals(ContainerActionTypeDto.START)) {
+            when(containerService.start(containerId))
+                    .thenReturn(container);
+        } else if (data.equals(ContainerActionTypeDto.STOP)) {
+            when(containerService.stop(containerId))
+                    .thenReturn(container);
+        }
+
+        /* test */
+        final ResponseEntity<ContainerBriefDto> response = containerEndpoint.modify(containerId, request, principal);
+        assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
+        assertNotNull(response.getBody());
+    }
+
+}
diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ImageEndpointIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ImageEndpointIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..689ac6ea555c3d00418c055ad72307b51653a552
--- /dev/null
+++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/endpoint/ImageEndpointIntegrationTest.java
@@ -0,0 +1,83 @@
+package at.tuwien.endpoint;
+
+import at.tuwien.BaseUnitTest;
+import at.tuwien.api.container.image.ImageBriefDto;
+import at.tuwien.api.container.image.ImageChangeDto;
+import at.tuwien.api.container.image.ImageCreateDto;
+import at.tuwien.api.container.image.ImageDto;
+import at.tuwien.config.DockerConfig;
+import at.tuwien.config.DockerDaemonConfig;
+import at.tuwien.config.ReadyConfig;
+import at.tuwien.endpoints.ImageEndpoint;
+import at.tuwien.entities.container.image.ContainerImage;
+import at.tuwien.exception.*;
+import at.tuwien.repository.jpa.ImageRepository;
+import at.tuwien.repository.jpa.RealmRepository;
+import at.tuwien.repository.jpa.UserRepository;
+import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.security.Principal;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+@Log4j2
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+public class ImageEndpointIntegrationTest extends BaseUnitTest {
+
+    @MockBean
+    private ReadyConfig readyConfig;
+
+    @MockBean
+    private RealmRepository realmRepository;
+
+    @MockBean
+    private UserRepository userRepository;
+
+    @Autowired
+    private ImageEndpoint imageEndpoint;
+
+    @BeforeEach
+    public void beforeEach() {
+        afterEach();
+        /* networks */
+        DockerConfig.createAllNetworks();
+        /* metadata database */
+        realmRepository.save(REALM_DBREPO);
+        userRepository.save(USER_2_SIMPLE);
+    }
+
+    @AfterEach
+    public void afterEach() {
+        DockerConfig.removeAllContainers();
+        DockerConfig.removeAllNetworks();
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"create-image"})
+    public void create_succeeds() throws UserNotFoundException, ImageAlreadyExistsException, DockerClientException,
+            ImageNotFoundException, ImageInvalidException {
+
+
+        /* test */
+        imageEndpoint.create(IMAGE_1_CREATE_DTO, USER_2_PRINCIPAL);
+    }
+
+}
diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java
index 88e2943220db0ad616e7534e2658e4c22744756b..35765c567f3a2d55a32a16df1f3b414f030a7978 100644
--- a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java
+++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/ContainerServiceIntegrationTest.java
@@ -2,6 +2,7 @@ package at.tuwien.service;
 
 import at.tuwien.BaseUnitTest;
 import at.tuwien.api.container.ContainerCreateRequestDto;
+import at.tuwien.api.container.ContainerDto;
 import at.tuwien.config.DockerConfig;
 import at.tuwien.config.ReadyConfig;
 import at.tuwien.entities.container.Container;
@@ -318,7 +319,7 @@ public class ContainerServiceIntegrationTest extends BaseUnitTest {
         containerRepository.save(CONTAINER_1_SIMPLE);
 
         /* test */
-        final Container response = containerService.inspect(CONTAINER_1_ID);
+        final ContainerDto response = containerService.inspect(CONTAINER_1_ID);
         assertEquals(CONTAINER_1_ID, response.getId());
         assertEquals(CONTAINER_1_NAME, response.getName());
         assertEquals(CONTAINER_1_INTERNALNAME, response.getInternalName());
@@ -346,4 +347,19 @@ public class ContainerServiceIntegrationTest extends BaseUnitTest {
             containerService.inspect(CONTAINER_1_ID);
         });
     }
+
+    @Test
+    public void list_notRunning_succeeds() throws InterruptedException {
+
+        /* mock */
+        DockerConfig.createContainer(null, CONTAINER_1_SIMPLE, CONTAINER_1_ENV);
+        DockerConfig.createContainer(null, CONTAINER_2_SIMPLE, CONTAINER_2_ENV);
+        DockerConfig.startContainer(CONTAINER_2_SIMPLE);
+        containerRepository.save(CONTAINER_1_SIMPLE);
+        containerRepository.save(CONTAINER_2_SIMPLE);
+
+        /* test */
+        final List<com.github.dockerjava.api.model.Container> response = containerService.list();
+        assertEquals(2, response.size());
+    }
 }
diff --git a/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1a06e35c2e8fec2b8de56c2f56c10d7de307308
--- /dev/null
+++ b/dbrepo-container-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java
@@ -0,0 +1,64 @@
+package at.tuwien.service;
+
+import at.tuwien.BaseUnitTest;
+import at.tuwien.config.ReadyConfig;
+import at.tuwien.entities.user.User;
+import at.tuwien.exception.UserNotFoundException;
+import at.tuwien.repository.jpa.RealmRepository;
+import at.tuwien.repository.jpa.UserRepository;
+import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@Log4j2
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+@SpringBootTest
+@ExtendWith(SpringExtension.class)
+public class UserServiceIntegrationTest extends BaseUnitTest {
+
+    @MockBean
+    private ReadyConfig readyConfig;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private RealmRepository realmRepository;
+
+    @Autowired
+    private UserService userService;
+
+    @BeforeEach
+    public void beforeEach() {
+        realmRepository.save(REALM_DBREPO);
+        userRepository.save(USER_1_SIMPLE);
+    }
+
+    @Test
+    public void findByUsername_succeeds() throws UserNotFoundException {
+
+        /* test */
+        final User response = userService.findByUsername(USER_1_USERNAME);
+        assertEquals(USER_1_ID, response.getId());
+        assertEquals(USER_1_USERNAME, response.getUsername());
+    }
+
+    @Test
+    public void findByUsername_fails() {
+
+        /* test */
+        assertThrows(UserNotFoundException.class, () -> {
+            userService.findByUsername(USER_2_USERNAME);
+        });
+    }
+
+}
diff --git a/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java b/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java
index badd04c8db0c8a4a6370a5312c0aa6dbde546a57..9f1cee243b03fa059059836691f52b0332702d48 100644
--- a/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java
+++ b/dbrepo-container-service/services/src/main/java/at/tuwien/service/ContainerService.java
@@ -1,8 +1,10 @@
 package at.tuwien.service;
 
 import at.tuwien.api.container.ContainerCreateRequestDto;
+import at.tuwien.api.container.ContainerDto;
 import at.tuwien.entities.container.Container;
 import at.tuwien.exception.*;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.security.Principal;
 import java.util.List;
@@ -10,49 +12,58 @@ import java.util.List;
 public interface ContainerService {
 
     /**
-     * @param createDto
-     * @param principal
-     * @return
-     * @throws ImageNotFoundException
-     * @throws DockerClientException
-     * @throws ContainerAlreadyExistsException
-     * @throws UserNotFoundException
+     * Creates a container.
+     *
+     * @param createDto The container metadata.
+     * @param principal The principal of the creating user.
+     * @return The container object, if successful.
+     * @throws ImageNotFoundException          The image of the container was not found in the metadata database.
+     * @throws DockerClientException           The docker client was unable to perform this action.
+     * @throws ContainerAlreadyExistsException A container with this name already exists.
+     * @throws UserNotFoundException           The user creating the container was not found in the metadata database.
      */
     Container create(ContainerCreateRequestDto createDto, Principal principal) throws ImageNotFoundException,
             DockerClientException, ContainerAlreadyExistsException, UserNotFoundException;
 
     /**
-     * @param containerId
-     * @return
-     * @throws ContainerNotFoundException
-     * @throws DockerClientException
+     * Stops a container by given id from the metadata database.
+     *
+     * @param containerId The container id.
+     * @return The container object, if successful.
+     * @throws ContainerNotFoundException The container was not found in the metadata database.
+     * @throws DockerClientException      The docker client was unable to perform this action.
      */
     Container stop(Long containerId) throws ContainerNotFoundException, DockerClientException, ContainerAlreadyStoppedException;
 
     /**
-     * @param containerId
-     * @throws ContainerNotFoundException
-     * @throws DockerClientException
-     * @throws ContainerStillRunningException
+     * Removes a stopped container by given id from the metadata database.
+     *
+     * @param containerId The container id.
+     * @throws ContainerNotFoundException     The container was not found in the metadata database.
+     * @throws DockerClientException          The docker client was unable to perform this action.
+     * @throws ContainerStillRunningException The container is still running and this action cannot be performed.
      */
     void remove(Long containerId) throws ContainerNotFoundException, DockerClientException,
             ContainerStillRunningException, ContainerAlreadyRemovedException;
 
     /**
-     * @param id
-     * @return
-     * @throws ContainerNotFoundException
+     * Finds a container with a specific id from the metadata database.
+     *
+     * @param id The container id.
+     * @return The container object, if successful.
+     * @throws ContainerNotFoundException The container was not found in the metadata database.
      */
     Container find(Long id) throws ContainerNotFoundException;
 
     /**
-     * @param id
-     * @return
-     * @throws ContainerNotFoundException
-     * @throws DockerClientException
-     * @throws ContainerNotRunningException
+     * Inspects a container state and resources by given id.
+     *
+     * @param id The container id.
+     * @return The container object.
+     * @throws DockerClientException        The docker client was unable to perform this action.
+     * @throws ContainerNotRunningException The docker container is not running.
      */
-    Container inspect(Long id) throws ContainerNotFoundException, DockerClientException, ContainerNotRunningException;
+    ContainerDto inspect(Long id) throws DockerClientException, ContainerNotRunningException, ContainerNotFoundException;
 
     /**
      * Retrieve a list of all containers from the metadata database
@@ -62,13 +73,20 @@ public interface ContainerService {
      */
     List<Container> getAll(Integer limit);
 
+    /**
+     * Find all containers on the server.
+     *
+     * @return List of containers.
+     */
     List<com.github.dockerjava.api.model.Container> list();
 
     /**
-     * @param containerId
-     * @return
-     * @throws ContainerNotFoundException
-     * @throws DockerClientException
+     * Starts a container with given id from the metadata database.
+     *
+     * @param containerId The container id.
+     * @return The container object, if successful.
+     * @throws ContainerNotFoundException The container was not found in the metadata database.
+     * @throws DockerClientException      The docker client was unable to perform this action.
      */
     Container start(Long containerId) throws ContainerNotFoundException, DockerClientException, ContainerAlreadyRunningException;
 }
diff --git a/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java b/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java
index 8c18fc898711887755d17b4661182df37f2f6cc1..e25363f4436dda27098d56a8f1a02913b2c2267a 100644
--- a/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java
+++ b/dbrepo-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java
@@ -1,6 +1,8 @@
 package at.tuwien.service.impl;
 
 import at.tuwien.api.container.ContainerCreateRequestDto;
+import at.tuwien.api.container.ContainerDto;
+import at.tuwien.api.container.ContainerStateDto;
 import at.tuwien.config.DockerDaemonConfig;
 import at.tuwien.entities.container.Container;
 import at.tuwien.entities.container.image.ContainerImage;
@@ -184,8 +186,7 @@ public class ContainerServiceImpl implements ContainerService {
 
     @Override
     @Transactional
-    public Container inspect(Long id) throws ContainerNotFoundException, DockerClientException,
-            ContainerNotRunningException {
+    public ContainerDto inspect(Long id) throws DockerClientException, ContainerNotRunningException, ContainerNotFoundException {
         final Container container = find(id);
         final InspectContainerResponse response;
         try {
@@ -207,15 +208,19 @@ public class ContainerServiceImpl implements ContainerService {
             log.error("Failed to inspect container state: container is not running");
             throw new ContainerNotRunningException("Failed to inspect container state");
         }
+        final ContainerDto dto = containerMapper.containerToContainerDto(container);
+        dto.setHash(container.getHash());
+        dto.setRunning(response.getState().getRunning());
+        dto.setState(containerMapper.containerStateToContainerStateDto(response.getState()));
         /* now we only support one network */
         response.getNetworkSettings()
                 .getNetworks()
                 .forEach((key, network) -> {
                     log.trace("key {} network {}", key, network);
-                    container.setIpAddress(network.getIpAddress());
+                    dto.setIpAddress(network.getIpAddress());
                 });
-        log.info("Inspect container with id {}", id);
-        return container;
+        log.info("Inspected container with hash {}", container.getHash());
+        return dto;
     }
 
     @Override
diff --git a/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java b/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java
index 63114d8ccee3353eca4e90fc34f36dfb98e65f3a..e07a29bf0b2012e4910602939f8b4125f1607976 100644
--- a/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java
+++ b/dbrepo-database-service/rest-service/src/main/java/at/tuwien/endpoints/DatabaseEndpoint.java
@@ -1,5 +1,6 @@
 package at.tuwien.endpoints;
 
+import at.tuwien.api.container.ContainerDto;
 import at.tuwien.api.database.*;
 import at.tuwien.api.error.ApiErrorDto;
 import at.tuwien.entities.container.Container;
@@ -257,7 +258,7 @@ public class DatabaseEndpoint {
                             mediaType = "application/json",
                             schema = @Schema(implementation = DatabaseDto.class))}),
             @ApiResponse(responseCode = "404",
-                    description = "Database could not be found",
+                    description = "Database or container could not be found",
                     content = {@Content(
                             mediaType = "application/json",
                             schema = @Schema(implementation = ApiErrorDto.class))}),
@@ -270,7 +271,7 @@ public class DatabaseEndpoint {
     public ResponseEntity<DatabaseDto> findById(@NotNull @PathVariable("id") Long containerId,
                                                 @NotNull @PathVariable Long databaseId,
                                                 Principal principal)
-            throws DatabaseNotFoundException, AccessDeniedException {
+            throws DatabaseNotFoundException, AccessDeniedException, ContainerNotFoundException {
         log.debug("endpoint find database, containerId={}, databaseId={}", containerId, databaseId);
         final Database database = databaseService.findById(containerId, databaseId);
         final DatabaseDto dto = databaseMapper.databaseToDatabaseDto(database);
@@ -281,7 +282,9 @@ public class DatabaseEndpoint {
                     .map(databaseMapper::databaseAccessToDatabaseAccessDto)
                     .collect(Collectors.toList()));
         }
-        log.trace("find database resulted in database {}", database);
+        final ContainerDto containerDto = containerService.inspect(containerId);
+        dto.setContainer(containerDto);
+        log.trace("find database resulted in dto {}", dto);
         return ResponseEntity.ok(dto);
     }
 
diff --git a/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java b/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java
index c32330b76d496cf6232be98a237062ee37f156d4..4cf0fc3edb0a0dc7898a07e5de495c825be5456b 100644
--- a/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java
+++ b/dbrepo-database-service/rest-service/src/test/java/at/tuwien/endpoint/DatabaseEndpointUnitTest.java
@@ -350,7 +350,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest {
 
     @Test
     @WithAnonymousUser
-    public void findById_anonymous_succeeds() throws AccessDeniedException, DatabaseNotFoundException {
+    public void findById_anonymous_succeeds() throws AccessDeniedException, DatabaseNotFoundException, ContainerNotFoundException {
 
         /* test */
         findById_generic(CONTAINER_1_ID, CONTAINER_1, DATABASE_1_ID, DATABASE_1, null);
@@ -368,7 +368,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest {
 
     @Test
     @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"})
-    public void findById_hasRole_succeeds() throws AccessDeniedException, DatabaseNotFoundException {
+    public void findById_hasRole_succeeds() throws AccessDeniedException, DatabaseNotFoundException, ContainerNotFoundException {
 
         /* pre-condition */
         assertTrue(DATABASE_3_PUBLIC);
@@ -380,7 +380,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest {
     @Test
     @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"})
     public void findById_hasRoleForeign_succeeds() throws AccessDeniedException,
-            DatabaseNotFoundException {
+            DatabaseNotFoundException, ContainerNotFoundException {
 
         /* pre-condition */
         assertTrue(DATABASE_3_PUBLIC);
@@ -392,7 +392,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest {
     @Test
     @WithMockUser(username = USER_1_USERNAME, authorities = {"find-database"})
     public void findById_ownerSeesAccessRights_succeeds() throws AccessDeniedException,
-            DatabaseNotFoundException {
+            DatabaseNotFoundException, ContainerNotFoundException {
 
         /* mock */
         when(accessService.list(DATABASE_1_ID))
@@ -506,7 +506,7 @@ public class DatabaseEndpointUnitTest extends BaseUnitTest {
     }
 
     public DatabaseDto findById_generic(Long containerId, Container container, Long databaseId, Database database,
-                                        Principal principal) throws DatabaseNotFoundException, AccessDeniedException {
+                                        Principal principal) throws DatabaseNotFoundException, AccessDeniedException, ContainerNotFoundException {
 
         /* mock */
         if (database != null) {
diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java b/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java
index b30f9a567ca1abd794c1f630e633815606c14d00..f3db3e030a05b7e91135fd8a0968d29eb9b6d2d5 100644
--- a/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java
+++ b/dbrepo-database-service/services/src/main/java/at/tuwien/config/GatewayConfig.java
@@ -27,8 +27,8 @@ public class GatewayConfig {
     @Value("${spring.rabbitmq.password}")
     private String brokerPassword;
 
-    @Bean("authenticationRestTemplate")
-    public RestTemplate authenticationRestTemplate() {
+    @Bean("gatewayRestTemplate")
+    public RestTemplate gatewayRestTemplate() {
         final RestTemplate restTemplate = new RestTemplate();
         restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(gatewayEndpoint));
         return restTemplate;
diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/ContainerServiceGateway.java b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/ContainerServiceGateway.java
new file mode 100644
index 0000000000000000000000000000000000000000..8fcd57a66e49f5c73a44e41e9958c2606344819c
--- /dev/null
+++ b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/ContainerServiceGateway.java
@@ -0,0 +1,14 @@
+package at.tuwien.gateway;
+
+import at.tuwien.api.container.ContainerDto;
+import at.tuwien.exception.ContainerNotFoundException;
+
+public interface ContainerServiceGateway {
+
+    /**
+     * @param id
+     * @return
+     * @throws ContainerNotFoundException
+     */
+    ContainerDto find(Long id) throws ContainerNotFoundException;
+}
diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/impl/ContainerServiceGatewayImpl.java b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/impl/ContainerServiceGatewayImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e366fdf984df58824a2fead84c68a535e6b2a41
--- /dev/null
+++ b/dbrepo-database-service/services/src/main/java/at/tuwien/gateway/impl/ContainerServiceGatewayImpl.java
@@ -0,0 +1,41 @@
+package at.tuwien.gateway.impl;
+
+import at.tuwien.api.container.ContainerDto;
+import at.tuwien.config.GatewayConfig;
+import at.tuwien.exception.ContainerNotFoundException;
+import at.tuwien.gateway.ContainerServiceGateway;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+@Slf4j
+@Service
+public class ContainerServiceGatewayImpl implements ContainerServiceGateway {
+
+    private final RestTemplate restTemplate;
+    private final GatewayConfig gatewayConfig;
+
+    @Autowired
+    public ContainerServiceGatewayImpl(@Qualifier("gatewayRestTemplate") RestTemplate restTemplate,
+                                       GatewayConfig gatewayConfig) {
+        this.restTemplate = restTemplate;
+        this.gatewayConfig = gatewayConfig;
+    }
+
+    @Override
+    public ContainerDto find(Long id) throws ContainerNotFoundException {
+        final String url = gatewayConfig.getGatewayEndpoint() + "/api/container/" + id;
+        final ResponseEntity<ContainerDto> response = restTemplate.exchange(url, HttpMethod.GET, null, ContainerDto.class);
+        if (!response.getStatusCode().equals(HttpStatus.OK)) {
+            log.error("Failed to find container: {}", response.getStatusCode());
+            throw new ContainerNotFoundException("Failed to find container");
+        }
+        return response.getBody();
+    }
+
+}
diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java b/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java
index ee70355c6d596650f7b0896dba30882cd760eeae..d58e11d4416cb62727f6a12688c2ff419fab2add 100644
--- a/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java
+++ b/dbrepo-database-service/services/src/main/java/at/tuwien/service/ContainerService.java
@@ -1,8 +1,11 @@
 package at.tuwien.service;
 
+import at.tuwien.api.container.ContainerDto;
 import at.tuwien.entities.container.Container;
 import at.tuwien.exception.ContainerNotFoundException;
 
 public interface ContainerService {
     Container find(Long id) throws ContainerNotFoundException;
+
+    ContainerDto inspect(Long id) throws ContainerNotFoundException;
 }
diff --git a/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java b/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java
index bb657b1812fc208a85020c84a28856c084273ac6..76c29263d4b42461ac01274551f5650a5b6397ba 100644
--- a/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java
+++ b/dbrepo-database-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java
@@ -1,7 +1,9 @@
 package at.tuwien.service.impl;
 
+import at.tuwien.api.container.ContainerDto;
 import at.tuwien.entities.container.Container;
 import at.tuwien.exception.ContainerNotFoundException;
+import at.tuwien.gateway.ContainerServiceGateway;
 import at.tuwien.repository.jpa.ContainerRepository;
 import at.tuwien.service.ContainerService;
 import lombok.extern.log4j.Log4j2;
@@ -15,10 +17,13 @@ import java.util.Optional;
 public class ContainerServiceImpl implements ContainerService {
 
     private final ContainerRepository containerRepository;
+    private final ContainerServiceGateway containerServiceGateway;
 
     @Autowired
-    public ContainerServiceImpl(ContainerRepository containerRepository) {
+    public ContainerServiceImpl(ContainerRepository containerRepository,
+                                ContainerServiceGateway containerServiceGateway) {
         this.containerRepository = containerRepository;
+        this.containerServiceGateway = containerServiceGateway;
     }
 
     @Override
@@ -30,4 +35,9 @@ public class ContainerServiceImpl implements ContainerService {
         }
         return optional.get();
     }
+
+    @Override
+    public ContainerDto inspect(Long id) throws ContainerNotFoundException {
+        return containerServiceGateway.find(id);
+    }
 }
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..a11c70f6219d1bd362775c50fedbefa5a9aa07f4
--- /dev/null
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java
@@ -0,0 +1,33 @@
+package at.tuwien.api.maintenance;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+import lombok.extern.jackson.Jacksonized;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Jacksonized
+@ToString
+public class BannerMessageBriefDto {
+
+    @NotNull
+    private BannerMessageTypeDto type;
+
+    @NotBlank
+    @Schema(example = "Maintenance starts on 8am on Monday")
+    private String message;
+
+    @Schema(example = "https://example.com")
+    private String link;
+
+    @JsonProperty("link_text")
+    @Schema(example = "More")
+    private String linkText;
+
+}
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2278f343f1186875b83510b9ea906c53930ce02
--- /dev/null
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java
@@ -0,0 +1,44 @@
+package at.tuwien.api.maintenance;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+import lombok.extern.jackson.Jacksonized;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Jacksonized
+@ToString
+public class BannerMessageCreateDto {
+
+    @NotNull
+    private BannerMessageTypeDto type;
+
+    @NotBlank
+    @Schema(example = "Maintenance starts on 8am on Monday")
+    private String message;
+
+    @Schema(example = "https://example.com")
+    private String link;
+
+    @JsonProperty("link_text")
+    @Schema(example = "More")
+    private String linkText;
+
+    @JsonProperty("display_start")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+    private Instant displayStart;
+
+    @JsonProperty("display_end")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+    private Instant displayEnd;
+
+}
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b822b9ed45b79c78b4a9c6f8c0b177d9d5e15ed
--- /dev/null
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java
@@ -0,0 +1,47 @@
+package at.tuwien.api.maintenance;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+import lombok.extern.jackson.Jacksonized;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Jacksonized
+@ToString
+public class BannerMessageDto {
+
+    @NotNull
+    private Long id;
+
+    @NotNull
+    private BannerMessageTypeDto type;
+
+    @NotBlank
+    @Schema(example = "Maintenance starts on 8am on Monday")
+    private String message;
+
+    @Schema(example = "https://example.com")
+    private String link;
+
+    @JsonProperty("link_text")
+    @Schema(example = "More")
+    private String linkText;
+
+    @JsonProperty("display_start")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+    private Instant displayStart;
+
+    @JsonProperty("display_end")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+    private Instant displayEnd;
+
+}
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a867f5ea4b06b44aff2efde9ee36c371718f374
--- /dev/null
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java
@@ -0,0 +1,28 @@
+package at.tuwien.api.maintenance;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+
+@Getter
+public enum BannerMessageTypeDto {
+
+    @JsonProperty("error")
+    ERROR("error"),
+
+    @JsonProperty("warning")
+    WARNING("warning"),
+
+    @JsonProperty("info")
+    INFO("info");
+
+    private String name;
+
+    BannerMessageTypeDto(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return this.name;
+    }
+}
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..107c2405ca76a791dbf8684eb8a7f510be3589f4
--- /dev/null
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java
@@ -0,0 +1,44 @@
+package at.tuwien.api.maintenance;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.*;
+import lombok.extern.jackson.Jacksonized;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Jacksonized
+@ToString
+public class BannerMessageUpdateDto {
+
+    @NotNull
+    private BannerMessageTypeDto type;
+
+    @NotBlank
+    @Schema(example = "Maintenance starts on 8am on Monday")
+    private String message;
+
+    @Schema(example = "https://example.com")
+    private String link;
+
+    @JsonProperty("link_text")
+    @Schema(example = "More")
+    private String linkText;
+
+    @JsonProperty("display_start")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+    private Instant displayStart;
+
+    @JsonProperty("display_end")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
+    private Instant displayEnd;
+
+}
diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3cf82d4997a7b67de2b0ee834615fd56a379cb4
--- /dev/null
+++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java
@@ -0,0 +1,43 @@
+package at.tuwien.entities.maintenance;
+
+import jakarta.persistence.*;
+import lombok.*;
+import org.hibernate.annotations.GenericGenerator;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.Instant;
+
+@Data
+@Entity
+@Builder
+@ToString
+@AllArgsConstructor
+@NoArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
+@Table(name = "mdb_banner_messages")
+@NamedQueries({
+        @NamedQuery(name = "BannerMessage.findByActive", query = "select m from BannerMessage m where (m.displayStart = null and m.displayEnd = null) or (m.displayStart = null and m.displayEnd >= NOW()) or (m.displayStart <= NOW() and m.displayEnd >= NOW()) or (m.displayStart <= NOW() and m.displayEnd = null)")
+})
+public class BannerMessage {
+
+    @Id
+    @EqualsAndHashCode.Include
+    @GeneratedValue(generator = "messages-sequence")
+    @GenericGenerator(name = "messages-sequence", strategy = "increment")
+    @Column(updatable = false, nullable = false)
+    private Long id;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false, columnDefinition = "enum('ERROR','WARNING','INFO')")
+    private BannerMessageType type;
+
+    @Column(nullable = false)
+    private String message;
+
+    @Column(columnDefinition = "TIMESTAMP")
+    private Instant displayStart;
+
+    @Column(columnDefinition = "TIMESTAMP")
+    private Instant displayEnd;
+
+}
diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d17965f482754e5c9e9aed8e6c9e9c82b6ab650
--- /dev/null
+++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java
@@ -0,0 +1,13 @@
+
+package at.tuwien.entities.maintenance;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public enum BannerMessageType {
+    WARNING,
+    ERROR,
+    INFO;
+}
\ No newline at end of file
diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql
index eb7809d1409e253272d63244c3ec444f14220b81..24798819e914a7e5c2bf34c44c561cee6bbfc936 100644
--- a/dbrepo-metadata-db/setup-schema.sql
+++ b/dbrepo-metadata-db/setup-schema.sql
@@ -332,6 +332,18 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_view`
     FOREIGN KEY (vdbid) REFERENCES mdb_databases (id)
 ) WITH SYSTEM VERSIONING;
 
+CREATE TABLE IF NOT EXISTS `fda`.`mdb_banner_messages`
+(
+    id            bigint                            NOT NULL AUTO_INCREMENT,
+    type          ENUM ('ERROR', 'WARNING', 'INFO') NOT NULL default 'INFO',
+    message       TEXT                              NOT NULL,
+    link          TEXT                              NULL,
+    link_text     VARCHAR(255)                      NULL,
+    display_start timestamp                         NULL,
+    display_end   timestamp                         NULL,
+    PRIMARY KEY (id)
+) WITH SYSTEM VERSIONING;
+
 CREATE TABLE IF NOT EXISTS `fda`.`mdb_view_columns`
 (
     id       BIGINT  NOT NULL AUTO_INCREMENT,
diff --git a/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java
index 2d91e8ac304ec00b5c851dc14599803c6b65a85d..6265b027b9757a6b0c311096023478fb0533a957 100644
--- a/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java
+++ b/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java
@@ -3,8 +3,9 @@ package at.tuwien.test;
 import at.tuwien.api.amqp.CreateVirtualHostDto;
 import at.tuwien.api.amqp.GrantVirtualHostPermissionsDto;
 import at.tuwien.api.auth.SignupRequestDto;
-import at.tuwien.api.container.image.ImageEnvItemDto;
-import at.tuwien.api.container.image.ImageEnvItemTypeDto;
+import at.tuwien.api.container.ContainerDto;
+import at.tuwien.api.container.ContainerStateDto;
+import at.tuwien.api.container.image.*;
 import at.tuwien.api.database.DatabaseCreateDto;
 import at.tuwien.api.database.DatabaseDto;
 import at.tuwien.api.database.LicenseDto;
@@ -20,6 +21,9 @@ import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto;
 import at.tuwien.api.database.table.constraints.ConstraintsCreateDto;
 import at.tuwien.api.database.table.constraints.foreignKey.ForeignKeyCreateDto;
 import at.tuwien.api.identifier.*;
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageTypeDto;
+import at.tuwien.api.maintenance.BannerMessageUpdateDto;
 import at.tuwien.api.user.*;
 import at.tuwien.entities.container.image.ContainerImageDate;
 import at.tuwien.entities.database.*;
@@ -30,6 +34,8 @@ import at.tuwien.entities.database.table.constraints.foreignKey.ForeignKey;
 import at.tuwien.entities.database.table.constraints.foreignKey.ForeignKeyReference;
 import at.tuwien.entities.database.table.constraints.unique.Unique;
 import at.tuwien.entities.identifier.*;
+import at.tuwien.entities.maintenance.BannerMessage;
+import at.tuwien.entities.maintenance.BannerMessageType;
 import at.tuwien.entities.user.Realm;
 import at.tuwien.entities.user.Role;
 import at.tuwien.entities.user.User;
@@ -96,7 +102,7 @@ import static java.time.temporal.ChronoUnit.*;
  * <br />
  * User 2 (authorities=default developer)
  * <br />
- * User 3 (authorities=empty)
+ * User 3 (authorities=default data-steward)
  */
 public abstract class BaseTest {
 
@@ -304,6 +310,14 @@ public abstract class BaseTest {
             .attributes(USER_1_ATTRIBUTES_DTO)
             .build();
 
+    public final static UserBriefDto USER_1_BRIEF_DTO = UserBriefDto.builder()
+            .id(USER_1_ID)
+            .username(USER_1_USERNAME)
+            .firstname(USER_1_FIRSTNAME)
+            .lastname(USER_1_LASTNAME)
+            .emailVerified(USER_1_VERIFIED)
+            .build();
+
     public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder()
             .username(USER_1_USERNAME)
             .email(USER_1_EMAIL)
@@ -701,6 +715,24 @@ public abstract class BaseTest {
             .hasTime(IMAGE_DATE_1_HAS_TIME)
             .build();
 
+    public final static ImageDateDto IMAGE_DATE_1_DTO = ImageDateDto.builder()
+            .id(IMAGE_DATE_1_ID)
+            .unixFormat(IMAGE_DATE_1_UNIX_FORMAT)
+            .databaseFormat(IMAGE_DATE_1_DATABASE_FORMAT)
+            .example(IMAGE_DATE_1_EXAMPLE)
+            .hasTime(IMAGE_DATE_1_HAS_TIME)
+            .build();
+
+    public final static ImageCreateDto IMAGE_1_CREATE_DTO = ImageCreateDto.builder()
+            .repository(IMAGE_1_REPOSITORY)
+            .tag(IMAGE_1_TAG)
+            .dialect(IMAGE_1_DIALECT)
+            .jdbcMethod(IMAGE_1_JDBC)
+            .driverClass(IMAGE_1_DRIVER)
+            .defaultPort(IMAGE_1_PORT)
+            .environment(IMAGE_1_ENV_DTO)
+            .build();
+
     public final static Long IMAGE_DATE_2_ID = 2L;
     public final static Long IMAGE_DATE_2_IMAGE_ID = IMAGE_1_ID;
     public final static String IMAGE_DATE_2_UNIX_FORMAT = "dd.MM.yy";
@@ -717,6 +749,14 @@ public abstract class BaseTest {
             .hasTime(IMAGE_DATE_2_HAS_TIME)
             .build();
 
+    public final static ImageDateDto IMAGE_DATE_2_DTO = ImageDateDto.builder()
+            .id(IMAGE_DATE_2_ID)
+            .unixFormat(IMAGE_DATE_2_UNIX_FORMAT)
+            .databaseFormat(IMAGE_DATE_2_DATABASE_FORMAT)
+            .example(IMAGE_DATE_2_EXAMPLE)
+            .hasTime(IMAGE_DATE_2_HAS_TIME)
+            .build();
+
     public final static Long IMAGE_DATE_3_ID = 3L;
     public final static Long IMAGE_DATE_3_IMAGE_ID = IMAGE_1_ID;
     public final static String IMAGE_DATE_3_UNIX_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS";
@@ -733,6 +773,14 @@ public abstract class BaseTest {
             .hasTime(IMAGE_DATE_3_HAS_TIME)
             .build();
 
+    public final static ImageDateDto IMAGE_DATE_3_DTO = ImageDateDto.builder()
+            .id(IMAGE_DATE_3_ID)
+            .unixFormat(IMAGE_DATE_3_UNIX_FORMAT)
+            .databaseFormat(IMAGE_DATE_3_DATABASE_FORMAT)
+            .example(IMAGE_DATE_3_EXAMPLE)
+            .hasTime(IMAGE_DATE_3_HAS_TIME)
+            .build();
+
     public final static ContainerImage IMAGE_1 = ContainerImage.builder()
             .id(IMAGE_1_ID)
             .repository(IMAGE_1_REPOSITORY)
@@ -763,6 +811,27 @@ public abstract class BaseTest {
             .environment(List.of() /* for jpa */)
             .build();
 
+    public final static ImageDto IMAGE_1_DTO = ImageDto.builder()
+            .id(IMAGE_1_ID)
+            .repository(IMAGE_1_REPOSITORY)
+            .tag(IMAGE_1_TAG)
+            .hash(IMAGE_1_HASH)
+            .compiled(IMAGE_1_BUILT)
+            .dialect(IMAGE_1_DIALECT)
+            .jdbcMethod(IMAGE_1_JDBC)
+            .driverClass(IMAGE_1_DRIVER)
+            .size(BigInteger.valueOf(IMAGE_1_SIZE))
+            .environment(IMAGE_1_ENV_DTO)
+            .defaultPort(IMAGE_1_PORT)
+            .dateFormats(List.of(IMAGE_DATE_1_DTO, IMAGE_DATE_2_DTO, IMAGE_DATE_3_DTO))
+            .build();
+
+    public final static ImageBriefDto IMAGE_1_BRIEF_DTO = ImageBriefDto.builder()
+            .id(IMAGE_1_ID)
+            .repository(IMAGE_1_REPOSITORY)
+            .tag(IMAGE_1_TAG)
+            .build();
+
     public final static Long IMAGE_2_ID = 2L;
     public final static String IMAGE_2_REPOSITORY = "mysql";
     public final static String IMAGE_2_TAG = "8.0";
@@ -824,6 +893,7 @@ public abstract class BaseTest {
     public final static Long CONTAINER_1_ID = 1L;
     public final static String CONTAINER_1_HASH = "deadbeef";
     public final static ContainerImage CONTAINER_1_IMAGE = IMAGE_1;
+    public final static ImageBriefDto CONTAINER_1_IMAGE_BRIEF_DTO = IMAGE_1_BRIEF_DTO;
     public final static String CONTAINER_1_NAME = "u01";
     public final static String CONTAINER_1_INTERNALNAME = "dbrepo-userdb-u01";
     public final static String CONTAINER_1_IP = "172.30.0.5";
@@ -831,6 +901,8 @@ public abstract class BaseTest {
     public final static HealthCheck CONTAINER_1_HEALTHCHECK = new HealthCheck()
             .withTest(List.of("CMD", "mysqladmin", "ping", "--host=127.0.0.1", "--password=mariadb"));
     public final static String[] CONTAINER_1_ENV = new String[]{"MARIADB_ROOT_PASSWORD=mariadb", "MARIADB_DATABASE=weather"};
+    public final static ContainerStateDto CONTAINER_1_STATE = ContainerStateDto.RUNNING;
+    public final static Boolean CONTAINER_1_RUNNING = true;
 
     public final static Container CONTAINER_1 = Container.builder()
             .id(CONTAINER_1_ID)
@@ -862,6 +934,19 @@ public abstract class BaseTest {
             .owner(null /* for jpa */)
             .build();
 
+    public final static ContainerDto CONTAINER_1_DTO = ContainerDto.builder()
+            .id(CONTAINER_1_ID)
+            .name(CONTAINER_1_NAME)
+            .internalName(CONTAINER_1_INTERNALNAME)
+            .image(CONTAINER_1_IMAGE_BRIEF_DTO)
+            .hash(CONTAINER_1_HASH)
+            .created(CONTAINER_1_CREATED)
+            .ipAddress(CONTAINER_1_IP)
+            .owner(USER_1_BRIEF_DTO)
+            .state(CONTAINER_1_STATE)
+            .running(CONTAINER_1_RUNNING)
+            .build();
+
     public final static Long CONTAINER_2_ID = 2L;
     public final static String CONTAINER_2_HASH = "deadbeef";
     public final static ContainerImage CONTAINER_2_IMAGE = IMAGE_1;
@@ -5440,4 +5525,55 @@ public abstract class BaseTest {
             .configure(".*")
             .build();
 
+    public final static Long BANNER_MESSAGE_1_ID = 1L;
+    public final static String BANNER_MESSAGE_1_MESSAGE = "Next maintenance in 7 days!";
+    public final static BannerMessageType BANNER_MESSAGE_1_TYPE = BannerMessageType.INFO;
+    public final static BannerMessageTypeDto BANNER_MESSAGE_1_TYPE_DTO = BannerMessageTypeDto.INFO;
+    public final static Instant BANNER_MESSAGE_1_START = Instant.ofEpochSecond(1684577786);
+    public final static Instant BANNER_MESSAGE_1_END = null;
+
+    public final static BannerMessage BANNER_MESSAGE_1 = BannerMessage.builder()
+            .id(BANNER_MESSAGE_1_ID)
+            .message(BANNER_MESSAGE_1_MESSAGE)
+            .type(BANNER_MESSAGE_1_TYPE)
+            .displayStart(BANNER_MESSAGE_1_START)
+            .displayEnd(BANNER_MESSAGE_1_END)
+            .build();
+    
+    public final static BannerMessageCreateDto BANNER_MESSAGE_1_CREATE_DTO = BannerMessageCreateDto.builder()
+            .message(BANNER_MESSAGE_1_MESSAGE)
+            .type(BANNER_MESSAGE_1_TYPE_DTO)
+            .displayStart(BANNER_MESSAGE_1_START)
+            .displayEnd(BANNER_MESSAGE_1_END)
+            .build();
+
+    public final static BannerMessageUpdateDto BANNER_MESSAGE_1_UPDATE_DTO = BannerMessageUpdateDto.builder()
+            .message(BANNER_MESSAGE_1_MESSAGE)
+            .type(BannerMessageTypeDto.WARNING)
+            .displayStart(BANNER_MESSAGE_1_START)
+            .displayEnd(BANNER_MESSAGE_1_END)
+            .build();
+
+    public final static Long BANNER_MESSAGE_2_ID = 2L;
+    public final static String BANNER_MESSAGE_2_MESSAGE = "No operation on Christmas 2022!";
+    public final static BannerMessageType BANNER_MESSAGE_2_TYPE = BannerMessageType.ERROR;
+    public final static BannerMessageTypeDto BANNER_MESSAGE_2_TYPE_DTO = BannerMessageTypeDto.ERROR;
+    public final static Instant BANNER_MESSAGE_2_START = Instant.ofEpochSecond(1671836400);
+    public final static Instant BANNER_MESSAGE_2_END = Instant.ofEpochSecond(1672009200);
+
+    public final static BannerMessage BANNER_MESSAGE_2 = BannerMessage.builder()
+            .id(BANNER_MESSAGE_2_ID)
+            .message(BANNER_MESSAGE_2_MESSAGE)
+            .type(BANNER_MESSAGE_2_TYPE)
+            .displayStart(BANNER_MESSAGE_2_START)
+            .displayEnd(BANNER_MESSAGE_2_END)
+            .build();
+
+    public final static BannerMessageCreateDto BANNER_MESSAGE_2_CREATE_DTO = BannerMessageCreateDto.builder()
+            .message(BANNER_MESSAGE_2_MESSAGE)
+            .type(BANNER_MESSAGE_2_TYPE_DTO)
+            .displayStart(BANNER_MESSAGE_2_START)
+            .displayEnd(BANNER_MESSAGE_2_END)
+            .build();
+
 }
diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml
index 6c3514de1ebfeaddd3b01c471881b13f4ff7b32a..8dfab218eee806e01a1c1d7ad8d91e54ba352ae4 100644
--- a/dbrepo-metadata-service/pom.xml
+++ b/dbrepo-metadata-service/pom.xml
@@ -114,6 +114,12 @@
             <version>${project.version}</version>
             <scope>compile</scope>
         </dependency>
+        <!-- Authentication -->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>${jwt.version}</version>
+        </dependency>
         <!-- IDE -->
         <dependency>
             <groupId>org.projectlombok</groupId>
diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
index 13d92092fc3e74832d3e1ae48610a1614ba223a7..7bec3fbf94675ec5f48f7b0b46298da40c9d51cb 100644
--- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
+++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
@@ -2,7 +2,6 @@ package at.tuwien.config;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpMethod;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
diff --git a/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
index 582fe83630c25eee8b35c90b1000db5c1fd48564..1c36be92a450333a9a36b2ae24c9f7c1de5a90da 100644
--- a/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
+++ b/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
@@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
 import io.swagger.v3.oas.annotations.security.SecurityScheme;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpMethod;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
diff --git a/dbrepo-ui/api/metadata.service.js b/dbrepo-ui/api/metadata.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c02ea9247eea2ab242bf5387652d4ed2e50b5bc
--- /dev/null
+++ b/dbrepo-ui/api/metadata.service.js
@@ -0,0 +1,104 @@
+import Vue from 'vue'
+import api from '@/api'
+
+class MetadataService {
+  findAllMessages () {
+    return new Promise((resolve, reject) => {
+      api.get('/api/maintenance/message', { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const messages = response.data
+          console.debug('response messages', messages)
+          resolve(messages)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load messages', error)
+          Vue.$toast.error(`[${code}] Failed to load messages: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  createMessage (data) {
+    return new Promise((resolve, reject) => {
+      api.post('/api/maintenance/message', data, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const messages = response.data
+          console.debug('response message', messages)
+          resolve(messages)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to create message', error)
+          Vue.$toast.error(`[${code}] Failed to create message: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  findMessage (id) {
+    return new Promise((resolve, reject) => {
+      api.get(`/api/maintenance/message/${id}`, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const messages = response.data
+          console.debug('response message', messages)
+          resolve(messages)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to find message', error)
+          Vue.$toast.error(`[${code}] Failed to find message: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  updateMessage (id, data) {
+    return new Promise((resolve, reject) => {
+      api.put(`/api/maintenance/message/${id}`, data, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const messages = response.data
+          console.debug('response message', messages)
+          resolve(messages)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to update message', error)
+          Vue.$toast.error(`[${code}] Failed to update message: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  deleteMessage (id) {
+    return new Promise((resolve, reject) => {
+      api.delete(`/api/maintenance/message/${id}`, { headers: { Accept: 'application/json' } })
+        .then(() => resolve())
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to delete message', error)
+          Vue.$toast.error(`[${code}] Failed to delete message: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
+  findActiveMessages () {
+    return new Promise((resolve, reject) => {
+      api.get('/api/maintenance/message/active', { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const messages = response.data
+          console.debug('response messages', messages)
+          resolve(messages)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to load active messages', error)
+          Vue.$toast.error(`[${code}] Failed to load active messages: ${message}`)
+          reject(error)
+        })
+    })
+  }
+}
+
+export default new MetadataService()
diff --git a/dbrepo-ui/components/DBToolbar.vue b/dbrepo-ui/components/DBToolbar.vue
index b8590bc855355dd9d2e0207a20becb97c25f4e6d..c899ab03eb0afde4713b7b5b77a6a2b70727c021 100644
--- a/dbrepo-ui/components/DBToolbar.vue
+++ b/dbrepo-ui/components/DBToolbar.vue
@@ -55,7 +55,7 @@
           <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table`">
             {{ $t('databases.toolbar.tables', { name: 'vue-i18n' }) }}
           </v-tab>
-          <v-tab v-if="hasReadAccess" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query`">
+          <v-tab v-if="canViewQueries" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query`">
             {{ $t('databases.toolbar.subsets', { name: 'vue-i18n' }) }}
           </v-tab>
           <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/view`">
@@ -121,6 +121,9 @@ export default {
       }
       return this.roles.includes('execute-query')
     },
+    canViewQueries () {
+      return this.database.is_public || this.hasReadAccess
+    },
     canCreateView () {
       if (!this.user || !this.isOwner) {
         return false
diff --git a/dbrepo-ui/components/DatabaseList.vue b/dbrepo-ui/components/DatabaseList.vue
index fc044ee2ff1321fdcb9e61aee930dbca0accd0eb..bdca982e74d2a9902c1fe37f35af6c39a3733441 100644
--- a/dbrepo-ui/components/DatabaseList.vue
+++ b/dbrepo-ui/components/DatabaseList.vue
@@ -1,13 +1,16 @@
 <template>
   <div>
     <v-progress-linear v-if="loadingContainers || loadingDatabases" :indeterminate="!error" />
+    <v-card v-if="!$vuetify.theme.dark && containers.length> 0" flat tile>
+      <v-divider class="mx-4" />
+    </v-card>
     <v-card
       v-for="(container, idx) in containers"
       :key="idx"
       :to="link(container)"
       flat
       tile>
-      <v-divider v-if="!$vuetify.theme.dark" class="mx-4" />
+      <v-divider v-if="idx !== 0" class="mx-4" />
       <v-card-title v-if="!hasDatabase(container)" v-text="container.name" />
       <v-card-title v-if="hasDatabase(container)">
         <a :href="`/container/${container.id}/database/${container.database.id}`">{{ container.name }}</a>
@@ -29,18 +32,8 @@
             v-text="container.database.identifier.publisher" />
         </div>
         <div v-text="identifierDescription(container)" />
-      </v-card-text>
-      <v-card-text v-if="needsStart(container) || needsDatabase(container)" class="db-buttons">
-        <v-btn
-          v-if="needsStart(container)"
-          small
-          secondary
-          :loading="container?.loading"
-          @click.stop="startContainer(container).then(() => createDatabase(container))">
-          Start
-        </v-btn>
         <v-btn
-          v-else-if="needsDatabase(container)"
+          v-if="needsDatabase(container)"
           small
           secondary
           :loading="container?.loading"
@@ -98,15 +91,6 @@ export default {
     formatCreators (container) {
       return ContainerMapper.containerToCreator(container)
     },
-    needsStart (container) {
-      if (!this.user) {
-        return false
-      }
-      if (container.creator.username !== this.user.username) {
-        return false
-      }
-      return container.running === false
-    },
     needsDatabase (container) {
       if (!this.user) {
         return false
@@ -144,16 +128,6 @@ export default {
         })
       this.loadingContainers = false
     },
-    startContainer (container) {
-      container.loading = true
-      return new Promise((resolve, reject) => {
-        ContainerService.modify(container.id, 'start')
-          .then(() => resolve())
-          .finally(() => {
-            container.loading = false
-          })
-      })
-    },
     createDatabase (container) {
       container.loading = true
       DatabaseService.create(container.id, { name: container.name, is_public: true })
diff --git a/dbrepo-ui/components/QueryList.vue b/dbrepo-ui/components/QueryList.vue
index 1bead29f290f43e95e83cb3a37ce4b35180558fd..4485e3494518a7b1e0f23b4053025358d8edbcd1 100644
--- a/dbrepo-ui/components/QueryList.vue
+++ b/dbrepo-ui/components/QueryList.vue
@@ -1,11 +1,16 @@
 <template>
   <div>
-    <v-progress-linear v-if="loadingIdentifiers || loadingQueries || error" :color="loadingColor" :value="loadProgress" />
-    <v-card v-if="!(loadingIdentifiers || loadingQueries) && queries && queries.length === 0" flat>
+    <v-progress-linear v-if="loadingIdentifiers || loadingQueries || error" :color="loadingColor" :indeterminate="!error" />
+    <v-card v-if="!error && !(loadingIdentifiers || loadingQueries) && queries && queries.length === 0" flat tile>
       <v-card-text>
         (no subsets)
       </v-card-text>
     </v-card>
+    <v-card v-if="error" flat tile>
+      <v-card-text>
+        Failed to load queries: database is not reachable
+      </v-card-text>
+    </v-card>
     <v-tabs-items>
       <div v-if="!loadingQueries && !error">
         <div v-for="(item,i) in queries" :key="i">
@@ -77,7 +82,6 @@ export default {
     return {
       loadingQueries: false,
       loadingIdentifiers: false,
-      loadProgress: 0,
       error: false,
       queries: [],
       identifiers: []
@@ -129,7 +133,6 @@ export default {
   mounted () {
     this.loadQueries()
     this.loadIdentifiers()
-    this.simulateProgress()
   },
   methods: {
     loadIdentifiers () {
@@ -148,6 +151,9 @@ export default {
         .then((queries) => {
           this.queries = queries
         })
+        .catch(() => {
+          this.error = true
+        })
         .finally(() => {
           this.loadingQueries = false
         })
@@ -181,20 +187,6 @@ export default {
         return 'primary--text'
       }
       return null
-    },
-    simulateProgress () {
-      if (this.loadProgress !== 0) {
-        return
-      }
-      const timeout = 30 * 1000 /* ms */
-      const ticks = 100 /* ms */
-      let i = 0
-      setInterval(() => {
-        if (i++ >= timeout && !this.error) {
-          return
-        }
-        this.loadProgress = ((i * 100) / timeout) * 100
-      }, ticks)
     }
   }
 }
diff --git a/dbrepo-ui/components/TableToolbar.vue b/dbrepo-ui/components/TableToolbar.vue
index b3f22595c99e203c8b9309e3b9cb29aa998d3f0b..69351b3121ea40023089d48934d99fa77ad74d9e 100644
--- a/dbrepo-ui/components/TableToolbar.vue
+++ b/dbrepo-ui/components/TableToolbar.vue
@@ -108,12 +108,18 @@ export default {
       return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data')
     },
     canEditTuple () {
+      if (this.selection === null || this.selection.length !== 1) {
+        return false
+      }
       if (!this.roles || !this.isDataTab) {
         return false
       }
       return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data')
     },
     canDeleteTuple () {
+      if (this.selection === null || this.selection.length < 1) {
+        return false
+      }
       if (!this.roles || !this.isDataTab) {
         return false
       }
@@ -199,7 +205,7 @@ export default {
           })
         TableService.deleteTuple(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, { keys: constraints })
           .then(() => {
-            this.$toast.success(`Deleted ${this.selection.length} rows(s)`)
+            this.$toast.success(`Deleted ${this.selection.length} row${this.selection.length !== 1 ? 's' : ''}`)
             this.$emit('modified', { success: true, action: 'delete' })
           })
       }
diff --git a/dbrepo-ui/components/UserToolbar.vue b/dbrepo-ui/components/UserToolbar.vue
index 462b4384962ad83edd2297cc7303df6152b5776e..878a48085952cbb9774cba87e61aad394b419bfe 100644
--- a/dbrepo-ui/components/UserToolbar.vue
+++ b/dbrepo-ui/components/UserToolbar.vue
@@ -12,6 +12,9 @@
       <v-tab to="/user/authentication">
         Authentication
       </v-tab>
+      <v-tab v-if="canHandleMessages" to="/user/developer">
+        Developer
+      </v-tab>
     </v-tabs>
   </div>
 </template>
@@ -23,6 +26,29 @@ export default {
     return {
       tab: null
     }
+  },
+  computed: {
+    user () {
+      return this.$store.state.user
+    },
+    roles () {
+      return this.$store.state.roles
+    },
+    canCreateMessage () {
+      if (!this.roles) {
+        return false
+      }
+      return this.roles.includes('create-maintenance-message')
+    },
+    canModifyMessage () {
+      if (!this.roles) {
+        return false
+      }
+      return this.roles.includes('modify-maintenance-message')
+    },
+    canHandleMessages () {
+      return this.canCreateMessage || this.canModifyMessage
+    }
   }
 }
 </script>
diff --git a/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue b/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f3090c1e139dfc4dd0768afd59e4f78150c9687e
--- /dev/null
+++ b/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue
@@ -0,0 +1,222 @@
+<template>
+  <div>
+    <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit">
+      <v-card>
+        <v-card-title v-text="title" />
+        <v-card-text>
+          <v-row dense>
+            <v-col>
+              <v-select
+                v-model="localMessage.type"
+                :items="types"
+                item-text="name"
+                item-value="value"
+                :rules="[v => !!v || $t('Required')]"
+                required
+                label="Type *" />
+            </v-col>
+          </v-row>
+          <v-row dense>
+            <v-col>
+              <v-text-field
+                v-model="localMessage.message"
+                :rules="[v => !!v || $t('Required')]"
+                required
+                label="Message *" />
+            </v-col>
+          </v-row>
+          <v-row dense>
+            <v-col cols="6">
+              <v-text-field
+                v-model="localMessage.display_start"
+                clearable
+                hint="YYYY-MM-dd HH:mm:ss"
+                label="Start timestamp" />
+            </v-col>
+            <v-col cols="6">
+              <v-text-field
+                v-model="localMessage.display_end"
+                clearable
+                hint="YYYY-MM-dd HH:mm:ss"
+                label="End timestamp" />
+            </v-col>
+          </v-row>
+        </v-card-text>
+        <v-card-actions>
+          <v-btn
+            v-if="isModification"
+            class="ml-2"
+            color="error"
+            @click="deleteMessage">
+            Delete
+          </v-btn>
+          <v-spacer />
+          <v-btn
+            class="mb-2"
+            @click="cancel">
+            Cancel
+          </v-btn>
+          <v-btn
+            id="database"
+            class="mb-2 ml-3 mr-2"
+            :disabled="!valid || loading"
+            :color="buttonColor"
+            type="submit"
+            :loading="loading"
+            @click="submitButton">
+            {{ buttonText }}
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-form>
+  </div>
+</template>
+
+<script>
+import MetadataService from '@/api/metadata.service'
+import { timestampToTimeZonedTimestamp, formatTimestampUTC } from '@/utils'
+
+export default {
+  props: {
+    id: {
+      type: Number,
+      default () {
+        return null
+      }
+    }
+  },
+  data () {
+    return {
+      valid: false,
+      loading: false,
+      error: false,
+      types: [
+        { name: 'Error', value: 'error' },
+        { name: 'Warning', value: 'warning' },
+        { name: 'Info', value: 'info' }
+      ],
+      localMessage: {
+        type: null,
+        message: null,
+        display_start: null,
+        display_end: null
+      },
+      modify: {
+        username: null,
+        type: null
+      }
+    }
+  },
+  computed: {
+    database () {
+      return this.$store.state.database
+    },
+    title () {
+      return (!this.isModification ? 'Create' : 'Modify') + ' maintenance message'
+    },
+    buttonColor () {
+      if (this.modify.type && this.modify.type === 'revoke') {
+        return 'error'
+      }
+      return 'secondary'
+    },
+    isModification () {
+      return this.id !== null
+    },
+    buttonText () {
+      return (this.isModification ? 'Modify' : 'Create') + ' message'
+    }
+  },
+  watch: {
+    id () {
+      this.init()
+    }
+  },
+  mounted () {
+    this.init()
+  },
+  methods: {
+    submit () {
+      this.$refs.form.validate()
+    },
+    cancel () {
+      this.$emit('close-dialog', { success: false })
+    },
+    init () {
+      if (!this.id) {
+        this.localMessage = {
+          type: null,
+          message: null,
+          display_start: null,
+          display_end: null
+        }
+      } else {
+        this.loadMessage(this.id)
+      }
+    },
+    loadMessage (id) {
+      MetadataService.findMessage(id)
+        .then((message) => {
+          message.display_start = formatTimestampUTC(message.display_start)
+          message.display_end = formatTimestampUTC(message.display_end)
+          this.localMessage = message
+        })
+    },
+    submitButton () {
+      if (this.isModification) {
+        this.updateMessage()
+      } else {
+        this.createMessage()
+      }
+    },
+    createMessage () {
+      this.loading = true
+      const payload = Object.assign({}, this.localMessage)
+      if (payload.display_start) {
+        payload.display_start = timestampToTimeZonedTimestamp(payload.display_start)
+      }
+      if (payload.display_end) {
+        payload.display_end = timestampToTimeZonedTimestamp(payload.display_end)
+      }
+      MetadataService.createMessage(payload)
+        .then(() => {
+          this.$emit('close-dialog', { success: true })
+          this.$emit('reload-messages', { success: true })
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    updateMessage () {
+      this.loading = true
+      const payload = Object.assign({}, this.localMessage)
+      delete payload.id
+      if (payload.display_start) {
+        payload.display_start = timestampToTimeZonedTimestamp(payload.display_start)
+      }
+      if (payload.display_end) {
+        payload.display_end = timestampToTimeZonedTimestamp(payload.display_end)
+      }
+      MetadataService.updateMessage(this.localMessage.id, payload)
+        .then(() => {
+          this.$emit('close-dialog', { success: true })
+          this.$emit('reload-messages', { success: true })
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    deleteMessage () {
+      this.loading = true
+      MetadataService.deleteMessage(this.localMessage.id)
+        .then(() => {
+          this.$emit('close-dialog', { success: true })
+          this.$emit('reload-messages', { success: true })
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    }
+  }
+}
+</script>
diff --git a/dbrepo-ui/layouts/default.vue b/dbrepo-ui/layouts/default.vue
index 8f9358cb23445b85c536ecd2a37662f3f8e0bbae..198c87bb14fa6e6109dc256a354448028a93a785 100644
--- a/dbrepo-ui/layouts/default.vue
+++ b/dbrepo-ui/layouts/default.vue
@@ -39,6 +39,16 @@
           </v-list-item-content>
         </v-list-item>
       </v-list>
+      <div id="messages">
+        <v-alert
+          v-for="(message, idx) in messages"
+          :key="idx"
+          class="banner"
+          border="left"
+          tile
+          :type="message.type"
+          v-text="message.messages" />
+      </div>
     </v-navigation-drawer>
     <v-form ref="form" @submit.prevent="submit">
       <v-app-bar fixed app>
@@ -70,8 +80,8 @@
             {{ $t('layout.signup', { name: 'vue-i18n' }) }}
           </v-btn>
         </div>
-        <div>
-          <v-btn v-if="user" to="/user" plain>
+        <div v-if="user">
+          <v-btn to="/user" plain>
             {{ user.username }}
           </v-btn>
           <v-menu bottom offset-y left>
@@ -105,19 +115,6 @@
         <nuxt />
       </v-container>
     </v-main>
-    <v-footer
-      v-if="sandbox"
-      padless>
-      <v-card
-        flat
-        tile
-        width="100%"
-        class="banner text-center">
-        <v-card-text class="black--text">
-          This is a <strong>TEST</strong> environment, do not use production/confidential data! — <a href="//github.com/fair-data-austria/dbrepo/issues/new" class="black--text">Report a bug</a>
-        </v-card-text>
-      </v-card>
-    </v-footer>
   </v-app>
 </template>
 
@@ -160,6 +157,9 @@ export default {
     locale () {
       return this.$store.state.locale
     },
+    messages () {
+      return this.$store.state.messages
+    },
     table () {
       return this.$store.state.table
     },
@@ -215,9 +215,7 @@ export default {
     }
   },
   mounted () {
-    if (this.refreshToken) {
-      AuthenticationService.authenticateToken(this.refreshToken)
-    }
+    this.$store.dispatch('reloadMessages')
     if (this.locale) {
       this.$i18n.locale = this.locale
     }
@@ -302,6 +300,16 @@ export default {
 }
 </script>
 <style>
+#messages {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+.banner {
+  width: 100%;
+  margin: 8px 0 0 0;
+}
 .search-result-title,
 .search-result-subtitle {
   overflow: hidden;
diff --git a/dbrepo-ui/nuxt.config.js b/dbrepo-ui/nuxt.config.js
index 9c06aff0c1b6dadcfebcf8354041a1a89ccb1562..9db2e6dc80c7f97dd3a17dca49d279c9bfc241a8 100644
--- a/dbrepo-ui/nuxt.config.js
+++ b/dbrepo-ui/nuxt.config.js
@@ -110,11 +110,10 @@ export default {
           primary: colors.blue.darken2,
           accent: colors.amber.darken3,
           secondary: colors.blueGrey.base,
-          info: colors.amber.lighten1,
+          info: colors.blue.lighten2,
           code: colors.grey.lighten4,
           warning: colors.orange.lighten2,
           error: colors.red.base /* is used by forms */,
-          banner: colors.red.lighten2,
           success: colors.teal.base
         },
         dark: {
diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue
index 36e9bcea2483b8cc10c8a5456b6be0242c8d154c..3abeb30d7c7cc34d070b973cddabe068e015a335 100644
--- a/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue
+++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/info.vue
@@ -213,9 +213,43 @@
                     <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" />
                     <span v-if="!loading" v-text="container_internal_name" />
                   </v-list-item-content>
+                  <v-list-item-title class="mt-2">
+                    Container IP
+                  </v-list-item-title>
+                  <v-list-item-content>
+                    <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" />
+                    <span v-if="!loading" v-text="container_ip" />
+                  </v-list-item-content>
+                  <v-list-item-title class="mt-2">
+                    Container State
+                  </v-list-item-title>
+                  <v-list-item-content>
+                    <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" />
+                    <span v-if="!loading" v-text="container_state" />
+                  </v-list-item-content>
                 </v-list-item-content>
               </v-list-item>
             </v-list>
+            <v-card-actions>
+              <v-btn
+                v-if="canStartContainer && needsStart"
+                small
+                secondary
+                color="secondary"
+                :loading="loadingStart"
+                @click.stop="startContainer">
+                Start Container
+              </v-btn>
+              <v-btn
+                v-if="canStopContainer && !needsStart"
+                small
+                secondary
+                color="error"
+                :loading="loadingStop"
+                @click.stop="stopContainer">
+                Stop Container
+              </v-btn>
+            </v-card-actions>
           </v-card-text>
         </v-card>
       </v-tab-item>
@@ -245,6 +279,7 @@ import { formatTimestampUTCLabel } from '@/utils'
 import Banner from '@/components/identifier/Banner'
 import DatabaseMapper from '@/api/database.mapper'
 import DeleteIdentifier from '@/components/dialogs/DeleteIdentifier.vue'
+import ContainerService from '@/api/container.service'
 
 export default {
   components: {
@@ -259,6 +294,8 @@ export default {
     return {
       loading: false,
       loadingDelete: false,
+      loadingStart: false,
+      loadingStop: false,
       editDialog: false,
       deleteDialog: false,
       persistDialog: false,
@@ -330,6 +367,12 @@ export default {
     container_internal_name () {
       return this.database.container.internal_name
     },
+    container_state () {
+      return this.database.container.state
+    },
+    container_ip () {
+      return this.database.container.ip_address
+    },
     showIdentifierCard () {
       if (this.hasIdentifier) {
         return true
@@ -348,6 +391,21 @@ export default {
       }
       return this.roles.includes('create-identifier') && this.isOwner
     },
+    canStartContainer () {
+      if (!this.roles) {
+        return false
+      }
+      if (this.roles.includes('modify-foreign-container-state')) {
+        return true
+      }
+      return this.roles.includes('modify-container-state') && this.isOwner
+    },
+    canStopContainer () {
+      if (!this.roles) {
+        return false
+      }
+      return this.roles.includes('modify-foreign-container-state')
+    },
     canEditIdentifier () {
       if (!this.roles || !this.hasIdentifier) {
         return false
@@ -410,6 +468,9 @@ export default {
         return false
       }
       return this.database.owner.username === this.user.username
+    },
+    needsStart () {
+      return !this.database.container.running
     }
   },
   methods: {
@@ -425,6 +486,30 @@ export default {
         await this.$store.dispatch('reloadDatabase')
       }
       this.deleteDialog = false
+    },
+    startContainer () {
+      this.loadingStart = true
+      return new Promise(() => {
+        ContainerService.modify(this.database.container.id, 'start')
+          .then(() => {
+            this.$store.dispatch('reloadDatabase')
+          })
+          .finally(() => {
+            this.loadingStart = false
+          })
+      })
+    },
+    stopContainer () {
+      this.loadingStop = true
+      return new Promise(() => {
+        ContainerService.modify(this.database.container.id, 'stop')
+          .then(() => {
+            this.$store.dispatch('reloadDatabase')
+          })
+          .finally(() => {
+            this.loadingStop = false
+          })
+      })
     }
   }
 }
diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue
index dcbe33966032f5b0c9bbfe9062d54ef259da85a9..eddbcca9f421c42e9b633547e3555325b896ada1 100644
--- a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue
+++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue
@@ -25,7 +25,7 @@
         </DownloadButton>
       </v-toolbar-title>
     </v-toolbar>
-    <v-card v-if="query && query.identifier" flat tile>
+    <v-card v-if="query.identifier" flat tile>
       <v-card-title>Identifier</v-card-title>
       <v-card-text>
         <v-list dense>
@@ -38,7 +38,7 @@
                 Persistent Identifier
               </v-list-item-title>
               <v-list-item-content>
-                <Banner :identifier="query.identifier" />
+                <Banner v-if="canPersistQuery" :identifier="query.identifier" />
               </v-list-item-content>
               <v-list-item-title class="mt-2">
                 Title
@@ -306,7 +306,7 @@ export default {
       return this.query.result_hash
     },
     canPersistQuery () {
-      if (!this.query || this.query.is_persisted) {
+      if (this.loadingQuery || !this.query || this.query.is_persisted) {
         return false
       }
       return UserUtils.hasReadAccess(this.access)
diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue
index dfd27fc763f40c273408f55263ea0ecd76647700..925403b70d519a406a8353fc407225bc663a9f40 100644
--- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue
+++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue
@@ -23,8 +23,14 @@
       </v-toolbar-title>
     </v-toolbar>
     <v-card tile>
-      <v-progress-linear v-if="loadingData > 0 || error" :value="loadProgress" :color="error ? 'error' : 'primary'" />
+      <v-progress-linear v-if="loadingData > 0 || error" :indeterminate="!error" :color="loadingColor" />
+      <v-card v-if="error" flat tile>
+        <v-card-text>
+          Failed to load table data: database is not reachable
+        </v-card-text>
+      </v-card>
       <v-data-table
+        v-if="!error"
         :headers="headers"
         :items="rows"
         :options.sync="options"
@@ -53,7 +59,6 @@ export default {
     return {
       loading: true,
       loadingData: 0,
-      loadProgress: 0,
       editTupleDialog: false,
       total: -1,
       footerProps: {
@@ -67,7 +72,8 @@ export default {
       version: null,
       lastReload: new Date(),
       tab: null,
-      error: false, // XXX: `error` is never changed
+      edit: false,
+      error: false,
       options: {
         page: 1,
         itemsPerPage: 10
@@ -85,7 +91,7 @@ export default {
   },
   computed: {
     loadingColor () {
-      return this.error ? 'red lighten-2' : 'primary'
+      return this.error ? 'error' : 'primary'
     },
     token () {
       return this.$store.state.token
@@ -168,7 +174,6 @@ export default {
   },
   mounted () {
     this.reload()
-    this.simulateProgress()
     this.loadProperties()
   },
   methods: {
@@ -277,6 +282,9 @@ export default {
             return row
           })
         })
+        .catch(() => {
+          this.error = true
+        })
         .finally(() => {
           this.loadingData--
         })
@@ -290,20 +298,6 @@ export default {
         .finally(() => {
           this.loadingData--
         })
-    },
-    simulateProgress () {
-      if (this.loadProgress !== 0) {
-        return
-      }
-      const timeout = 30 * 1000 /* ms */
-      const ticks = 100 /* ms */
-      let i = 0
-      setInterval(() => {
-        if (i++ >= timeout && !this.error) {
-          return
-        }
-        this.loadProgress = ((i * 100) / timeout) * 100
-      }, ticks)
     }
   }
 }
diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue
index 171c0e219e5d4d5e861ff06a8ed8e57c4faf2b4d..13fd4569163b0a6d08979ba84c2e660c654c80bd 100644
--- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue
+++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue
@@ -83,10 +83,6 @@
               </v-list-item-title>
               <v-list-item-content v-if="canWriteQueues" class="amqp-consumer">
                 <span v-text="`${consumersUp}/${consumersTotal}`" />
-                <v-badge
-                  class="ml-1"
-                  :color="consumersState.color"
-                  :content="consumersState.text" />
               </v-list-item-content>
             </v-list-item-content>
           </v-list-item>
@@ -154,15 +150,6 @@ export default {
     access () {
       return this.$store.state.access
     },
-    consumersState () {
-      if (this.consumersTotal === 0 || this.consumersTotal - this.consumersUp > 0 || this.loadingConsumers) {
-        return { color: 'warning', text: 'pending' }
-      }
-      if (this.consumersTotal === 0) {
-        return { color: 'error', text: 'down' }
-      }
-      return { color: 'success', text: 'up' }
-    },
     consumersTotal () {
       return this.consumers.length
     },
diff --git a/dbrepo-ui/pages/user/authentication.vue b/dbrepo-ui/pages/user/authentication.vue
index 92af24111141ae469c878c4e6eb8df2132e550e2..877ae8ce165781981e83898c878df80318f55584 100644
--- a/dbrepo-ui/pages/user/authentication.vue
+++ b/dbrepo-ui/pages/user/authentication.vue
@@ -40,7 +40,6 @@
                   </v-btn>
                 </v-col>
               </v-row>
-              <pre>{{ $refs.form3 }}</pre>
             </v-form>
           </v-card-text>
         </v-card>
diff --git a/dbrepo-ui/pages/user/developer.vue b/dbrepo-ui/pages/user/developer.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7825e5624307bd69ddea0c086d411adf3afd5e22
--- /dev/null
+++ b/dbrepo-ui/pages/user/developer.vue
@@ -0,0 +1,133 @@
+<template>
+  <div v-if="canHandleMessages">
+    <UserToolbar />
+    <v-tabs-items v-model="tab">
+      <v-tab-item>
+        <v-card flat tile>
+          <v-card-title>Maintenance Messages</v-card-title>
+          <v-data-table
+            :headers="headers"
+            :items="messages"
+            :loading="loadingMessages"
+            :items-per-page="10">
+            <template v-slot:item.action="{ item }">
+              <v-btn
+                x-small
+                @click="modifyMessage(item)">
+                Modify
+              </v-btn>
+            </template>
+          </v-data-table>
+          <v-card-text>
+            <v-btn
+              small
+              color="secondary"
+              :disabled="!canCreateMessage"
+              @click="createMessage">
+              Create Message
+            </v-btn>
+          </v-card-text>
+        </v-card>
+      </v-tab-item>
+    </v-tabs-items>
+    <v-dialog
+      v-model="dialog"
+      persistent
+      max-width="640">
+      <EditMaintenanceMessage :id="messageId" @close-dialog="closeDialog" />
+    </v-dialog>
+  </div>
+</template>
+
+<script>
+import UserToolbar from '@/components/UserToolbar'
+import MetadataService from '@/api/metadata.service'
+import EditMaintenanceMessage from '@/components/dialogs/EditMaintenanceMessage'
+import { isActiveMessage } from '@/utils'
+
+export default {
+  components: {
+    UserToolbar,
+    EditMaintenanceMessage
+  },
+  data () {
+    return {
+      tab: 0,
+      headers: [
+        { text: 'Active', value: 'active' },
+        { text: 'Type', value: 'type' },
+        { text: 'Message', value: 'message' },
+        { text: 'Action', value: 'action' }
+      ],
+      messages: [],
+      loadingMessages: false,
+      dialog: false,
+      messageId: null
+    }
+  },
+  computed: {
+    token () {
+      return this.$store.state.token
+    },
+    user () {
+      return this.$store.state.user
+    },
+    roles () {
+      return this.$store.state.roles
+    },
+    canCreateMessage () {
+      if (!this.roles) {
+        return false
+      }
+      return this.roles.includes('create-maintenance-message')
+    },
+    canModifyMessage () {
+      if (!this.roles) {
+        return false
+      }
+      return this.roles.includes('modify-maintenance-message')
+    },
+    canHandleMessages () {
+      return this.canCreateMessage || this.canModifyMessage
+    }
+  },
+  mounted () {
+    this.loadMessages()
+  },
+  methods: {
+    submit () {
+    },
+    modifyMessage (message) {
+      this.messageId = message.id
+      this.dialog = true
+    },
+    createMessage () {
+      this.messageId = null
+      this.dialog = true
+    },
+    loadMessages () {
+      MetadataService.findAllMessages()
+        .then((messages) => {
+          this.messages = messages.map((message) => {
+            message.active = isActiveMessage(message) ? '● true' : 'false'
+            message.action = 'hello'
+            return message
+          })
+        })
+        .catch(() => {
+          this.loadingMessages = false
+        })
+        .finally(() => {
+          this.loadingMessages = false
+        })
+    },
+    closeDialog (event) {
+      this.dialog = false
+      if (event.success) {
+        this.loadMessages()
+        this.$store.dispatch('reloadMessages')
+      }
+    }
+  }
+}
+</script>
diff --git a/dbrepo-ui/store/index.js b/dbrepo-ui/store/index.js
index e42e49e0d00ab0266b0b35aabab5c273a190146c..690814ed85e1e2528880a6a27a037f669f577136 100644
--- a/dbrepo-ui/store/index.js
+++ b/dbrepo-ui/store/index.js
@@ -3,6 +3,7 @@ import Vuex, { Store } from 'vuex'
 import UserService from '@/api/user.service'
 import DatabaseService from '@/api/database.service'
 import TableService from '@/api/table.service'
+import MetadataService from '@/api/metadata.service'
 
 Vue.use(Vuex)
 
@@ -16,7 +17,8 @@ const store = new Store({
     database: null,
     table: null,
     access: null,
-    locale: null
+    locale: null,
+    messages: []
   },
   getters: {
     getToken: state => state.token,
@@ -26,7 +28,8 @@ const store = new Store({
     getDatabase: state => state.database,
     getTable: state => state.table,
     getAccess: state => state.access,
-    getLocale: state => state.locale
+    getLocale: state => state.locale,
+    getMessages: state => state.messages
   },
   mutations: {
     SET_TOKEN (state, token) {
@@ -52,6 +55,9 @@ const store = new Store({
     },
     SET_LOCALE (state, locale) {
       state.locale = locale
+    },
+    SET_MESSAGES (state, messages) {
+      state.messages = messages
     }
   },
   actions: {
@@ -78,6 +84,12 @@ const store = new Store({
         .then((table) => {
           commit('SET_TABLE', table)
         })
+    },
+    reloadMessages ({ state, commit }) {
+      MetadataService.findActiveMessages()
+        .then((messages) => {
+          commit('SET_MESSAGES', messages)
+        })
     }
   }
 })
diff --git a/dbrepo-ui/utils/index.js b/dbrepo-ui/utils/index.js
index 01ed03767c643e6b57c5bf64c5c0c62710c3781e..f7b9f553b55076aa9a5846993a4ffc3376386299 100644
--- a/dbrepo-ui/utils/index.js
+++ b/dbrepo-ui/utils/index.js
@@ -99,6 +99,32 @@ function isOrcid (orcid) {
   return orcid.charAt(orcid.length - 1) === checksum
 }
 
+function isActiveMessage (message) {
+  if (!message) {
+    return false
+  }
+  if (message.display_start === null || message.display_end === null) {
+    return true
+  }
+  if (message.display_start === null || new Date(message.display_end) >= new Date()) {
+    return true
+  }
+  if (new Date(message.display_start) <= new Date() || new Date(message.display_end) >= new Date()) {
+    return true
+  }
+  if (new Date(message.display_start) <= new Date() || message.display_end === null) {
+    return true
+  }
+  return false
+}
+
+function timestampToTimeZonedTimestamp (str) {
+  if (str === null) {
+    return null
+  }
+  return format(new Date(str), 'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'')
+}
+
 module.exports = {
   notEmpty,
   formatTimestamp,
@@ -109,5 +135,7 @@ module.exports = {
   formatYearUTC,
   formatMonthUTC,
   formatDayUTC,
-  isOrcid
+  isOrcid,
+  isActiveMessage,
+  timestampToTimeZonedTimestamp
 }
diff --git a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java
new file mode 100644
index 0000000000000000000000000000000000000000..18fa41da61db5dc80d8870cb5bb2099ca5dbab81
--- /dev/null
+++ b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java
@@ -0,0 +1,161 @@
+package at.tuwien.endpoint;
+
+import at.tuwien.api.maintenance.BannerMessageBriefDto;
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageDto;
+import at.tuwien.api.maintenance.BannerMessageUpdateDto;
+import at.tuwien.api.user.UserBriefDto;
+import at.tuwien.exception.BannerMessageNotFoundException;
+import at.tuwien.mapper.BannerMessageMapper;
+import at.tuwien.service.BannerMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Log4j2
+@CrossOrigin(origins = "*")
+@RestController
+@RequestMapping("/api/maintenance")
+public class MaintenanceEndpoint {
+
+    private final BannerMessageMapper bannerMessageMapper;
+    private final BannerMessageService bannerMessageService;
+
+    @Autowired
+    public MaintenanceEndpoint(BannerMessageMapper bannerMessageMapper, BannerMessageService bannerMessageService) {
+        this.bannerMessageMapper = bannerMessageMapper;
+        this.bannerMessageService = bannerMessageService;
+    }
+
+    @GetMapping("/message")
+    @Operation(summary = "Find maintenance messages")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200",
+                    description = "List messages",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageDto[].class))}),
+    })
+    public ResponseEntity<List<BannerMessageDto>> list() {
+        log.debug("endpoint list active maintenance messages");
+        final List<BannerMessageDto> dtos = bannerMessageService.findAll()
+                .stream()
+                .map(bannerMessageMapper::bannerMessageToBannerMessageDto)
+                .toList();
+        log.trace("list maintenance messages results in dtos {}", dtos);
+        return ResponseEntity.ok(dtos);
+    }
+
+    @GetMapping("/message/{id}")
+    @Operation(summary = "Find one maintenance message")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200",
+                    description = "Get messages",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageDto.class))}),
+    })
+    public ResponseEntity<BannerMessageDto> find(@NotNull @PathVariable("id") Long messageId)
+            throws BannerMessageNotFoundException {
+        log.debug("endpoint find one maintenance messages");
+        final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.find(messageId));
+        log.trace("find one maintenance message results in dto {}", dto);
+        return ResponseEntity.ok(dto);
+    }
+
+    @GetMapping("/message/active")
+    @Operation(summary = "Find active maintenance messages")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200",
+                    description = "List messages",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageBriefDto[].class))}),
+    })
+    public ResponseEntity<List<BannerMessageBriefDto>> active() {
+        log.debug("endpoint list active maintenance messages");
+        final List<BannerMessageBriefDto> dtos = bannerMessageService.getActive()
+                .stream()
+                .map(bannerMessageMapper::bannerMessageToBannerMessageBriefDto)
+                .toList();
+        log.trace("list active maintenance messages results in dtos {}", dtos);
+        return ResponseEntity.ok(dtos);
+    }
+
+    @PostMapping("/message")
+    @Operation(summary = "Create maintenance message")
+    @PreAuthorize("hasAuthority('create-maintenance-message')")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "201",
+                    description = "Created message",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageBriefDto.class))}),
+    })
+    public ResponseEntity<BannerMessageDto> create(@Valid @RequestBody BannerMessageCreateDto data) {
+        log.debug("endpoint create maintenance message, data={}", data);
+        final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.create(data));
+        log.trace("create maintenance message results in dto {}", dto);
+        return ResponseEntity.status(HttpStatus.CREATED)
+                .body(dto);
+    }
+
+    @PutMapping("/message/{id}")
+    @Operation(summary = "Update maintenance message")
+    @PreAuthorize("hasAuthority('update-maintenance-message')")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "202",
+                    description = "Updated message",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageBriefDto.class))}),
+            @ApiResponse(responseCode = "404",
+                    description = "Could not find message",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageNotFoundException.class))}),
+    })
+    public ResponseEntity<BannerMessageDto> update(@NotNull @PathVariable("id") Long messageId,
+                                                        @Valid @RequestBody BannerMessageUpdateDto data)
+            throws BannerMessageNotFoundException {
+        log.debug("endpoint update maintenance message, messageId={}, data={}", messageId, data);
+        final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.update(messageId, data));
+        log.trace("update maintenance message results in dto {}", dto);
+        return ResponseEntity.status(HttpStatus.ACCEPTED)
+                .body(dto);
+    }
+
+    @DeleteMapping("/message/{id}")
+    @Operation(summary = "Delete maintenance message")
+    @PreAuthorize("hasAuthority('delete-maintenance-message')")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "202",
+                    description = "Deleted message",
+                    content = {@Content(mediaType = "application/json")}),
+            @ApiResponse(responseCode = "404",
+                    description = "Could not find message",
+                    content = {@Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = BannerMessageNotFoundException.class))}),
+    })
+    public ResponseEntity<?> delete(@NotNull @PathVariable("id") Long messageId)
+            throws BannerMessageNotFoundException {
+        log.debug("endpoint delete maintenance message, messageId={}", messageId);
+        bannerMessageService.delete(messageId);
+        return ResponseEntity.status(HttpStatus.ACCEPTED)
+                .build();
+    }
+
+}
diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/MaintenanceEndpointUnitTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/MaintenanceEndpointUnitTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..36097258c132aa0525a1d5c180ca150fc9709d68
--- /dev/null
+++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/MaintenanceEndpointUnitTest.java
@@ -0,0 +1,352 @@
+package at.tuwien.endpoint;
+
+import at.tuwien.BaseUnitTest;
+import at.tuwien.api.maintenance.BannerMessageBriefDto;
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageDto;
+import at.tuwien.api.maintenance.BannerMessageUpdateDto;
+import at.tuwien.config.ReadyConfig;
+import at.tuwien.entities.maintenance.BannerMessage;
+import at.tuwien.exception.*;
+import at.tuwien.service.BannerMessageService;
+import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@Log4j2
+@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class)
+@SpringBootTest
+@ExtendWith(SpringExtension.class)
+public class MaintenanceEndpointUnitTest extends BaseUnitTest {
+
+    @MockBean
+    private ReadyConfig readyConfig;
+
+    @MockBean
+    private BannerMessageService bannerMessageService;
+
+    @Autowired
+    private MaintenanceEndpoint maintenanceEndpoint;
+
+    @Test
+    @WithAnonymousUser
+    public void list_anonymous_succeeds() {
+
+        /* test */
+        list_generic();
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void list_noRole_succeeds() {
+
+        /* test */
+        list_generic();
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"list-maintenance-messages"})
+    public void list_hasRole_succeeds() {
+
+        /* test */
+        list_generic();
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void find_anonymous_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        find_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void find_noRole_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        find_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"find-maintenance-message"})
+    public void find_hasRole_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        find_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"find-maintenance-message"})
+    public void find_hasRoleNotFound_fails() {
+
+        /* test */
+        assertThrows(BannerMessageNotFoundException.class, () -> {
+            find_generic(BANNER_MESSAGE_1_ID, null);
+        });
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void active_anonymous_succeeds() {
+
+        /* test */
+        active_generic();
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void active_noRole_succeeds() {
+
+        /* test */
+        active_generic();
+    }
+
+    @Test
+    @WithMockUser(username = USER_1_USERNAME, authorities = {"list-maintenance-messages"})
+    public void active_hasRole_succeeds() {
+
+        /* test */
+        active_generic();
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void create_anonymous_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            create_generic(BANNER_MESSAGE_1_CREATE_DTO, BANNER_MESSAGE_1);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void create_noRole_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            create_generic(BANNER_MESSAGE_1_CREATE_DTO, BANNER_MESSAGE_1);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"create-maintenance-message"})
+    public void create_hasRole_succeeds() {
+
+        /* test */
+        create_generic(BANNER_MESSAGE_1_CREATE_DTO, BANNER_MESSAGE_1);
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void update_anonymous_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void update_noRole_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"update-maintenance-message"})
+    public void update_hasRole_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"update-maintenance-message"})
+    public void update_hasRoleNotFound_fails() {
+
+        /* test */
+        assertThrows(BannerMessageNotFoundException.class, () -> {
+            update_generic(BANNER_MESSAGE_1_UPDATE_DTO, BANNER_MESSAGE_1_ID, null);
+        });
+    }
+
+    @Test
+    @WithAnonymousUser
+    public void delete_anonymous_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            delete_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_4_USERNAME)
+    public void delete_noRole_fails() {
+
+        /* test */
+        assertThrows(AccessDeniedException.class, () -> {
+            delete_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+        });
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-maintenance-message"})
+    public void delete_hasRole_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        delete_generic(BANNER_MESSAGE_1_ID, BANNER_MESSAGE_1);
+    }
+
+    @Test
+    @WithMockUser(username = USER_2_USERNAME, authorities = {"delete-maintenance-message"})
+    public void delete_hasRoleNotFound_fails() {
+
+        /* test */
+        assertThrows(BannerMessageNotFoundException.class, () -> {
+            delete_generic(BANNER_MESSAGE_1_ID, null);
+        });
+    }
+
+    /* ################################################################################################### */
+    /* ## GENERIC TEST CASES                                                                            ## */
+    /* ################################################################################################### */
+
+    protected void list_generic() {
+
+        /* mock */
+        when(bannerMessageService.findAll())
+                .thenReturn(List.of(BANNER_MESSAGE_1, BANNER_MESSAGE_2));
+
+        /* test */
+        final ResponseEntity<List<BannerMessageDto>> response = maintenanceEndpoint.list();
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertNotNull(response.getBody());
+        final List<BannerMessageDto> body = response.getBody();
+        assertEquals(2, body.size());
+        final BannerMessageDto message0 = body.get(0);
+        assertEquals(BANNER_MESSAGE_1_ID, message0.getId());
+        assertEquals(BANNER_MESSAGE_1_TYPE_DTO, message0.getType());
+        assertEquals(BANNER_MESSAGE_1_MESSAGE, message0.getMessage());
+        final BannerMessageDto message1 = body.get(1);
+        assertEquals(BANNER_MESSAGE_2_ID, message1.getId());
+        assertEquals(BANNER_MESSAGE_2_TYPE_DTO, message1.getType());
+        assertEquals(BANNER_MESSAGE_2_MESSAGE, message1.getMessage());
+    }
+
+    protected void find_generic(Long messageId, BannerMessage message) throws BannerMessageNotFoundException {
+
+        /* mock */
+        if (message != null) {
+            when(bannerMessageService.find(messageId))
+                    .thenReturn(message);
+        } else {
+            doThrow(BannerMessageNotFoundException.class)
+                    .when(bannerMessageService)
+                    .find(messageId);
+        }
+
+        /* test */
+        final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.find(messageId);
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertNotNull(response.getBody());
+        final BannerMessageDto body = response.getBody();
+        assertEquals(BANNER_MESSAGE_1_ID, body.getId());
+        assertEquals(BANNER_MESSAGE_1_MESSAGE, body.getMessage());
+        assertEquals(BANNER_MESSAGE_1_TYPE_DTO, body.getType());
+        assertEquals(BANNER_MESSAGE_1_START, body.getDisplayStart());
+        assertEquals(BANNER_MESSAGE_1_END, body.getDisplayEnd());
+    }
+
+    protected void active_generic() {
+
+        /* mock */
+        when(bannerMessageService.getActive())
+                .thenReturn(List.of(BANNER_MESSAGE_1));
+
+        /* test */
+        final ResponseEntity<List<BannerMessageBriefDto>> response = maintenanceEndpoint.active();
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertNotNull(response.getBody());
+        final List<BannerMessageBriefDto> body = response.getBody();
+        assertEquals(1, body.size());
+        final BannerMessageBriefDto message0 = body.get(0);
+        assertEquals(BANNER_MESSAGE_1_TYPE_DTO, message0.getType());
+        assertEquals(BANNER_MESSAGE_1_MESSAGE, message0.getMessage());
+    }
+
+    protected void create_generic(BannerMessageCreateDto data, BannerMessage message) {
+
+        /* mock */
+        when(bannerMessageService.create(data))
+                .thenReturn(message);
+
+        /* test */
+        final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.create(data);
+        assertEquals(HttpStatus.CREATED, response.getStatusCode());
+        assertNotNull(response.getBody());
+    }
+
+    protected void update_generic(BannerMessageUpdateDto data, Long messageId, BannerMessage message)
+            throws BannerMessageNotFoundException {
+
+        /* mock */
+        if (message != null) {
+            when(bannerMessageService.update(messageId, data))
+                    .thenReturn(message);
+        } else {
+            doThrow(BannerMessageNotFoundException.class)
+                    .when(bannerMessageService)
+                    .update(messageId, data);
+        }
+
+        /* test */
+        final ResponseEntity<BannerMessageDto> response = maintenanceEndpoint.update(messageId, data);
+        assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
+        assertNotNull(response.getBody());
+    }
+
+    protected void delete_generic(Long messageId, BannerMessage message)
+            throws BannerMessageNotFoundException {
+
+        /* mock */
+        if (message != null) {
+            when(bannerMessageService.find(messageId))
+                    .thenReturn(message);
+            doNothing()
+                    .when(bannerMessageService)
+                    .delete(messageId);
+        } else {
+            doThrow(BannerMessageNotFoundException.class)
+                    .when(bannerMessageService)
+                    .delete(messageId);
+        }
+
+        /* test */
+        final ResponseEntity<?> response = maintenanceEndpoint.delete(messageId);
+        assertEquals(HttpStatus.ACCEPTED, response.getStatusCode());
+        assertNull(response.getBody());
+    }
+}
diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8759d8e3c684e7df97c79adcfa756e3b1a220f14
--- /dev/null
+++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/BannerMessageServiceIntegrationTest.java
@@ -0,0 +1,141 @@
+package at.tuwien.service;
+
+import at.tuwien.BaseUnitTest;
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageTypeDto;
+import at.tuwien.api.maintenance.BannerMessageUpdateDto;
+import at.tuwien.entities.maintenance.BannerMessage;
+import at.tuwien.entities.maintenance.BannerMessageType;
+import at.tuwien.entities.user.Realm;
+import at.tuwien.exception.BannerMessageNotFoundException;
+import at.tuwien.exception.RealmNotFoundException;
+import at.tuwien.repository.jpa.BannerMessageRepository;
+import at.tuwien.repository.jpa.RealmRepository;
+import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Log4j2
+@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+@SpringBootTest
+@ExtendWith(SpringExtension.class)
+public class BannerMessageServiceIntegrationTest extends BaseUnitTest {
+
+    @Autowired
+    private BannerMessageRepository bannerMessageRepository;
+
+    @Autowired
+    private BannerMessageService bannerMessageService;
+
+    @BeforeEach
+    public void beforeEach() {
+        bannerMessageRepository.save(BANNER_MESSAGE_1);
+        bannerMessageRepository.save(BANNER_MESSAGE_2);
+    }
+
+    @Test
+    public void findAll_succeeds() {
+
+        /* test */
+        final List<BannerMessage> response = bannerMessageService.findAll();
+        assertEquals(2, response.size());
+    }
+
+    @Test
+    public void getActive_succeeds() {
+
+        /* test */
+        final List<BannerMessage> response = bannerMessageService.getActive();
+        assertEquals(1, response.size());
+        final BannerMessage message0 = response.get(0);
+        assertEquals(BANNER_MESSAGE_1_ID, message0.getId());
+        assertEquals(BANNER_MESSAGE_1_MESSAGE, message0.getMessage());
+        assertEquals(BANNER_MESSAGE_1_TYPE, message0.getType());
+    }
+
+    @Test
+    public void find_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        final BannerMessage response = bannerMessageService.find(BANNER_MESSAGE_1_ID);
+        assertEquals(BANNER_MESSAGE_1_ID, response.getId());
+        assertEquals(BANNER_MESSAGE_1_MESSAGE, response.getMessage());
+        assertEquals(BANNER_MESSAGE_1_TYPE, response.getType());
+    }
+
+    @Test
+    public void find_notFound_fails() {
+
+        /* test */
+        assertThrows(BannerMessageNotFoundException.class, () -> {
+            bannerMessageService.find(9999L);
+        });
+    }
+
+    @Test
+    public void create_succeeds() {
+        final BannerMessageCreateDto request = BannerMessageCreateDto.builder()
+                .message("test")
+                .type(BannerMessageTypeDto.INFO)
+                .build();
+
+        /* test */
+        final BannerMessage response = bannerMessageService.create(request);
+        assertEquals("test", response.getMessage());
+        assertEquals(BannerMessageType.INFO, response.getType());
+    }
+
+    @Test
+    public void update_succeeds() throws BannerMessageNotFoundException {
+        final BannerMessageUpdateDto request = BannerMessageUpdateDto.builder()
+                .message("test")
+                .type(BannerMessageTypeDto.INFO)
+                .build();
+
+        /* test */
+        final BannerMessage response = bannerMessageService.update(BANNER_MESSAGE_1_ID, request);
+        assertEquals("test", response.getMessage());
+        assertEquals(BannerMessageType.INFO, response.getType());
+    }
+
+    @Test
+    public void update_notFound_fails() {
+        final BannerMessageUpdateDto request = BannerMessageUpdateDto.builder()
+                .message("test")
+                .type(BannerMessageTypeDto.INFO)
+                .build();
+
+        /* test */
+        assertThrows(BannerMessageNotFoundException.class, () -> {
+            bannerMessageService.update(9999L, request);
+        });
+    }
+
+    @Test
+    public void delete_succeeds() throws BannerMessageNotFoundException {
+
+        /* test */
+        bannerMessageService.delete(BANNER_MESSAGE_1_ID);
+    }
+
+    @Test
+    public void delete_notFound_fails() {
+
+        /* test */
+        assertThrows(BannerMessageNotFoundException.class, () -> {
+            bannerMessageService.delete(9999L);
+        });
+    }
+}
diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
index 3f1cf7c1ce8ee31ca780df5040443152d00c915b..fe44809a4407c9c4e8f3b9a9e89222894a3da6d2 100644
--- a/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
+++ b/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java
@@ -43,6 +43,7 @@ public class WebSecurityConfig {
         final OrRequestMatcher publicEndpoints = new OrRequestMatcher(
                 new AntPathRequestMatcher("/api/user/**", "GET"),
                 new AntPathRequestMatcher("/api/user/**", "POST"),
+                new AntPathRequestMatcher("/api/maintenance/**", "GET"),
                 new AntPathRequestMatcher("/v3/api-docs.yaml"),
                 new AntPathRequestMatcher("/v3/api-docs/**"),
                 new AntPathRequestMatcher("/swagger-ui/**"),
diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4587b14539c329f409be25e5e67b2651771be88
--- /dev/null
+++ b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java
@@ -0,0 +1,20 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class BannerMessageNotFoundException extends Exception {
+
+    public BannerMessageNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public BannerMessageNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public BannerMessageNotFoundException(Throwable thr) {
+        super(thr);
+    }
+}
diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java b/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..8fd2b7dc51936768af6e2d9410887c15e6f19c1b
--- /dev/null
+++ b/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java
@@ -0,0 +1,24 @@
+package at.tuwien.mapper;
+
+import at.tuwien.api.maintenance.BannerMessageBriefDto;
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageDto;
+import at.tuwien.api.maintenance.BannerMessageTypeDto;
+import at.tuwien.entities.maintenance.BannerMessage;
+import at.tuwien.entities.maintenance.BannerMessageType;
+import org.mapstruct.Mapper;
+
+@Mapper(componentModel = "spring")
+public interface BannerMessageMapper {
+
+    org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BannerMessageMapper.class);
+
+    BannerMessageDto bannerMessageToBannerMessageDto(BannerMessage data);
+
+    BannerMessageBriefDto bannerMessageToBannerMessageBriefDto(BannerMessage data);
+
+    BannerMessage bannerMessageCreateDtoToBannerMessage(BannerMessageCreateDto data);
+
+    BannerMessageType bannerMessageTypeDtoToBannerMessageType(BannerMessageTypeDto data);
+
+}
diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java b/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..bcdbf9f83266e10f0fab4f59126ffa936bb9b537
--- /dev/null
+++ b/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java
@@ -0,0 +1,14 @@
+package at.tuwien.repository.jpa;
+
+import at.tuwien.entities.maintenance.BannerMessage;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface BannerMessageRepository extends JpaRepository<BannerMessage, Long> {
+
+    List<BannerMessage> findByActive();
+
+}
diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java
new file mode 100644
index 0000000000000000000000000000000000000000..a674fbbbdd494f9999bf1bcdcc92ff68e892a465
--- /dev/null
+++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java
@@ -0,0 +1,60 @@
+package at.tuwien.service;
+
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageUpdateDto;
+import at.tuwien.entities.maintenance.BannerMessage;
+import at.tuwien.exception.BannerMessageNotFoundException;
+
+import java.util.List;
+
+public interface BannerMessageService {
+
+    /**
+     * Finds all messages in the metadata database.
+     *
+     * @return List of messages.
+     */
+    List<BannerMessage> findAll();
+
+    /**
+     * Finds all messages that are valid at the current point in time.
+     *
+     * @return List of active messages.
+     */
+    List<BannerMessage> getActive();
+
+    /**
+     * Finds a specific message by given id in the metadata database.
+     *
+     * @param id The message id.
+     * @return The message, if successful.
+     * @throws BannerMessageNotFoundException The message was not found in the metadata database.
+     */
+    BannerMessage find(Long id) throws BannerMessageNotFoundException;
+
+    /**
+     * Creates a new maintenance message in the metadata database.
+     *
+     * @param data The message data.
+     * @return The created message, if successful.
+     */
+    BannerMessage create(BannerMessageCreateDto data);
+
+    /**
+     * Updates a maintenance message by given id in the metadata database.
+     *
+     * @param id   The message id.
+     * @param data The updated message data.
+     * @return The updated message, if successful.
+     * @throws BannerMessageNotFoundException The message was not found in the metadata database.
+     */
+    BannerMessage update(Long id, BannerMessageUpdateDto data) throws BannerMessageNotFoundException;
+
+    /**
+     * Deletes a maintenance message by given id in the metadata database.
+     *
+     * @param id The message id.
+     * @throws BannerMessageNotFoundException The message was not found in the metadata database.
+     */
+    void delete(Long id) throws BannerMessageNotFoundException;
+}
diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c38de25fcf98665ee2d3945a35fd2082837256d
--- /dev/null
+++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java
@@ -0,0 +1,78 @@
+package at.tuwien.service.impl;
+
+import at.tuwien.api.maintenance.BannerMessageCreateDto;
+import at.tuwien.api.maintenance.BannerMessageUpdateDto;
+import at.tuwien.entities.maintenance.BannerMessage;
+import at.tuwien.exception.BannerMessageNotFoundException;
+import at.tuwien.mapper.BannerMessageMapper;
+import at.tuwien.repository.jpa.BannerMessageRepository;
+import at.tuwien.service.BannerMessageService;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Log4j2
+@Service
+public class BannerMessageServiceImpl implements BannerMessageService {
+
+    private final BannerMessageMapper bannerMessageMapper;
+    private final BannerMessageRepository bannerMessageRepository;
+
+    @Autowired
+    public BannerMessageServiceImpl(BannerMessageMapper bannerMessageMapper,
+                                    BannerMessageRepository bannerMessageRepository) {
+        this.bannerMessageMapper = bannerMessageMapper;
+        this.bannerMessageRepository = bannerMessageRepository;
+    }
+
+    @Override
+    public List<BannerMessage> findAll() {
+        return bannerMessageRepository.findAll();
+    }
+
+    @Override
+    public List<BannerMessage> getActive() {
+        return bannerMessageRepository.findByActive();
+    }
+
+    @Override
+    public BannerMessage find(Long id) throws BannerMessageNotFoundException {
+        final Optional<BannerMessage> optional = bannerMessageRepository.findById(id);
+        if (optional.isEmpty()) {
+            log.error("Failed to find banner message with id {}", id);
+            throw new BannerMessageNotFoundException("Failed to find banner message with id " + id);
+        }
+        return optional.get();
+    }
+
+    @Override
+    public BannerMessage create(BannerMessageCreateDto data) {
+        final BannerMessage entity = bannerMessageMapper.bannerMessageCreateDtoToBannerMessage(data);
+        final BannerMessage message = bannerMessageRepository.save(entity);
+        log.info("Created banner message with id {}", message.getId());
+        return message;
+    }
+
+    @Override
+    public BannerMessage update(Long id, BannerMessageUpdateDto data) throws BannerMessageNotFoundException {
+        final BannerMessage entity = find(id);
+        entity.setMessage(data.getMessage());
+        entity.setDisplayEnd(data.getDisplayEnd());
+        entity.setDisplayStart(data.getDisplayStart());
+        entity.setType(bannerMessageMapper.bannerMessageTypeDtoToBannerMessageType(data.getType()));
+        final BannerMessage message = bannerMessageRepository.save(entity);
+        log.info("Updated banner message with id {}", message.getId());
+        return message;
+    }
+
+    @Override
+    public void delete(Long id) throws BannerMessageNotFoundException {
+        find(id);
+        bannerMessageRepository.deleteById(id);
+        log.info("Deleted banner message with id {}", id);
+    }
+
+}
diff --git a/dbrepo.conf b/dbrepo.conf
index e972bfcd581b9c95d82ac18eb99870a197df5a56..4220e14b6820ec93a8f500ac35bb57cd600ee411 100644
--- a/dbrepo.conf
+++ b/dbrepo.conf
@@ -105,6 +105,15 @@ server {
         proxy_read_timeout      90;
     }
 
+    location /api/maintenance {
+        proxy_set_header        Host $host;
+        proxy_set_header        X-Real-IP $remote_addr;
+        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header        X-Forwarded-Proto $scheme;
+        proxy_pass              http://user;
+        proxy_read_timeout      90;
+    }
+
     location /api/identifier {
         proxy_set_header        Host $host;
         proxy_set_header        X-Real-IP $remote_addr;