diff --git a/.ci/docker-compose-ci.yml b/.ci/docker-compose-ci.yml
index 4951f9fbbc2..db3d61e40cf 100644
--- a/.ci/docker-compose-ci.yml
+++ b/.ci/docker-compose-ci.yml
@@ -29,8 +29,11 @@ services:
     container_name: memcached
   discovery:
     # Uncomment this line to use the official course-discovery base image
-    image: edxops/discovery:latest
-
+    build:
+      context: ../.
+      target: dev
+      args:
+        PYTHON_VERSION: "${PYTHON_VERSION}"
     # Uncomment the next two lines to build from a local configuration repo
     # build: ../configuration/docker/build/discovery/
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 42e9ce63275..634f3a0a017 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,6 +18,20 @@ jobs:
 
     steps:
       - uses: actions/checkout@v3
+      - name: Format Python Version
+        id: format_python_version
+        shell: bash
+        run: |
+          # Remove 'py' and insert a dot to format the version
+          FORMATTED_VERSION=${{ matrix.python-version }}  # e.g., py38
+          FORMATTED_VERSION=${FORMATTED_VERSION/py3/3.}    # becomes 3.8
+          
+          # Set environment variables
+          echo "PYTHON_VERSION=$FORMATTED_VERSION" >> $GITHUB_ENV
+          
+          # Output formatted version for use in subsequent steps
+          echo "Using Python Version: $PYTHON_VERSION"
+
       - run: make ci_up
       - name: run tests
         env:
@@ -42,6 +56,8 @@ jobs:
     steps:
       - uses: actions/checkout@v3
       - run: make ci_up
+        env:
+          PYTHON_VERSION: 3.8
       - name: Download all artifacts
         # Downloads coverage1, coverage2, etc.
         uses: actions/download-artifact@v2
@@ -54,6 +70,8 @@ jobs:
     steps:
       - uses: actions/checkout@v3
       - run: make ci_up
+        env:
+          PYTHON_VERSION: 3.8
       - run: make ci_quality
 
   semgrep:
@@ -61,5 +79,7 @@ jobs:
     steps:
       - uses: actions/checkout@v3
       - run: make ci_up
+        env:
+          PYTHON_VERSION: 3.8
       - name: Run semgrep Django rules
         run: make ci_semgrep
diff --git a/Dockerfile b/Dockerfile
index 675a64dfef3..dd2f0c89c6a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,12 @@
 FROM ubuntu:focal as app
 
+ARG PYTHON_VERSION=3.8
+
 ENV DEBIAN_FRONTEND noninteractive
 # System requirements.
-RUN apt update && \
+RUN apt-get update && \
+  apt-get install -y software-properties-common && \
+  apt-add-repository -y ppa:deadsnakes/ppa && \
   apt-get install -qy \
   curl \
   gettext \
@@ -10,11 +14,14 @@ RUN apt update && \
   git \
   language-pack-en \
   build-essential \
-  python3.8-dev \
-  python3-virtualenv \
-  python3.8-distutils \
+  python${PYTHON_VERSION}-dev \
+  python${PYTHON_VERSION}-distutils \
   libmysqlclient-dev \
   libssl-dev \
+  # Current version of Pillow (9.5.0) doesn't provide pre-built wheel for python 3.12,
+  # So this apt package is needed for building Pillow on 3.12,
+  # and can be removed when version of Pillow is upgraded to 10.5.0+
+  libjpeg-dev \
   # mysqlclient >= 2.2.0 requires pkg-config.
   pkg-config \
   libcairo2-dev && \
@@ -38,8 +45,12 @@ ENV PATH "${DISCOVERY_VENV_DIR}/bin:${DISCOVERY_NODEENV_DIR}/bin:$PATH"
 ENV DISCOVERY_CFG "/edx/etc/discovery.yml"
 ENV DISCOVERY_CODE_DIR "${DISCOVERY_CODE_DIR}"
 ENV DISCOVERY_APP_DIR "${DISCOVERY_APP_DIR}"
+ENV PYTHON_VERSION "${PYTHON_VERSION}"
+
+RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python${PYTHON_VERSION}
+RUN pip install virtualenv
 
-RUN virtualenv -p python3.8 --always-copy ${DISCOVERY_VENV_DIR}
+RUN virtualenv -p python${PYTHON_VERSION} --always-copy ${DISCOVERY_VENV_DIR}
 
 # No need to activate discovery venv as it is already in path
 RUN pip install nodeenv
diff --git a/course_discovery/apps/api/v1/tests/test_views/test_search.py b/course_discovery/apps/api/v1/tests/test_views/test_search.py
index 5e0213a20d8..80c86fc0ea5 100644
--- a/course_discovery/apps/api/v1/tests/test_views/test_search.py
+++ b/course_discovery/apps/api/v1/tests/test_views/test_search.py
@@ -1,5 +1,6 @@
 import datetime
 import json
+import sys
 import urllib.parse
 import uuid
 
@@ -66,7 +67,12 @@ def assert_successful_search(self, path=None, serializer=None):
             'next': None,
         }
         actual = response_data['objects'] if path == self.faceted_path else response_data
-        self.assertDictContainsSubset(expected, actual)
+        if sys.version_info > (3, 9):
+            # Remove this pylint disable once discovery reaches python 3.11+
+            # pylint: disable=unsupported-binary-operation
+            self.assertEqual(actual, actual | expected)  # pragma: no cover
+        else:
+            self.assertDictContainsSubset(expected, actual)
 
         return course_run, response_data
 
@@ -90,7 +96,13 @@ def assert_response_includes_availability_facets(self, response_data):
                 'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_upcoming'})
             },
         }
-        self.assertDictContainsSubset(expected, response_data['queries'])
+
+        if sys.version_info > (3, 9):
+            # Remove this pylint disable once discovery reaches python 3.11+
+            # pylint: disable=unsupported-binary-operation
+            self.assertEqual(response_data['queries'], response_data['queries'] | expected)  # pragma: no cover
+        else:
+            self.assertDictContainsSubset(expected, response_data['queries'])
 
     @ddt.data(faceted_path, list_path, detailed_path)
     def test_authentication(self, path):
@@ -117,7 +129,13 @@ def test_faceted_search(self):
             'text': course_run.pacing_type,
             'count': 1,
         }
-        self.assertDictContainsSubset(expected, response_data['fields']['pacing_type'][0])
+        actual = response_data['fields']['pacing_type'][0]
+        if sys.version_info > (3, 9):
+            # Remove this pylint disable once discovery reaches python 3.11+
+            # pylint: disable=unsupported-binary-operation
+            self.assertEqual(actual, actual | expected)  # pragma: no cover
+        else:
+            self.assertDictContainsSubset(expected, actual)
 
     def test_invalid_query_facet(self):
         """ Verify the endpoint returns HTTP 400 if an invalid facet is requested. """
@@ -202,7 +220,12 @@ def test_exclude_unavailable_program_types(self, path, serializer, result_locati
                 self.serialize_course_run_search(course_run, serializer=serializer)
             ]
         }
-        self.assertDictContainsSubset(expected, response_data)
+        if sys.version_info > (3, 9):
+            # Remove this pylint disable once discovery reaches python 3.11+
+            # pylint: disable=unsupported-binary-operation
+            self.assertEqual(response_data, response_data | expected)  # pragma: no cover
+        else:
+            self.assertDictContainsSubset(expected, response_data)
 
         # Check that the program is indeed the active one.
         for key in result_location_keys: