diff --git a/.gitignore b/.gitignore
index 9df71ef52ead788553d90c771678432f2fc33db0..dfdc67da9e07f8516efdc4e2f5c130db382efcff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,7 @@ docs/vale/styles/*
 venv/
 .venv/
 
+.DS_Store # macOS
+Thumbs.db # Windows
+
 config.json
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e2b511ec087450ebec6817b8f894a5225bdb155d..323c1f19acd847dffd33494f6466300e576f2558 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,7 +12,7 @@ run-tox-pipeline:
   stage: tox
   tags:
     - docker-executor
-  image: python:3.10
+  image: python:3.11
 
   # Change pip's cache directory to be inside the project directory since we can
   # only cache local items.
diff --git a/Dockerfile b/Dockerfile
index cfbea5eed21afa529766915f91e42991e4d80fe6..8b5f4742ef0870afbb4316193ff198f2184f701d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,31 @@
-FROM python:3.11
+FROM python:3.11-alpine
 
 ARG ARTIFACT_VERSION
 
 WORKDIR /app
 
-RUN apt update && apt install -y gcc libc-dev libffi-dev curl vim && \
+RUN apk add --update --no-cache gcc libc-dev libffi-dev curl vim ansible bash openssh && \
     addgroup -S appgroup && adduser -S appuser -G appgroup -h /app
 
+
+# Create ansible.cfg file and set custom paths for collections and roles
+RUN mkdir -p /app/gap/collections /app/gap/roles /etc/ansible && \
+    printf "[defaults]\ncollections_paths = /app/gap/collections\nroles_path = /app/gap/roles" > /etc/ansible/ansible.cfg
+
 RUN pip install \
     --pre \
     --extra-index-url https://artifactory.software.geant.org/artifactory/api/pypi/geant-swd-pypi/simple \
     --target /app \
-    goat-lso==${ARTIFACT_VERSION}
+    goat-lso==${ARTIFACT_VERSION} && \
+    ansible-galaxy collection install  \
+                   community.general  \
+                   juniper.device \
+                   junipernetworks.junos \
+                   geant.gap_ansible -p /app/gap/collections && \
+    ansible-galaxy role install Juniper.junos -p /app/gap/roles
 
 RUN chown -R appuser:appgroup /app
 USER appuser
 EXPOSE 8000
+ENTRYPOINT []
 CMD ["python", "-m",  "uvicorn", "lso.app:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/config.json.example b/config.json.example
index ef9197d0788b9a9576ae86f42866fffa99a7f712..64ddb818152b0fcb6bff2565084c66933b6621a4 100644
--- a/config.json.example
+++ b/config.json.example
@@ -1,3 +1,3 @@
 {
-  "ansible_playbooks_root_dir": "/"
+  "ansible_playbooks_root_dir": "/app/gap/collections/ansible_collections/geant/gap_ansible/playbooks"
 }
diff --git a/docker/Dockerfile b/docker/Dockerfile
deleted file mode 100644
index 9fc9409ce0d4f790740f5c356c405191ab7e24fc..0000000000000000000000000000000000000000
--- a/docker/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM python:alpine3.17
-
-LABEL version="1.0" 
-LABEL maintainer="Geant LSO Team <@geant.org>"
-
-RUN apk add --no-cache bash curl vim gcc libc-dev libffi-dev
-# RUN pip install --pre --extra-index-url https://artifactory.software.geant.org/artifactory/api/pypi/geant-swd-pypi/simple goat-lso
-
-WORKDIR /opt/lso
-COPY . .
-RUN pip install -e .
-RUN pip install httpx sphinx sphinx_rtd_theme vale ansible
-
-# Generate documentation
-RUN ./build-docs.sh
-
-# Generate sample configuration file, and remove an existing one if present
-RUN rm -f config.json >/dev/null 2>&1
-RUN ln -s config.json.example config.json
-
-# ENTRYPOINT ["sleep", "inf"]
-# ENTRYPOINT ["SETTINGS_FILENAME=./config.json", "python", "-m", "lso.app"]
-ENTRYPOINT ["./docker/app-run.sh"]
diff --git a/docker/app-run.sh b/docker/app-run.sh
deleted file mode 100755
index 6b3cd586e0455e9cb8f96d726dd3b6835a957e2b..0000000000000000000000000000000000000000
--- a/docker/app-run.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-
-SETTINGS_FILENAME=./config.json python -m lso.app
diff --git a/docker/docker-start.sh b/docker/docker-start.sh
deleted file mode 100755
index 7aba417c617cbb42b57083c693a7a55163266964..0000000000000000000000000000000000000000
--- a/docker/docker-start.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env bash
-
-goat_name="goat-lso"
-goat_image="goat/lso"
-goat_tag="1.0"
-
-if [[ $(docker image list | grep "${goat_image}" | grep -c "${goat_tag}") -eq 0 ]]; then
-  docker build -f docker/Dockerfile -t ${goat_image}:${goat_tag} .
-fi
-if [[ $(docker ps -a | grep -c "${goat_image}:${goat_tag}") -eq 0 ]]; then
-  docker run -d -p 44444:44444 --name ${goat_name} ${goat_image}:${goat_tag} >/dev/null 2>&1
-fi
-if [[ "$( docker container inspect -f '{{.State.Status}}' ${goat_name} )" != "running" ]]; then
-  docker start ${goat_name} >/dev/null 2>&1
-fi
-
-sleep 1
-
-# Check endpoints
-curl -f http://localhost:44444/docs >/dev/null 2>&1
-if [[ $? -eq 0 ]]; then
-  echo "LSO is running. OpenAPI available at http://localhost:44444/docs"
-else
-  echo "LSO is not running"
-fi
diff --git a/docker/docker-stop.sh b/docker/docker-stop.sh
deleted file mode 100755
index 1cde4b07e467ade9ea2649ec041c08f0041deb37..0000000000000000000000000000000000000000
--- a/docker/docker-stop.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-docker stop goat-lso >/dev/null 2>&1
-docker rm goat-lso >/dev/null 2>&1
diff --git a/lso/config.py b/lso/config.py
index 9819b9548f925639e2b24c8a027151a0ed72dc8c..edf5546fe2020a81c43d37bbffd68ce2580d71b1 100644
--- a/lso/config.py
+++ b/lso/config.py
@@ -10,7 +10,7 @@ import os
 from typing import TextIO
 
 import jsonschema
-from pydantic import BaseModel, DirectoryPath
+from pydantic import BaseModel
 
 CONFIG_SCHEMA = {
     "$schema": "http://json-schema.org/draft-07/schema#",
@@ -25,7 +25,7 @@ DEFAULT_REQUEST_TIMEOUT = 10
 class Config(BaseModel):
     """Simple Config class that only contains the path to the used Ansible playbooks."""
 
-    ansible_playbooks_root_dir: DirectoryPath
+    ansible_playbooks_root_dir: str
 
 
 def load_from_file(file: TextIO) -> Config:
diff --git a/lso/playbook.py b/lso/playbook.py
index 19ff44e72402499ed111841aa921421e08d006bd..9650985340496fd7e166e8e9dea740f8b2ba6bbb 100644
--- a/lso/playbook.py
+++ b/lso/playbook.py
@@ -18,8 +18,6 @@ from lso.config import DEFAULT_REQUEST_TIMEOUT
 
 logger = logging.getLogger(__name__)
 
-config_params = config.load()
-
 
 # enum.StrEnum is only available in python 3.11
 class PlaybookJobStatus(str, enum.Enum):
@@ -50,6 +48,7 @@ class PlaybookLaunchResponse(BaseModel):
 
 
 def get_playbook_path(playbook_name: str) -> str:
+    config_params = config.load()
     return os.path.join(config_params.ansible_playbooks_root_dir, playbook_name)
 
 
diff --git a/test/conftest.py b/test/conftest.py
index 78112127ae71436ed6b44fae8a03c290af92c1bf..2e3586910cb413d536185eb6e0dde86a70186157 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,7 +1,8 @@
 import json
 import os
 import tempfile
-from typing import Any, Generator
+from io import StringIO
+from typing import Any, Callable, Generator
 
 import pytest
 from faker import Faker
@@ -10,28 +11,42 @@ from fastapi.testclient import TestClient
 import lso
 
 
+@pytest.fixture
+def mocked_ansible_runner_run() -> Callable:
+    class Runner:
+        def __init__(self) -> None:
+            self.status = "success"
+            self.rc = 0
+            self.stdout = StringIO("[{'step one': 'results'}, {'step two': 2}]")
+
+    def run(*args: Any, **kwargs: Any) -> Runner:
+        return Runner()
+
+    return run
+
+
 @pytest.fixture(scope="session")
-def config_data() -> dict[str, str]:
+def configuration_data() -> dict[str, str]:
     """Start the server with valid configuration data."""
-    return {"ansible_playbooks_root_dir": "/"}
+    return {"ansible_playbooks_root_dir": "/app/gap/collections/ansible_collections/geant/gap_ansible/playbooks"}
 
 
 @pytest.fixture(scope="session")
-def config_file(config_data: dict[str, str]) -> Generator[str, Any, None]:
+def data_config_filename(configuration_data: dict[str, str]) -> Generator[str, Any, None]:
     """Fixture that will yield a filename that contains a valid configuration.
 
     :return: Path to valid configuration file
     """
     with tempfile.NamedTemporaryFile(mode="w") as file:
-        file.write(json.dumps(config_data))
+        file.write(json.dumps(configuration_data))
         file.flush()
         yield file.name
 
 
 @pytest.fixture(scope="session")
-def client(config_file: str) -> Generator[TestClient, Any, None]:
+def client(data_config_filename: str) -> Generator[TestClient, Any, None]:
     """Return a client that can be used to test the server."""
-    os.environ["SETTINGS_FILENAME"] = config_file
+    os.environ["SETTINGS_FILENAME"] = data_config_filename
     app = lso.create_app()
     yield TestClient(app)  # wait here until calling context ends
 
diff --git a/test/routes/__init__.py b/test/routes/__init__.py
index b1490578a4f0cb8871454268461cb9c9b582878b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/test/routes/__init__.py
+++ b/test/routes/__init__.py
@@ -1,15 +0,0 @@
-from io import StringIO
-from typing import Any
-
-TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume"
-
-
-class Runner:
-    def __init__(self) -> None:
-        self.status = "success"
-        self.rc = 0
-        self.stdout = StringIO("[{'step one': 'results'}, {'step two': 2}]")
-
-
-def test_ansible_runner_run(**kwargs: Any) -> Runner:
-    return Runner()
diff --git a/test/routes/test_ip_trunk.py b/test/routes/test_ip_trunk.py
index 99a8d9afd41b90a7479c2e7fd9521902fe25d7c4..21f74cae880a5aea2b40aedd381a4b4281094a83 100644
--- a/test/routes/test_ip_trunk.py
+++ b/test/routes/test_ip_trunk.py
@@ -1,4 +1,5 @@
 import time
+from typing import Callable
 from unittest.mock import patch
 
 import jsonschema
@@ -8,7 +9,8 @@ from faker import Faker
 from starlette.testclient import TestClient
 
 from lso.playbook import PlaybookLaunchResponse
-from test.routes import TEST_CALLBACK_URL, test_ansible_runner_run
+
+TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume"
 
 
 @pytest.fixture(scope="session")
@@ -157,7 +159,9 @@ def migration_object(faker: Faker) -> dict:
 
 
 @responses.activate
-def test_ip_trunk_provisioning(client: TestClient, subscription_object: dict) -> None:
+def test_ip_trunk_provisioning(
+    client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable
+) -> None:
     responses.post(url=TEST_CALLBACK_URL, status=200)
 
     params = {
@@ -170,7 +174,7 @@ def test_ip_trunk_provisioning(client: TestClient, subscription_object: dict) ->
         "subscription": subscription_object,
     }
 
-    with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+    with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
         rv = client.post("/api/ip_trunk/", json=params)
         assert rv.status_code == 200
         response = rv.json()
@@ -184,7 +188,9 @@ def test_ip_trunk_provisioning(client: TestClient, subscription_object: dict) ->
 
 
 @responses.activate
-def test_ip_trunk_modification(client: TestClient, subscription_object: dict) -> None:
+def test_ip_trunk_modification(
+    client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable
+) -> None:
     responses.post(url=TEST_CALLBACK_URL, status=200)
 
     params = {
@@ -197,7 +203,7 @@ def test_ip_trunk_modification(client: TestClient, subscription_object: dict) ->
         "old_subscription": subscription_object,
     }
 
-    with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+    with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
         rv = client.put("/api/ip_trunk/", json=params)
         assert rv.status_code == 200
         response = rv.json()
@@ -211,7 +217,7 @@ def test_ip_trunk_modification(client: TestClient, subscription_object: dict) ->
 
 
 @responses.activate
-def test_ip_trunk_deletion(client: TestClient, subscription_object: dict) -> None:
+def test_ip_trunk_deletion(client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable) -> None:
     responses.post(url=TEST_CALLBACK_URL, status=204)
 
     params = {
@@ -223,7 +229,7 @@ def test_ip_trunk_deletion(client: TestClient, subscription_object: dict) -> Non
         "subscription": subscription_object,
     }
 
-    with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+    with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
         rv = client.request(url="/api/ip_trunk/", method=responses.DELETE, json=params)
         assert rv.status_code == 200
         response = rv.json()
@@ -237,7 +243,9 @@ def test_ip_trunk_deletion(client: TestClient, subscription_object: dict) -> Non
 
 
 @responses.activate
-def test_ip_trunk_migration(client: TestClient, subscription_object: dict, migration_object: dict) -> None:
+def test_ip_trunk_migration(
+    client: TestClient, subscription_object: dict, migration_object: dict, mocked_ansible_runner_run: Callable
+) -> None:
     responses.post(url=TEST_CALLBACK_URL, status=204)
 
     params = {
@@ -251,7 +259,7 @@ def test_ip_trunk_migration(client: TestClient, subscription_object: dict, migra
         "new_side": migration_object,
     }
 
-    with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+    with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
         rv = client.post(url="/api/ip_trunk/migrate", json=params)
         assert rv.status_code == 200
         response = rv.json()
diff --git a/test/routes/test_router.py b/test/routes/test_router.py
index 1b8a7bde2b279749c24a20d5810f506b03a74056..6a9f2fffa21e847a1e3294f1658ce312a6fe3a10 100644
--- a/test/routes/test_router.py
+++ b/test/routes/test_router.py
@@ -1,4 +1,5 @@
 import time
+from typing import Callable
 from unittest.mock import patch
 
 import jsonschema
@@ -7,11 +8,12 @@ from faker import Faker
 from starlette.testclient import TestClient
 
 from lso.playbook import PlaybookLaunchResponse
-from test.routes import TEST_CALLBACK_URL, test_ansible_runner_run
+
+TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume"
 
 
 @responses.activate
-def test_router_provisioning(client: TestClient, faker: Faker) -> None:
+def test_router_provisioning(client: TestClient, faker: Faker, mocked_ansible_runner_run: Callable) -> None:
     responses.put(url=TEST_CALLBACK_URL, status=200)
 
     params = {
@@ -40,7 +42,7 @@ def test_router_provisioning(client: TestClient, faker: Faker) -> None:
         },
     }
 
-    with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+    with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
         rv = client.post("/api/router/", json=params)
         assert rv.status_code == 200
         response = rv.json()
diff --git a/test/test_config.py b/test/test_config.py
index 83a47d172bc486495a85900a56a25c73941020e7..bb724c7f74638f079491d895675674676454b6e2 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -9,12 +9,12 @@ import pytest
 from lso import config
 
 
-def test_validate_testenv_config(config_file: str) -> None:
+def test_validate_testenv_config(data_config_filename: str) -> None:
     """Load a configuration from a file.
 
-    :param config_file: Configuration file pytest fixture
+    :param data_config_filename: Configuration file pytest fixture
     """
-    os.environ["SETTINGS_FILENAME"] = config_file
+    os.environ["SETTINGS_FILENAME"] = data_config_filename
     params = config.load()
     assert params
 
diff --git a/tox.ini b/tox.ini
index e8715b0ffb439b3ec55e7e9eb56f087b46e21aad..927b8974a48b33a3031b4b28dce6c342837cf1eb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py310
+envlist = py311
 
 [flake8]
 ignore = W503
@@ -15,19 +15,19 @@ per-file-ignores =
 [testenv]
 passenv = XDG_CACHE_HOME,USE_COMPOSE
 setenv =
-    SETTINGS_FILENAME = config.json.example
+    SETTINGS_FILENAME = dummy.json
 deps =
     coverage
     -r requirements.txt
 
 commands =
-    coverage erase
-    coverage run --source lso -m pytest
-    coverage xml
-    coverage html
-    coverage report --fail-under 80
     isort -c .
     ruff .
     black --check .
     mypy .
     flake8
+    coverage erase
+    coverage run --source lso -m pytest
+    coverage xml
+    coverage html
+    coverage report --fail-under 80