diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md
index 3596f4f000..5320939aa9 100644
--- a/docs/docs/start/config.md
+++ b/docs/docs/start/config.md
@@ -402,7 +402,5 @@ The following [plugin](../extend/plugins.md) configuration options are available
 | --- | --- | --- | --- |
 | INVENTREE_PLUGINS_ENABLED | plugins_enabled | Enable plugin support | False |
 | INVENTREE_PLUGIN_NOINSTALL | plugin_noinstall | Disable Plugin installation via API - only use plugins.txt file | False |
+| INVENTREE_PLUGIN_FILE | plugins_plugin_file | Location of plugin installation file | *Not specified* |
 | INVENTREE_PLUGIN_DIR | plugins_plugin_dir | Location of external plugin directory | *Not specified* |
-
-!!! info "Plugins Directory"
-    If the `INVENTREE_PLUGIN_DIR` environment variable is not specified, the default location is the `plugins` directory within the InvenTree installation.
diff --git a/src/backend/InvenTree/InvenTree/config.py b/src/backend/InvenTree/InvenTree/config.py
index 5f1b8ee4f6..5c62fc66c6 100644
--- a/src/backend/InvenTree/InvenTree/config.py
+++ b/src/backend/InvenTree/InvenTree/config.py
@@ -283,6 +283,37 @@ def get_backup_dir(create=True):
     return bd
 
 
+def get_plugin_file():
+    """Returns the path of the InvenTree plugins specification file.
+
+    Note: It will be created if it does not already exist!
+    """
+    # Check if the plugin.txt file (specifying required plugins) is specified
+    plugin_file = get_setting('INVENTREE_PLUGIN_FILE', 'plugin_file')
+
+    if not plugin_file:
+        # If not specified, look in the same directory as the configuration file
+        config_dir = get_config_file().parent
+        plugin_file = config_dir.joinpath('plugins.txt')
+    else:
+        # Make sure we are using a modern Path object
+        plugin_file = Path(plugin_file)
+
+    if not plugin_file.exists():
+        logger.warning(
+            'Plugin configuration file does not exist - creating default file'
+        )
+        logger.info("Creating plugin file at '%s'", plugin_file)
+        ensure_dir(plugin_file.parent)
+
+        # If opening the file fails (no write permission, for example), then this will throw an error
+        plugin_file.write_text(
+            '# InvenTree Plugins (uses PIP framework to install)\n\n'
+        )
+
+    return plugin_file
+
+
 def get_plugin_dir():
     """Returns the path of the custom plugins directory."""
     return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py
index 01d5b54805..7e294b8ac8 100644
--- a/src/backend/InvenTree/InvenTree/settings.py
+++ b/src/backend/InvenTree/InvenTree/settings.py
@@ -143,6 +143,8 @@ PLUGINS_INSTALL_DISABLED = get_boolean_setting(
     'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
 )
 
+PLUGIN_FILE = config.get_plugin_file()
+
 # Plugin test settings
 PLUGIN_TESTING = get_setting(
     'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
@@ -158,6 +160,8 @@ PLUGIN_RETRY = get_setting(
     'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
 )  # How often should plugin loading be tried?
 
+PLUGIN_FILE_CHECKED = False  # Was the plugin file checked?
+
 STATICFILES_DIRS = []
 
 # Translated Template settings
diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py
index e0a1573b28..bf9ad84477 100644
--- a/src/backend/InvenTree/InvenTree/tests.py
+++ b/src/backend/InvenTree/InvenTree/tests.py
@@ -1180,6 +1180,23 @@ class TestSettings(InvenTreeTestCase):
         # make sure to clean up
         settings.TESTING_ENV = False
 
+    def test_initial_install(self):
+        """Test if install of plugins on startup works."""
+        from plugin import registry
+
+        if not settings.DOCKER:
+            # Check an install run
+            response = registry.install_plugin_file()
+            self.assertEqual(response, 'first_run')
+
+            # Set dynamic setting to True and rerun to launch install
+            InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
+            registry.reload_plugins(full_reload=True)
+
+        # Check that there was another run
+        response = registry.install_plugin_file()
+        self.assertEqual(response, True)
+
     def test_helpers_cfg_file(self):
         """Test get_config_file."""
         # normal run - not configured
@@ -1199,6 +1216,24 @@ class TestSettings(InvenTreeTestCase):
                 str(config.get_config_file()).lower(),
             )
 
+    def test_helpers_plugin_file(self):
+        """Test get_plugin_file."""
+        # normal run - not configured
+
+        valid = ['inventree/plugins.txt', 'inventree/data/plugins.txt']
+
+        self.assertTrue(
+            any(opt in str(config.get_plugin_file()).lower() for opt in valid)
+        )
+
+        # with env set
+        with self.in_env_context({
+            'INVENTREE_PLUGIN_FILE': '_testfolder/my_special_plugins.txt'
+        }):
+            self.assertIn(
+                '_testfolder/my_special_plugins.txt', str(config.get_plugin_file())
+            )
+
     def test_helpers_setting(self):
         """Test get_setting."""
         TEST_ENV_NAME = '123TEST'
