diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..415565666428bb46865793c121d4d28793d3c927
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,13 @@
+FROM "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye"
+
+RUN apt-get update && \
+    apt-get upgrade -y && \
+    apt-get install -y \
+    libev-dev \
+    zlib1g-dev \
+    libjpeg-dev
+
+RUN pip install --upgrade pip && \
+    pip install bjoern tox setuptools
+
+CMD ["sleep", "infinity"]
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000000000000000000000000000000000..233ca1055ca148b2a5db4381e36fcb010c12da25
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,19 @@
+{
+	"name": "amivapi devcontainer setup",
+	"dockerComposeFile": "docker-compose.yml",
+	"service": "amivapi",
+	"workspaceFolder": "/api",
+	"features": {
+		"ghcr.io/devcontainers/features/git:1": {},
+		"ghcr.io/devcontainers-extra/features/tox:2": {}
+	},
+	"postCreateCommand": "pip install -r requirements.txt && pip install -e /api/.",
+	"customizations": {
+		"vscode": {
+			"extensions": [
+				"ms-python.flake8",
+				"ms-python.python"
+			]
+		}
+	}
+}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e555a1545967e0eef1dbcb5b822becd9e4aa4bb0
--- /dev/null
+++ b/.devcontainer/docker-compose.yml
@@ -0,0 +1,36 @@
+version: "3"
+services:
+  amivapi:
+    build:
+      context: .
+    ports:
+      - "5000:5000"
+    volumes:
+      - ../:/api:cached
+    environment:
+      - AMIVAPI_CONFIG=/api/dev_config.py
+    depends_on:
+      - mongodb
+
+  mongodb:
+    image: mongo:5.0.14
+    ports:
+      - "27017:27017"
+    volumes:
+      - ../dev_mongoinit.js:/docker-entrypoint-initdb.d/dev_mongoinit.js:ro
+
+  admintool:
+    image: amiveth/admintool:local
+    ports:
+      - "9000:80"
+
+  mongo-express:
+    image: mongo-express
+    ports:
+      - "8081:8081"
+    environment:
+      - ME_CONFIG_MONGODB_URL=mongodb://mongodb:27017
+      - ME_CONFIG_BASICAUTH_USERNAME=admin
+      - ME_CONFIG_BASICAUTH_PASSWORD=admin
+    depends_on:
+      - mongodb
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 2785977fab461c1a1a322d40781b6154925f8a15..0000000000000000000000000000000000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-version: 2
-updates:
-  # Maintain dependencies for GitHub Actions
-  - package-ecosystem: "github-actions"
-    directory: "/"
-    schedule:
-      interval: "monthly"
-    groups:
-      all:
-        patterns:
-          - "*"
-  - package-ecosystem: "docker"
-    directory: "/"
-    schedule:
-      interval: "weekly"
-  - package-ecosystem: "pip"
-    directory: "/"
-    schedule:
-      interval: "weekly"
-    groups:
-      eve:
-        patterns:
-          - "eve"
-          - "flask"
-          - "pymongo"
-      test:
-        patterns:
-          - "pytest*"
-          - "tox"
-          - "flake8"
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
deleted file mode 100644
index 825f0cd2f17a97b2db234bf667cd24636e00c9e8..0000000000000000000000000000000000000000
--- a/.github/workflows/cd.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: CD
-
-on:
-  workflow_run:
-    workflows: [CI]
-    branches: [master]
-    types: 
-      - completed
-
-jobs:
-  deploy:
-    runs-on: ubuntu-latest
-    container: amiveth/service-update-helper:latest
-    if: ${{ github.event.workflow_run.conclusion == 'success' }}
-
-    strategy:
-      matrix:
-        deploy-url:
-          - https://deploy-cluster.amiv.ethz.ch
-          - https://deploy-fallback.amiv.ethz.ch
-        deploy-service:
-          - amivapi
-          - amivapi-cron
-          - amivapi-dev
-          - amivapi-dev-cron
-        exclude:
-          - deploy-url: 'https://deploy-fallback.amiv.ethz.ch'
-            deploy-service: 'amivapi-dev'
-          - deploy-url: 'https://deploy-fallback.amiv.ethz.ch'
-            deploy-service: 'amivapi-dev-cron'
-
-    env:
-      CI_DEPLOY_URL: ${{ matrix.deploy-url }}
-      CI_DEPLOY_SERVICE: ${{ matrix.deploy-service }}
-      CI_DEPLOY_TOKEN: ${{ secrets.CI_DEPLOY_TOKEN }}
-
-    steps:
-      - run: /update.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 3da3c301c1a690d9cf675e1c94c7f95534782424..0000000000000000000000000000000000000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,91 +0,0 @@
-name: CI
-
-on:
-  schedule:
-    - cron: '5 4 * * 1'
-  push:
-    branches: [ master ]
-  pull_request:
-
-jobs:
-  test:
-    # Due to drop of out-of-the-box support in ubuntu-22.04 for MongoDB 5.0,
-    # we cannot use ubuntu-latest. have to consider this in the future. as ubuntu-20.04 will be OBSOLETE in 2025.
-    runs-on: ubuntu-20.04
-    strategy:
-      matrix:
-        python-version: ['3.10','3.12']
-
-    services:
-      mongodb:
-        image: mongo:5.0.14
-        env:
-          MONGO_INITDB_DATABASE: amivapi
-        options: >-
-          --health-cmd mongo
-          --health-start-period 20s
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          - 27017:27017
-
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install tox tox-gh-actions setuptools
-      - name: Create MongoDB User
-        run: mongo test_amivapi --eval 'db.createUser({user:"test_user",pwd:"test_pw",roles:["readWrite"]});'
-      - name: Test with tox
-        run: tox
-      - uses: codecov/codecov-action@v4
-        with:
-          token: ${{ secrets.CODECOV_TOKEN }}
-          files: ./coverage.xml
-          flags: unittests
-          fail_ci_if_error: true
-          verbose: true
-
-
-  build:
-    runs-on: ubuntu-latest
-    needs: test
-
-    env:
-      IMAGE_NAME: amiveth/amivapi
-
-    steps:
-      - uses: actions/checkout@v4
-      # Workaround: https://github.com/docker/build-push-action/issues/461
-      - name: Setup Docker buildx
-        uses: docker/setup-buildx-action@v3
-      # Login against a Docker registry except on PR
-      - name: Log into Docker Hub registry
-        if: github.event_name != 'pull_request'
-        uses: docker/login-action@v3
-        with:
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-      # Extract metadata (tags, labels) for Docker
-      - name: Extract Docker metadata
-        id: meta
-        uses: docker/metadata-action@v5
-        with:
-          images: ${{ env.IMAGE_NAME }}
-          tags: type=raw,value=latest
-      # Build and push Docker image with Buildx (don't push on PR)
-      - name: Build and push Docker image
-        id: build-and-push
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          pull: true
-          push: ${{ github.event_name != 'pull_request' }}
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3451d4baff6df87232ff2501241480e211b63056
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,42 @@
+stages:
+  - test
+  - build
+  - deploy
+
+test:
+  stage: test
+  image: python:3.12
+  services:
+    - name: mongo:5.0.14
+      alias: mongodb
+  before_script:
+    - pip install --upgrade pip
+    - pip install tox
+  script:
+    - tox
+
+build_master:
+  stage: build
+  image: docker:latest
+  before_script:
+    - echo "$CI_DOCKER_REGISTRY_TOKEN" | docker login -u "$CI_DOCKER_REGISTRY_USER" --password-stdin
+  script:
+    - docker build --pull -t "$CI_REGISTRY_IMAGE" ./
+    - docker push "$CI_REGISTRY_IMAGE"
+  only:
+    - master
+
+build_dev:
+  stage: build
+  image: docker:stable
+  before_script:
+    - echo "$CI_DOCKER_REGISTRY_TOKEN_DEV" | docker login -u "$CI_DOCKER_REGISTRY_USER_DEV" --password-stdin
+  script:
+    - docker build --pull -t "$CI_REGISTRY_IMAGE_DEV" ./
+    - docker push "$CI_REGISTRY_IMAGE_DEV"
+
+deploy:
+  stage: deploy
+  image: amiveth/ansible-ci-helper
+  script:
+    - python /main.py
diff --git a/amivapi/tests/config.py b/amivapi/tests/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..771b0bf50cc79f0437aea21f9b12ced787daabe7
--- /dev/null
+++ b/amivapi/tests/config.py
@@ -0,0 +1,30 @@
+""" Config file used for tox tests.
+It is called by the utils.py setUp function. """
+from passlib.context import CryptContext
+import logging
+
+# Do not change this has to be the same as in tests/utils.py
+MONGO_HOST = 'mongodb'
+MONGO_PORT = 27017
+MONGO_DBNAME = 'test_amivapi'
+MONGO_USERNAME = 'test_user'
+MONGO_PASSWORD = 'test_pw'
+
+ROOT_PASSWORD = 'root'
+
+API_MAIL = 'api@test.ch'
+SMTP_SERVER = ''
+TESTING = True
+DEBUG = True   # This makes eve's error messages more helpful
+LDAP_USERNAME = None  # LDAP test require special treatment
+LDAP_PASSWORD = None  # LDAP test require special treatment
+SENTRY_DSN = None
+SENTRY_ENVIRONMENT = None
+PASSWORD_CONTEXT = CryptContext(
+    schemes=["pbkdf2_sha256"],
+    pbkdf2_sha256__default_rounds=10,
+    # min_rounds is used to determine if a hash needs to be upgraded
+    pbkdf2_sha256__min_rounds=8,
+)
+
+LOG_LEVEL = logging.DEBUG
diff --git a/amivapi/tests/utils.py b/amivapi/tests/utils.py
index 3e41fbac7da573fe7b734ec154ce9031311cc455..298bde1bd1c1b7ec50837963c6be87f79eaaefb2 100644
--- a/amivapi/tests/utils.py
+++ b/amivapi/tests/utils.py
@@ -16,7 +16,6 @@ from bson import ObjectId
 from flask import g
 from flask.testing import FlaskClient
 from flask.wrappers import Response
-from passlib.context import CryptContext
 from pymongo import MongoClient
 
 from amivapi import bootstrap
@@ -98,27 +97,6 @@ class WebTest(unittest.TestCase, FixtureMixin):
     Inspired by eve standard testing class.
     """
 
-    # Test Config overwrites
-    test_config = {
-        'MONGO_DBNAME': 'test_amivapi',
-        'MONGO_USERNAME': 'test_user',
-        'MONGO_PASSWORD': 'test_pw',
-        'API_MAIL': 'api@test.ch',
-        'SMTP_SERVER': '',
-        'TESTING': True,
-        'DEBUG': True,   # This makes eve's error messages more helpful
-        'LDAP_USERNAME': None,  # LDAP test require special treatment
-        'LDAP_PASSWORD': None,  # LDAP test require special treatment
-        'SENTRY_DSN': None,
-        'SENTRY_ENVIRONMENT': None,
-        'PASSWORD_CONTEXT': CryptContext(
-            schemes=["pbkdf2_sha256"],
-            pbkdf2_sha256__default_rounds=10,
-            # min_rounds is used to determine if a hash needs to be upgraded
-            pbkdf2_sha256__min_rounds=8,
-        )
-    }
-
     def setUp(self, **extra_config):
         """Set up the testing client and database connection.
 
