diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 68c09c922a67533eb826777cc85decdacfce2b7a..d5e77c7e271e315c0d7cea4ce4b6994c5d2a32dc 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -3,7 +3,8 @@ module JavascriptHelper
     javascript_include_tag asset_path(js)
   end
 
-  def page_specific_javascript_bundle_tag(js)
-    javascript_include_tag(*webpack_asset_paths(js))
+  # deprecated; use webpack_bundle_tag directly instead
+  def page_specific_javascript_bundle_tag(bundle)
+    webpack_bundle_tag(bundle)
   end
 end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6bacda9fe75153913e25ea770a344247629eb3b1
--- /dev/null
+++ b/app/helpers/webpack_helper.rb
@@ -0,0 +1,30 @@
+require 'webpack/rails/manifest'
+
+module WebpackHelper
+  def webpack_bundle_tag(bundle)
+    javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
+  end
+
+  # override webpack-rails gem helper until changes can make it upstream
+  def gitlab_webpack_asset_paths(source, extension: nil)
+    return "" unless source.present?
+
+    paths = Webpack::Rails::Manifest.asset_paths(source)
+    if extension
+      paths = paths.select { |p| p.ends_with? ".#{extension}" }
+    end
+
+    # include full webpack-dev-server url for rspec tests running locally
+    if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled
+      host = Rails.configuration.webpack.dev_server.host
+      port = Rails.configuration.webpack.dev_server.port
+      protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
+
+      paths.map! do |p|
+        "#{protocol}://#{host}:#{port}#{p}"
+      end
+    end
+
+    paths
+  end
+end
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index a611481a0a43c3f4532cfd70206bd451f30d511e..19473b6ab276ddad03f0ae36d0d47ca9bf413829 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,9 +28,9 @@
   = stylesheet_link_tag "application", media: "all"
   = stylesheet_link_tag "print",       media: "print"
 
-  = javascript_include_tag(*webpack_asset_paths("runtime"))
-  = javascript_include_tag(*webpack_asset_paths("common"))
-  = javascript_include_tag(*webpack_asset_paths("main"))
+  = webpack_bundle_tag "runtime"
+  = webpack_bundle_tag "common"
+  = webpack_bundle_tag "main"
 
   - if content_for?(:page_specific_javascripts)
     = yield :page_specific_javascripts
diff --git a/changelogs/unreleased/27729-improve-webpack-dev-environment.yml b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d04ea70ab1ce0fb3d7a42d0eaca90ccba3789248
--- /dev/null
+++ b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml
@@ -0,0 +1,4 @@
+---
+title: Add webpack_bundle_tag helper to improve non-localhost GDK configurations
+merge_request: 10604
+author:
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 0fea3f8222b83cd1634e27d399c36ade718c8c0f..ffb161900933d5d1b1dc616c3020c9e2b5a69a40 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -11,6 +11,7 @@ var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeMod
 var ROOT_PATH = path.resolve(__dirname, '..');
 var IS_PRODUCTION = process.env.NODE_ENV === 'production';
 var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
+var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
 var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
 var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
 var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
@@ -182,12 +183,13 @@ if (IS_PRODUCTION) {
 if (IS_DEV_SERVER) {
   config.devtool = 'cheap-module-eval-source-map';
   config.devServer = {
+    host: DEV_SERVER_HOST,
     port: DEV_SERVER_PORT,
     headers: { 'Access-Control-Allow-Origin': '*' },
     stats: 'errors-only',
     inline: DEV_SERVER_LIVERELOAD
   };
-  config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
+  config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath;
   config.plugins.push(
     // watch node_modules for changes if we encounter a missing module compile error
     new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index e74eb72951599138c8e5115e1997c7078fc762f2..2ddcbe13afa7faf0c60c4668c1727c8e676034fe 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -48,8 +48,8 @@ Steps to split page-specific JavaScript from the main `main.js`:
 
 ```haml
 - content_for :page_specific_javascripts do
-  = page_specific_javascript_bundle_tag('lib_chart')
-  = page_specific_javascript_bundle_tag('graphs')
+  = webpack_bundle_tag 'lib_chart'
+  = webpack_bundle_tag 'graphs'
 ```
 
 The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`