diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 2ce55f03c1..d81e200dce 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -26,7 +26,8 @@ jobs:
   # Build the docker image
   build:
     runs-on: ubuntu-latest
-
+    permissions:
+      id-token: write
     env:
       GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
@@ -56,13 +57,22 @@ jobs:
       - name: Set up Docker Buildx
         if: github.event_name != 'pull_request'
         uses: docker/setup-buildx-action@v1
+      - name: Set up cosign
+        uses: sigstore/cosign-installer@48866aa521d8bf870604709cd43ec2f602d03ff2
       - name: Login to Dockerhub
         if: github.event_name != 'pull_request'
         uses: docker/login-action@v1
         with:
           username: ${{ secrets.DOCKER_USERNAME }}
           password: ${{ secrets.DOCKER_PASSWORD }}
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a
+        with:
+          images: |
+            inventree/inventree
       - name: Build and Push
+        id: build-and-push
         if: github.event_name != 'pull_request'
         uses: docker/build-push-action@v2
         with:
@@ -74,6 +84,10 @@ jobs:
           build-args: |
             commit_hash=${{ env.git_commit_hash }}
             commit_date=${{ env.git_commit_date }}
+      - name: Sign the published image
+        env:
+          COSIGN_EXPERIMENTAL: "true"
+        run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
       - name: Push to Stable Branch
         uses: ad-m/github-push-action@master
         if: env.stable_release == 'true'
diff --git a/Dockerfile b/Dockerfile
index f9d34ec1c1..bc2dc9d9d7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -127,20 +127,9 @@ FROM base as dev
 
 ENV INVENTREE_DEBUG=True
 
-ENV INVENTREE_DEV_DIR="${INVENTREE_HOME}/dev"
-
 # Location for python virtual environment
 # If the INVENTREE_PY_ENV variable is set, the entrypoint script will use it!
-ENV INVENTREE_PY_ENV="${INVENTREE_DEV_DIR}/env"
-
-# Override default path settings
-ENV INVENTREE_STATIC_ROOT="${INVENTREE_DEV_DIR}/static"
-ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DEV_DIR}/media"
-ENV INVENTREE_PLUGIN_DIR="${INVENTREE_DEV_DIR}/plugins"
-
-ENV INVENTREE_CONFIG_FILE="${INVENTREE_DEV_DIR}/config.yaml"
-ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DEV_DIR}/secret_key.txt"
-ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DEV_DIR}/plugins.txt"
+ENV INVENTREE_PY_ENV="${INVENTREE_DATA_DIR}/env"
 
 WORKDIR ${INVENTREE_HOME}
 
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 133474571e..447e07b09c 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -114,9 +114,9 @@ C) Look for default key file "secret_key.txt"
 d) Create "secret_key.txt" if it does not exist
 """
 
-if os.getenv("INVENTREE_SECRET_KEY"):
+if secret_key := os.getenv("INVENTREE_SECRET_KEY"):
     # Secret key passed in directly
-    SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip()  # pragma: no cover
+    SECRET_KEY = secret_key.strip()  # pragma: no cover
     logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY")  # pragma: no cover
 else:
     # Secret key passed in by file location
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 2a3377c408..1f9c11e7fc 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -691,7 +691,7 @@ class TestSettings(helpers.InvenTreeTestCase):
 
         valid = [
             'inventree/config.yaml',
-            'inventree/dev/config.yaml',
+            'inventree/data/config.yaml',
         ]
 
         self.assertTrue(any([opt in config.get_config_file().lower() for opt in valid]))
@@ -706,7 +706,7 @@ class TestSettings(helpers.InvenTreeTestCase):
 
         valid = [
             'inventree/plugins.txt',
-            'inventree/dev/plugins.txt',
+            'inventree/data/plugins.txt',
         ]
 
         self.assertTrue(any([opt in config.get_plugin_file().lower() for opt in valid]))
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 620b4f343b..c71defc40c 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -813,6 +813,14 @@ class Build(MPTTModel, ReferenceIndexingMixin):
         interchangeable = kwargs.get('interchangeable', False)
         substitutes = kwargs.get('substitutes', True)
 
+        def stock_sort(item, bom_item, variant_parts):
+            if item.part == bom_item.sub_part:
+                return 1
+            elif item.part in variant_parts:
+                return 2
+            else:
+                return 3
+
         # Get a list of all 'untracked' BOM items
         for bom_item in self.untracked_bom_items:
 
@@ -859,15 +867,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
 
             This ensures that allocation priority is first given to "direct" parts
             """