@@ -132,11 +110,14 @@ class WebTest(unittest.TestCase, FixtureMixin):
         if sys.version_info >= (3, 2):
             self.assertItemsEqual = self.assertCountEqual
 
+        # Initialize test user in mongodb
+        self.init_test_user()
+
         # create eve app and test client
         config = {}
-        config.update(self.test_config)
         config.update(extra_config)
-        self.app = bootstrap.create_app(**config)
+        self.app = bootstrap.create_app(
+            config_file='amivapi/tests/config.py', **config)
         self.app.response_class = TestResponse
         self.app.test_client_class = TestClient
         self.app.test_mails = []
@@ -151,10 +132,29 @@ class WebTest(unittest.TestCase, FixtureMixin):
             authSource=self.app.config['MONGO_DBNAME'])
         self.db = self.connection[self.app.config['MONGO_DBNAME']]
 
+    def init_test_user(self):
+        """Initialize test user this is done statically here.
+        Skips user creation if he already exists.
+        """
+        # Ensure the test user is created in the db
+        with MongoClient('mongodb', 27017) as client:
+
+            db = client['test_amivapi']
+
+            # Check if the user already exists by querying usersInfo
+            user_info = db.command('usersInfo', 'test_user')
+
+            # If no users are returned, create the user
+            if not user_info.get('users'):
+                db.command('createUser',
+                           'test_user',
+                           pwd='test_pw',
+                           roles=['readWrite'])
+
     def tearDown(self):
         """Tear down after testing."""
         # delete testing database