diff --git a/src/backend/InvenTree/config_template.yaml b/src/backend/InvenTree/config_template.yaml
index 9dd5c3e2ba..934fa05d69 100644
--- a/src/backend/InvenTree/config_template.yaml
+++ b/src/backend/InvenTree/config_template.yaml
@@ -91,6 +91,7 @@ tracing:
 # Set this variable to True to enable InvenTree Plugins, or use the environment variable INVENTREE_PLUGINS_ENABLED
 plugins_enabled: False
 #plugin_noinstall: True
+#plugin_file: '/path/to/plugins.txt'
 #plugin_dir: '/path/to/plugins/'
 
 # Set this variable to True to enable auto-migrations, or use the environment variable INVENTREE_AUTO_UPDATE
diff --git a/src/backend/InvenTree/plugin/apps.py b/src/backend/InvenTree/plugin/apps.py
index f0dd345561..fa7801d72e 100644
--- a/src/backend/InvenTree/plugin/apps.py
+++ b/src/backend/InvenTree/plugin/apps.py
@@ -40,6 +40,18 @@ class PluginAppConfig(AppConfig):
             logger.info('Loading InvenTree plugins')
 
             if not registry.is_loading:
+                # this is the first startup
+                try:
+                    from common.models import InvenTreeSetting
+
+                    if InvenTreeSetting.get_setting(
+                        'PLUGIN_ON_STARTUP', create=False, cache=False
+                    ):
+                        # make sure all plugins are installed
+                        registry.install_plugin_file()
+                except Exception:  # pragma: no cover
+                    pass
+
                 # Perform a full reload of the plugin registry
                 registry.reload_plugins(
                     full_reload=True, force_reload=True, collect=True
diff --git a/src/backend/InvenTree/plugin/helpers.py b/src/backend/InvenTree/plugin/helpers.py
index 161ef678cc..9eee31bc39 100644
--- a/src/backend/InvenTree/plugin/helpers.py
+++ b/src/backend/InvenTree/plugin/helpers.py
@@ -177,37 +177,27 @@ def get_modules(pkg, path=None):
     elif type(path) is not list:
         path = [path]
 
-    try:
-        packages = pkgutil.walk_packages(path)
-    except Exception as e:
-        raise IntegrationPluginError(pkg.__name__, str(e))
+    for finder, name, _ in pkgutil.walk_packages(path):
+        try:
+            if sys.version_info < (3, 12):
+                module = finder.find_module(name).load_module(name)
+            else:
+                spec = finder.find_spec(name)
+                module = module_from_spec(spec)
+                sys.modules[name] = module
+                spec.loader.exec_module(module)
+            pkg_names = getattr(module, '__all__', None)
+            for k, v in vars(module).items():
+                if not k.startswith('_') and (pkg_names is None or k in pkg_names):
+                    context[k] = v
+            context[name] = module
+        except AppRegistryNotReady:  # pragma: no cover
+            pass
+        except Exception as error:
+            # this 'protects' against malformed plugin modules by more or less silently failing
 
-    try:
-        for finder, name, _ in packages:
-            try:
-                if sys.version_info < (3, 12):
-                    try:
-                        module = finder.find_module(name).load_module(name)
-                    except Exception as e:
-                        raise IntegrationPluginError(name, str(e))
-
-                else:
-                    spec = finder.find_spec(name)
-                    module = module_from_spec(spec)
-                    sys.modules[name] = module
-                    spec.loader.exec_module(module)
-                pkg_names = getattr(module, '__all__', None)
-                for k, v in vars(module).items():
-                    if not k.startswith('_') and (pkg_names is None or k in pkg_names):
-                        context[k] = v
-                context[name] = module
-            except AppRegistryNotReady:  # pragma: no cover
-                pass
-    except Exception as error:
-        # this 'protects' against malformed plugin modules by more or less silently failing
-
-        # log to stack
-        log_error({name: str(error)}, 'discovery')
+            # log to stack
+            log_error({name: str(error)}, 'discovery')
 
     return [v for k, v in context.items()]
 
@@ -216,8 +206,8 @@ def get_classes(module) -> list:
     """Get all classes in a given module."""
     try:
         return inspect.getmembers(module, inspect.isclass)
-    except Exception as e:
-        raise IntegrationPluginError(module.__name__, str(e))
+    except Exception:
+        return []
 
 
 def get_plugins(pkg, baseclass, path=None):
@@ -233,12 +223,7 @@ def get_plugins(pkg, baseclass, path=None):
     # Iterate through each module in the package
     for mod in modules:
         # Iterate through each class in the module
-        try:
-            classes = get_classes(mod)
-        except IntegrationPluginError:
-            continue
-
-        for item in classes:
+        for item in get_classes(mod):
             plugin = item[1]
             if issubclass(plugin, baseclass) and plugin.NAME:
                 plugins.append(plugin)
diff --git a/src/backend/InvenTree/plugin/installer.py b/src/backend/InvenTree/plugin/installer.py
index 4551f29560..daf62cee96 100644
--- a/src/backend/InvenTree/plugin/installer.py
+++ b/src/backend/InvenTree/plugin/installer.py
@@ -155,6 +155,98 @@ def plugins_dir():
     return pd.absolute()
 
 
+def install_plugins_file():
+    """Install plugins from the plugins file."""
+    logger.info('Installing plugins from plugins file')
+
+    pf = settings.PLUGIN_FILE
+
+    if not pf or not pf.exists():
+        logger.warning('Plugin file %s does not exist', str(pf))
+        return
+
+    plugin_dir = plugins_dir()
+
+    cmd = ['install', '-U', '--target', str(plugin_dir), '-r', str(pf)]
+
+    try:
+        pip_command(*cmd)
+    except subprocess.CalledProcessError as error:
+        output = error.output.decode('utf-8')
+        logger.exception('Plugin file installation failed: %s', str(output))
+        log_error('pip')
+        return False
+    except Exception as exc:
+        logger.exception('Plugin file installation failed: %s', exc)
+        log_error('pip')
+        return False
+
+    # Update static files
+    plugin.staticfiles.collect_plugins_static_files()
+    plugin.staticfiles.clear_plugins_static_files()
+
+    # At this point, the plugins file has been installed
+    return True
+
+
+def update_plugins_file(install_name, remove=False):
+    """Add a plugin to the plugins file."""
+    logger.info('Adding plugin to plugins file: %s', install_name)
+
+    pf = settings.PLUGIN_FILE
+
+    if not pf or not pf.exists():
+        logger.warning('Plugin file %s does not exist', str(pf))
+        return
+
+    def compare_line(line: str):
+        """Check if a line in the file matches the installname."""
+        return line.strip().split('==')[0] == install_name.split('==')[0]
+
+    # First, read in existing plugin file
+    try:
+        with pf.open(mode='r') as f:
+            lines = f.readlines()
+    except Exception as exc:
+        logger.exception('Failed to read plugins file: %s', str(exc))
+        return
+
+    # Reconstruct output file
+    output = []
+
+    found = False
+
+    # Check if plugin is already in file
+    for line in lines:
+        # Ignore processing for any commented lines
+        if line.strip().startswith('#'):
+            output.append(line)
+            continue
+
+        if compare_line(line):
+            found = True
+            if not remove:
+                # Replace line with new install name
+                output.append(install_name)
+        else:
+            output.append(line)
+
+    # Append plugin to file
+    if not found and not remove:
+        output.append(install_name)
+
+    # Write file back to disk
+    try:
+        with pf.open(mode='w') as f:
+            for line in output:
+                f.write(line)
+
+                if not line.endswith('\n'):
+                    f.write('\n')
+    except Exception as exc:
+        logger.exception('Failed to add plugin to plugins file: %s', str(exc))
+
+
 def install_plugin(url=None, packagename=None, user=None, version=None):
     """Install a plugin into the python virtual environment.
 
@@ -217,6 +309,9 @@ def install_plugin(url=None, packagename=None, user=None, version=None):
     except subprocess.CalledProcessError as error:
         handle_pip_error(error, 'plugin_install')
 
+    # Save plugin to plugins file
+    update_plugins_file(full_pkg)
+
     # Reload the plugin registry, to discover the new plugin
     from plugin.registry import registry
 
@@ -281,6 +376,9 @@ def uninstall_plugin(cfg: plugin.models.PluginConfig, user=None, delete_config=T
         # No matching install target found
         raise ValidationError(_('Plugin installation not found'))
 
+    # Update the plugins file
+    update_plugins_file(package_name, remove=True)
+
     if delete_config:
         # Remove the plugin configuration from the database
         cfg.delete()
diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py
index dac8ac05f4..4c29e06fea 100644
--- a/src/backend/InvenTree/plugin/registry.py
+++ b/src/backend/InvenTree/plugin/registry.py
@@ -428,6 +428,19 @@ class PluginsRegistry:
 
         self.mixin_modules = collected_mixins
 
+    def install_plugin_file(self):
+        """Make sure all plugins are installed in the current environment."""
+        if settings.PLUGIN_FILE_CHECKED:
+            logger.info('Plugin file was already checked')
+            return True
+
+        from plugin.installer import install_plugins_file
+
+        if install_plugins_file():
+            settings.PLUGIN_FILE_CHECKED = True
+            return 'first_run'
+        return False
+
     # endregion
 
     # region general internal loading / activating / deactivating / unloading
diff --git a/src/backend/InvenTree/templates/InvenTree/settings/plugin.html b/src/backend/InvenTree/templates/InvenTree/settings/plugin.html
index 030c1e5224..f97c98795e 100644
--- a/src/backend/InvenTree/templates/InvenTree/settings/plugin.html
+++ b/src/backend/InvenTree/templates/InvenTree/settings/plugin.html
@@ -25,6 +25,7 @@
         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_URL" icon="fa-link" %}
         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" icon="fa-sitemap" %}
         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %}
+        {% include "InvenTree/settings/setting.html" with key="PLUGIN_ON_STARTUP" %}
     </tbody>
 </table>
 </div>