-            def stock_sort(item):
-                if item.part == bom_item.sub_part:
-                    return 1
-                elif item.part in variant_parts:
-                    return 2
-                else:
-                    return 3
-
-            available_stock = sorted(available_stock, key=stock_sort)
+            available_stock = sorted(available_stock, key=lambda item, b=bom_item, v=variant_parts: stock_sort(item, b, v))
 
             if len(available_stock) == 0:
                 # No stock items are available
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index 634a07c580..25b974c94e 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -495,7 +495,7 @@ class PurchaseOrderLineItemList(APIDownloadMixin, ListCreateAPI):
     search_fields = [
         'part__part__name',
         'part__part__description',
-        'part__MPN',
+        'part__manufacturer_part__MPN',
         'part__SKU',
         'reference',
     ]
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index 1c4b36d3aa..8e301acf39 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -168,23 +168,16 @@
 {% if order.status == PurchaseOrderStatus.PENDING %}
 $('#new-po-line').click(function() {
 
-    var fields = poLineItemFields({
-        order: {{ order.pk }},
+    createPurchaseOrderLineItem({{ order.pk }}, {
         {% if order.supplier %}
         supplier: {{ order.supplier.pk }},
         {% if order.supplier.currency %}
         currency: '{{ order.supplier.currency }}',
         {% endif %}
         {% endif %}
-    });
-
-    constructForm('{% url "api-po-line-list" %}', {
-        fields: fields,
-        method: 'POST',
-        title: '{% trans "Add Line Item" %}',
         onSuccess: function() {
             $('#po-line-table').bootstrapTable('refresh');
-        },
+        }
     });
 });
 
diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py
index 13cfb47d79..3f95d56694 100644
--- a/InvenTree/plugin/base/integration/test_mixins.py
+++ b/InvenTree/plugin/base/integration/test_mixins.py
@@ -252,18 +252,19 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         self.assertEqual(result.reason, 'OK')
 
         # api_call with full url
-        result = self.mixin.api_call('https://api.github.com/orgs/inventree', endpoint_is_url=True)
+        result = self.mixin.api_call('orgs/inventree')
         self.assertTrue(result)
 
         # api_call with post and data
         result = self.mixin.api_call(
-            'repos/inventree/InvenTree',
-            method='GET'
+            'https://reqres.in/api/users/',
+            data={"name": "morpheus", "job": "leader"},
+            method='POST',
+            endpoint_is_url=True,
         )
 
         self.assertTrue(result)
-        self.assertEqual(result['name'], 'InvenTree')
-        self.assertEqual(result['html_url'], 'https://github.com/inventree/InvenTree')
+        self.assertEqual(result['name'], 'morpheus')
 
         # api_call with filter
         result = self.mixin.api_call('repos/inventree/InvenTree/stargazers', url_args={'page': '2'})
diff --git a/InvenTree/templates/js/translated/company.js b/InvenTree/templates/js/translated/company.js
index 5af324d32f..681735db6a 100644
--- a/InvenTree/templates/js/translated/company.js
+++ b/InvenTree/templates/js/translated/company.js
@@ -102,10 +102,14 @@ function editManufacturerPart(part, options={}) {
 }
 
 
-function supplierPartFields() {
+function supplierPartFields(options={}) {
 
-    return {
-        part: {},
+    var fields = {
+        part: {
+            filters: {
+                purchaseable: true,
+            }
+        },
         manufacturer_part: {
             filters: {
                 part_detail: true,
@@ -128,6 +132,12 @@ function supplierPartFields() {
             icon: 'fa-box',
         }
     };
+
+    if (options.part) {
+        fields.manufacturer_part.filters.part = options.part;
+    }
+
+    return fields;
 }
 
 /*
@@ -135,10 +145,11 @@ function supplierPartFields() {
  */
 function createSupplierPart(options={}) {
 
-    var fields = supplierPartFields();
+    var fields = supplierPartFields({
+        part: options.part,
+    });
 
     if (options.part) {
-        fields.manufacturer_part.filters.part = options.part;
         fields.part.hidden = true;
         fields.part.value = options.part;
     }
diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js
index f3527bcdee..97970f67d4 100644
--- a/InvenTree/templates/js/translated/model_renderers.js
+++ b/InvenTree/templates/js/translated/model_renderers.js
@@ -255,15 +255,20 @@ function renderOwner(name, data, parameters={}, options={}) {
 // eslint-disable-next-line no-unused-vars
 function renderPurchaseOrder(name, data, parameters={}, options={}) {
 
-    var html = `<span>${data.reference}</span>`;
-
-    var thumbnail = null;
+    var html = '';
 
     if (data.supplier_detail) {
         thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
 
-        html += ' - ' + select2Thumbnail(thumbnail);
-        html += `<span>${data.supplier_detail.name}</span>`;
+        html += select2Thumbnail(thumbnail);
+    }
+
+    html += `<span>${data.reference}</span>`;
+
+    var thumbnail = null;
+
+    if (data.supplier_detail) {
+        html += ` - <span>${data.supplier_detail.name}</span>`;
     }
 
     if (data.description) {
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index f8166fc693..e6300b7326 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -16,6 +16,7 @@
     renderLink,
     salesOrderStatusDisplay,
     setupFilterList,
+    supplierPartFields,
 */
 
 /* exported
@@ -25,6 +26,8 @@
     completePurchaseOrder,
     completeShipment,
     completePendingShipments,
+    createPurchaseOrder,
+    createPurchaseOrderLineItem,
     createSalesOrder,
     createSalesOrderShipment,
     editPurchaseOrderLineItem,
@@ -539,6 +542,26 @@ function createPurchaseOrder(options={}) {
 }
 
 
+// Create a new PurchaseOrderLineItem
+function createPurchaseOrderLineItem(order, options={}) {
+
+    var fields = poLineItemFields({
+        order: order,
+        supplier: options.supplier,
+        currency: options.currency,
+    });
+
+    constructForm('{% url "api-po-line-list" %}', {
+        fields: fields,
+        method: 'POST',
+        title: '{% trans "Add Line Item" %}',
+        onSuccess: function(response) {
+            handleFormSuccess(response, options);
+        }
+    });
+}
+
+
 /* Construct a set of fields for the SalesOrderLineItem form */
 function soLineItemFields(options={}) {
 
@@ -590,13 +613,40 @@ function poLineItemFields(options={}) {
 
     var fields = {
         order: {
-            hidden: true,
+            filters: {
+                supplier_detail: true,
+            }
         },
         part: {
             filters: {
                 part_detail: true,
                 supplier_detail: true,
                 supplier: options.supplier,
+            },
+            secondary: {
+                method: 'POST',
+                title: '{% trans "Add Supplier Part" %}',
+                fields: function(data) {
+                    var fields = supplierPartFields({
+                        part: data.part,
+                    });
+
+                    fields.supplier.value = options.supplier;
+
+                    // Adjust manufacturer part query based on selected part
+                    fields.manufacturer_part.adjustFilters = function(query, opts) {
+
+                        var part = getFormFieldValue('part', {}, opts);
+
+                        if (part) {
+                            query.part = part;
+                        }
+
+                        return query;
+                    };
+
+                    return fields;
+                }
             }
         },
         quantity: {},
@@ -610,6 +660,7 @@ function poLineItemFields(options={}) {
 
     if (options.order) {
         fields.order.value = options.order;
+        fields.order.hidden = true;
     }
 
     if (options.currency) {
diff --git a/docker-compose.yml b/docker-compose.yml
index baba646883..8ab381a5bd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,7 +22,7 @@ services:
     inventree-dev-db:
         container_name: inventree-dev-db
         image: postgres:13
-        ports:
+        expose:
             - ${INVENTREE_DB_PORT:-5432}/tcp
         environment:
             - PGDATA=/var/lib/postgresql/data/dev/pgdb
diff --git a/docker/production/.env b/docker/production/.env
index 055ecff57d..171f2053e6 100644
--- a/docker/production/.env
+++ b/docker/production/.env
@@ -42,4 +42,7 @@ INVENTREE_CACHE_PORT=6379
 # Enable plugins?
 INVENTREE_PLUGINS_ENABLED=False
 
+# Image tag that should be used
+INVENTREE_TAG=stable
+
 COMPOSE_PROJECT_NAME=inventree-production
diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml
index 092e62f510..45f0c01c7b 100644
--- a/docker/production/docker-compose.yml
+++ b/docker/production/docker-compose.yml
@@ -45,7 +45,7 @@ services:
     inventree-db:
         container_name: inventree-db
         image: postgres:13
-        ports:
+        expose:
             - ${INVENTREE_DB_PORT:-5432}/tcp
         environment:
             - PGDATA=/var/lib/postgresql/data/pgdb
@@ -65,16 +65,16 @@ services:
             - inventree-db
         env_file:
             - .env
-        ports:
-            - ${INVENTREE_CACHE_PORT:-6379}:6379
-        restart: unless-stopped
+        expose:
+            - ${INVENTREE_CACHE_PORT:-6379}
+        restart: always
 
     # InvenTree web server services
     # Uses gunicorn as the web server
     inventree-server:
         container_name: inventree-server
         # If you wish to specify a particular InvenTree version, do so here
-        image: inventree/inventree:stable
+        image: inventree/inventree:${INVENTREE_TAG:-stable}
         expose:
             - 8000
         depends_on:
@@ -85,13 +85,14 @@ services:
         volumes:
             # Data volume must map to /home/inventree/data
             - inventree_data:/home/inventree/data
+            - inventree_plugins:/home/inventree/InvenTree/plugins
         restart: unless-stopped
 
     # Background worker process handles long-running or periodic tasks
     inventree-worker:
         container_name: inventree-worker
         # If you wish to specify a particular InvenTree version, do so here
-        image: inventree/inventree:stable
+        image: inventree/inventree:${INVENTREE_TAG:-stable}
         command: invoke worker
         depends_on:
             - inventree-server
@@ -100,6 +101,7 @@ services:
         volumes:
             # Data volume must map to /home/inventree/data
             - inventree_data:/home/inventree/data
+            - inventree_plugins:/home/inventree/InvenTree/plugins
         restart: unless-stopped
 
     # nginx acts as a reverse proxy
@@ -126,7 +128,6 @@ services:
         restart: unless-stopped
 
 volumes:
-    # NOTE: Change /path/to/data to a directory on your local machine
     # Persistent data, stored external to the container(s)
     inventree_data:
         driver: local
@@ -135,3 +136,10 @@ volumes:
             o: bind
             # This directory specified where InvenTree data are stored "outside" the docker containers
             device: ${INVENTREE_EXT_VOLUME:?You must specify the 'INVENTREE_EXT_VOLUME' variable in the .env file!}
+    inventree_plugins:
+        driver: local
+        driver_opts:
+          type: none
+          o: bind
+          # This directory specified where the optional local plugin directory is stored "outside" the docker containers
+          device: ${INVENTREE_EXT_PLUGINS:-./}
diff --git a/tasks.py b/tasks.py
index 5161676321..80a8180b3b 100644
--- a/tasks.py
+++ b/tasks.py
@@ -406,7 +406,13 @@ def import_fixtures(c):
 
 
 # Execution tasks
-@task(help={'address': 'Server address:port (default=127.0.0.1:8000)'})
+@task
+def wait(c):
+    """Wait until the database connection is ready."""
+    return manage(c, "wait_for_db")
+
+
+@task(pre=[wait], help={'address': 'Server address:port (default=127.0.0.1:8000)'})
 def server(c, address="127.0.0.1:8000"):
     """Launch a (deveopment) server using Django's in-built webserver.
 
@@ -415,12 +421,6 @@ def server(c, address="127.0.0.1:8000"):
     manage(c, "runserver {address}".format(address=address), pty=True)
 
 
-@task
-def wait(c):
-    """Wait until the database connection is ready."""
-    return manage(c, "wait_for_db")
-
-
 @task(pre=[wait])
 def worker(c):
     """Run the InvenTree background worker process."""