-        self.connection.drop_database(self.test_config['MONGO_DBNAME'])
+        self.connection.drop_database(self.app.config['MONGO_DBNAME'])
         # close database connection
         self.connection.close()
 
diff --git a/dev_mongoinit.js b/dev_mongoinit.js
index 46da2db6f8f36151d062779478e745da0317e60b..a437c58cf55dc7f82d3072d0e5be6f0a470c6e75 100644
--- a/dev_mongoinit.js
+++ b/dev_mongoinit.js
@@ -9,65 +9,4 @@ db.createUser(
             }
         ]
     }
-);
-
-db.getSiblingDB('test_amivapi').createUser(
-    {
-        user: "test_user",
-        pwd: "test_pw",
-        roles: [
-            {
-                role: "readWrite",
-                db: "test_amivapi"
-            }
-        ]
-    }
-);
-
-// Use the following code to create default user, group and oauthclient for local development
-// db = db.getSiblingDB('amivapi');
-
-// // Create admin user with password admin
-// let userId = db.users.insertOne({
-//     nethz: 'admin',
-//     password: '$pbkdf2-sha256$5$OqfUmtNaq5UyRohxDuGckw$9H/UL5N5dA7JmUq7ohRPfmJ84OUnpRKjTgsMeuFilXM',
-//     email: "admin@example.com",
-//     membership: "regular",
-//     gender: "female",
-//     firstname: "ad",
-//     lastname: "min",
-//     _etag: "27f987fd9dd45d491e5aea3e27730israndom",
-// }).insertedId;
-
-// // Create admin group with permissions on all resources
-// let groupId = db.groups.insertOne({
-//     name: 'admin',
-//     permissions: {
-//         apikeys: "readwrite",
-//         users: "readwrite",
-//         sessions: "readwrite",
-//         events: "readwrite",
-//         eventsignups: "readwrite",
-//         groups: "readwrite",
-//         groupmemberships: "readwrite",
-//         joboffers: "readwrite",
-//         beverages: "read",
-//         studydocuments: "readwrite",
-//         oauthclients: "readwrite",
-//     },
-//     _etag: "27f987fd9dd45d491e5aea3e27730israndom",
-// }).insertedId;
-
-// // Add admin to admin group
-// db.groupmemberships.insertOne({
-//     user: userId,
-//     group: groupId,
-//     _etag: "27f987fd9dd45d491e5aea3e27730israndom",
-// })
-
-// // Add Local Tool client for admin tool
-// db.oauthclients.insertOne({
-//     client_id: "Local Tool",
-//     redirect_uri: "http://localhost",
-//     _etag: "27f987fd9dd45d491e5aea3e27730israndom",
-// });
\ No newline at end of file
+);
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
index 57e93eedabc3335719a7db3e1745e5a4fbde9495..5528226d395b777fc440ab259a30102dbb10ef90 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,17 +4,16 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = py38, py310, py312, flake8
+envlist = py312, flake8
 
 [gh-actions]
 python =
-    3.10: py310, flake8
     3.12: py312, flake8
 
 [testenv]
 # `-rs` shows summary on skipped tests by default
-commands = py.test \
-    --cov-report term-missing --cov-report xml:coverage.xml --cov=amivapi -rs {posargs} amivapi/tests
+commands = 
+    py.test --cov-report term-missing --cov-report xml:coverage.xml --cov=amivapi -rs {posargs} amivapi/tests
 install_command = pip install {opts} {packages}
 deps =
     -r requirements.txt