Girder doesn't work behind a subpath reverse proxy
The girder web client and the API page was using relative paths like these
```html
<script src="static/built/girder_lib.min.js"></script>
<script src="../static/built/swagger/swagger-ui.min.js"></script>
```
It worked when being put under a subpath by a reverse proxy.
such as http://localhost/subpath/


However, the latest master, uses absolute path
```html
<script src="/static/built/girder_lib.min.js"></script>
<script src="/static/built/swagger/swagger-ui.min.js"></script>
```
It doesn't work behind reverse proxy. becuase the the resource doesn't exists at `http://localhost/static/built/girder_lib.min.js` 
It actually exist at 
`http://localhost/subpath/static/built/girder_lib.min.js`
(Font and the API page still won't work even if I remove the leading slash by monkey patching)

Setting core.route_table like these doesn't help 
```json
[{"key":"core.route_table","value":{"core_girder":"/","core_static_root":"/subpath/static"}}]
```
Becuase even ouptut changed to
```html
<script src="/subpath/static/built/girder_lib.min.js"></script>
```
the resource now actually exists at
`http://localhost/subpath/subpath/static/built/girder_lib.min.js`


I didn't pinpoint which commits changed the behavior, it might be caused by a few commits.
But girder at this hash works. 7327d0039d760114a09aafd6d102b89561144f63
The reverse proxy I was using is a node application, the test script is [here](https://gist.github.com/matthewma7/052c994f09fa11768af07f5424aad07e). 
diff --git a/Dockerfile b/Dockerfile
index 4308e5ceb..a00570a1b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,6 +22,6 @@ COPY . /girder/
 # TODO: Do we want to create editable installs of plugins as well?  We
 # will need a plugin only requirements file for this.
 RUN pip install --upgrade --upgrade-strategy eager --editable .
-RUN girder build --static-root /static
+RUN girder build --static-public-path /static
 
 ENTRYPOINT ["girder", "serve"]
diff --git a/Dockerfile.py3 b/Dockerfile.py3
index e7c609614..36a224569 100644
--- a/Dockerfile.py3
+++ b/Dockerfile.py3
@@ -27,6 +27,6 @@ ENV LANG=C.UTF-8
 # TODO: Do we want to create editable installs of plugins as well?  We
 # will need a plugin only requirements file for this.
 RUN pip install --upgrade --upgrade-strategy eager --editable .
-RUN girder build --static-root /static
+RUN girder build --static-public-path /static
 
 ENTRYPOINT ["girder", "serve"]
diff --git a/devops/ansible/roles/girder/library/test/test_setting.yml b/devops/ansible/roles/girder/library/test/test_setting.yml
index ba2e2bf6c..3322585d6 100644
--- a/devops/ansible/roles/girder/library/test/test_setting.yml
+++ b/devops/ansible/roles/girder/library/test/test_setting.yml
@@ -60,7 +60,6 @@
           key: "core.route_table"
           value:
             core_girder: "/girder"
-            core_static_root: "/static"
 
     - name: Set multiple settings for email
       girder:
diff --git a/docs/deploy.rst b/docs/deploy.rst
index e1db88753..6e9e887cd 100644
--- a/docs/deploy.rst
+++ b/docs/deploy.rst
@@ -28,7 +28,6 @@ example, we have the following:
 
     [server]
     api_root = "/girder/api/v1"
-    static_root = "/girder/static"
 
 .. note:: If your chosen proxy server does not add the appropriate
    ``X-Forwarded-Host`` header (containing the host used in http requests,
diff --git a/docs/migration-guide.rst b/docs/migration-guide.rst
index 255456b9c..3a096d341 100644
--- a/docs/migration-guide.rst
+++ b/docs/migration-guide.rst
@@ -181,18 +181,26 @@ itself depends on the core web client and all plugin web clients. The build proc
 place (in the Girder Python package) in both development and production installs. The built assets
 are installed into a virtual environment specific static path ``{sys.prefix}/share/girder``.
 
-Static root is required during web client build
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-The static root, indicating the base URL where web client files are served from,
-is now required when the web client is built. The primary implication is that if the static root
-setting is changed, the web client must be immediately rebuilt. Also, note the API changes:
+Static public path is required during web client build
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The static `public path <https://webpack.js.org/guides/public-path/>`_, indicating the base URL
+where web client files are served from, is now required when the web client is built. Most
+deployments can simply accept the default value of ``/static``, unless serving Girder from a CDN or
+mounting at a subpath using a reverse proxy.
 
+If the static public path setting is changed, the web client must be immediately rebuilt. When the
+web client is built without access to database settings, ``girder build --static-public-path ..``
+can be used to pass the static public path.
+
+The static public path setting replaces all previous "static root" functionality. Accordingly:
+
+* The server now serves all static content from ``/static``. The ``GIRDER_STATIC_ROUTE_ID`` constant
+  has been removed.
+* In the server, ``girder.utility.server.getStaticRoot`` has been removed.
 * In the web client, ``girder.rest.staticRoot``, ``girder.rest.getStaticRoot``, and
-  ``girder.rest.setStaticRoot`` have been removed
-* The ability to set the web client static root via the special element
+  ``girder.rest.setStaticRoot`` have been removed.
+* The ability to set the web client static root / public path via the special element
   ``<div id="g-global-info-staticroot">`` has been removed
-* In the server, the ``girder.utility.server.getStaticRoot()`` function now returns an absolute path
-  unless set otherwise (which is not recommended).
 
 Server changes
 ++++++++++++++
diff --git a/girder/api/api_docs.mako b/girder/api/api_docs.mako
index 9539fdbd3..651c1c861 100644
--- a/girder/api/api_docs.mako
+++ b/girder/api/api_docs.mako
@@ -3,13 +3,13 @@
   <head>
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>${brandName | h} - REST API Documentation</title>
-    <link rel="stylesheet" href="${staticRoot}/built/swagger/css/reset.css">
-    <link rel="stylesheet" href="${staticRoot}/built/swagger/css/screen.css">
-    <link rel="stylesheet" href="${staticRoot}/built/swagger/docs.css">
-    <link rel="icon" type="image/png" href="${staticRoot}/built/Girder_Favicon.png">
+    <link rel="stylesheet" href="${staticPublicPath}/built/swagger/css/reset.css">
+    <link rel="stylesheet" href="${staticPublicPath}/built/swagger/css/screen.css">
+    <link rel="stylesheet" href="${staticPublicPath}/built/swagger/docs.css">
+    <link rel="icon" type="image/png" href="${staticPublicPath}/built/Girder_Favicon.png">
     <style type="text/css">
       .response_throbber {
-        content: url("${staticRoot}/built/swagger/images/throbber.gif");
+        content: url("${staticPublicPath}/built/swagger/images/throbber.gif");
       }
       #api_info {
         display: none;
@@ -45,22 +45,22 @@
           class="swagger-ui-wrap docs-swagger-container">
       </div>
     </div>
-    <script src="${staticRoot}/built/swagger/lib/object-assign-pollyfill.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/jquery-1.8.0.min.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/jquery.slideto.min.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/jquery.wiggle.min.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/jquery.ba-bbq.min.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/handlebars-4.0.5.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/lodash.min.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/backbone-min.js"></script>
-    <script src="${staticRoot}/built/swagger/swagger-ui.min.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/highlight.9.1.0.pack.js"></script>
-    <script src="${staticRoot}/built/swagger/lib/highlight.9.1.0.pack_extended.js"></script>
-    <script src='${staticRoot}/built/swagger/lib/jsoneditor.min.js'></script>
-    <script src='${staticRoot}/built/swagger/lib/marked.js'></script>
-    <script src="${staticRoot}/built/swagger/girder-swagger.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/object-assign-pollyfill.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/jquery-1.8.0.min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/jquery.slideto.min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/jquery.wiggle.min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/jquery.ba-bbq.min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/handlebars-4.0.5.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/lodash.min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/backbone-min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/swagger-ui.min.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/highlight.9.1.0.pack.js"></script>
+    <script src="${staticPublicPath}/built/swagger/lib/highlight.9.1.0.pack_extended.js"></script>
+    <script src='${staticPublicPath}/built/swagger/lib/jsoneditor.min.js'></script>
+    <script src='${staticPublicPath}/built/swagger/lib/marked.js'></script>
+    <script src="${staticPublicPath}/built/swagger/girder-swagger.js"></script>
     % if mode == 'testing':
-    <script src="${staticRoot}/built/testing.min.js"></script>
+    <script src="${staticPublicPath}/built/testing.min.js"></script>
     % endif
   </body>
 </html>
diff --git a/girder/api/describe.py b/girder/api/describe.py
index aac62bab9..75a2dc2c4 100644
--- a/girder/api/describe.py
+++ b/girder/api/describe.py
@@ -456,18 +456,12 @@ class ApiDocs(WebrootBase):
         super(ApiDocs, self).__init__(templatePath)
 
         curConfig = config.getConfig()
-        mode = curConfig['server'].get('mode', '')
-
-        self.vars = {
-            'apiRoot': '',
-            'staticRoot': '',
-            'mode': mode
-        }
+        self.vars['mode'] = curConfig['server'].get('mode', '')
 
     def _renderHTML(self):
         from girder.utility import server
         self.vars['apiRoot'] = server.getApiRoot()
-        self.vars['staticRoot'] = server.getStaticRoot()
+        self.vars['staticPublicPath'] = Setting().get(SettingKey.STATIC_PUBLIC_PATH)
         self.vars['brandName'] = Setting().get(SettingKey.BRAND_NAME)
         return super(ApiDocs, self)._renderHTML()
 
diff --git a/girder/api/v1/system.py b/girder/api/v1/system.py
index c37441196..0066a5b27 100644
--- a/girder/api/v1/system.py
+++ b/girder/api/v1/system.py
@@ -28,8 +28,7 @@ import logging
 import traceback
 
 from girder.api import access
-from girder.constants import GIRDER_ROUTE_ID, GIRDER_STATIC_ROUTE_ID, \
-    SettingKey, TokenScope, ACCESS_FLAGS, VERSION
+from girder.constants import GIRDER_ROUTE_ID, SettingKey, TokenScope, ACCESS_FLAGS, VERSION
 from girder.exceptions import GirderException, ResourcePathNotFound, RestException
 from girder.models.collection import Collection
 from girder.models.file import File
@@ -208,7 +207,7 @@ class System(Resource):
         setting = Setting()
         routeTable = setting.get(SettingKey.ROUTE_TABLE)
         oldPlugins = setting.get(SettingKey.PLUGINS_ENABLED)
-        reservedRoutes = {GIRDER_ROUTE_ID, GIRDER_STATIC_ROUTE_ID}
+        reservedRoutes = {GIRDER_ROUTE_ID}
 
         routeTableChanged = False
         removedRoutes = (
diff --git a/girder/cli/build.py b/girder/cli/build.py
index d9a736729..508dc3ecf 100644
--- a/girder/cli/build.py
+++ b/girder/cli/build.py
@@ -27,9 +27,9 @@ import sys
 import click
 import six
 
-from girder.constants import STATIC_ROOT_DIR
+from girder.constants import SettingKey, STATIC_ROOT_DIR
+from girder.models.setting import Setting
 from girder.plugin import allPlugins, getPlugin
-from girder.utility.server import getStaticRoot
 
 # monkey patch shutil for python < 3
 if not six.PY3:
@@ -49,8 +49,8 @@ _GIRDER_BUILD_ASSETS_PATH = os.path.realpath(resource_filename('girder', 'web_cl
               help='Full path to the npm executable to use.')
 @click.option('--reinstall/--no-reinstall', default=True,
               help='Force regenerate node_modules.')
-@click.option('--static-root', help='Runtime static root path in the web client.')
-def main(dev, watch, watch_plugin, npm, reinstall, static_root):
+@click.option('--static-public-path', help='URL public path of static content.')
+def main(dev, watch, watch_plugin, npm, reinstall, static_public_path):
     if shutil.which(npm) is None:
         raise click.UsageError(
             'No npm executable was detected.  Please ensure the npm executable is in your '
@@ -82,7 +82,8 @@ def main(dev, watch, watch_plugin, npm, reinstall, static_root):
     buildCommand = [
         npm, 'run', 'build', '--',
         '--static-path=%s' % STATIC_ROOT_DIR,
-        '--static-url=%s' % (static_root or getStaticRoot()),
+        '--static-public-path=%s' % (
+            static_public_path or Setting().get(SettingKey.STATIC_PUBLIC_PATH)),
         quiet
     ]
     if watch:
diff --git a/girder/constants.py b/girder/constants.py
index 9291a309b..ea23f5069 100644
--- a/girder/constants.py
+++ b/girder/constants.py
@@ -34,7 +34,6 @@ ACCESS_FLAGS = {}
 
 # Identifier for Girder's entry in the route table
 GIRDER_ROUTE_ID = 'core_girder'
-GIRDER_STATIC_ROUTE_ID = 'core_static_root'
 
 # Threshold below which text search results will be sorted by their text score.
 # Setting this too high causes mongodb to use too many resources for searches
@@ -175,6 +174,7 @@ class SettingKey(object):
     SMTP_PASSWORD = 'core.smtp.password'
     SMTP_PORT = 'core.smtp.port'
     SMTP_USERNAME = 'core.smtp.username'
+    STATIC_PUBLIC_PATH = 'core.static_public_path'
     UPLOAD_MINIMUM_CHUNK_SIZE = 'core.upload_minimum_chunk_size'
     USER_DEFAULT_FOLDERS = 'core.user_default_folders'
 
@@ -215,6 +215,7 @@ class SettingDefault(object):
         SettingKey.SMTP_HOST: 'localhost',
         SettingKey.SMTP_PORT: 25,
         SettingKey.SMTP_ENCRYPTION: 'none',
+        SettingKey.STATIC_PUBLIC_PATH: '/static',
         SettingKey.UPLOAD_MINIMUM_CHUNK_SIZE: 1024 * 1024 * 5,
         SettingKey.USER_DEFAULT_FOLDERS: 'public_private'
     }
diff --git a/girder/models/setting.py b/girder/models/setting.py
index 817525e5c..23e044514 100644
--- a/girder/models/setting.py
+++ b/girder/models/setting.py
@@ -23,7 +23,7 @@ import pymongo
 import six
 import re
 
-from ..constants import GIRDER_ROUTE_ID, GIRDER_STATIC_ROUTE_ID, SettingDefault, SettingKey
+from ..constants import GIRDER_ROUTE_ID, SettingDefault, SettingKey
 from .model_base import Model
 from girder import logprint
 from girder.exceptions import ValidationException
@@ -275,6 +275,14 @@ class Setting(Model):
         if val and not val.startswith('http://') and not val.startswith('https://'):
             raise ValidationException('Server root must start with http:// or https://.', 'value')
 
+    @staticmethod
+    @setting_utilities.validator(SettingKey.STATIC_PUBLIC_PATH)
+    def validateCoreStaticRoot(doc):
+        val = doc['value']
+        if not (val.startswith('/') or re.match('^https?://', val)):
+            raise ValidationException(
+                'Static root must begin with a forward slash or contain a URL scheme.')
+
     @staticmethod
     @setting_utilities.validator(SettingKey.CORS_ALLOW_METHODS)
     def validateCoreCorsAllowMethods(doc):
@@ -380,19 +388,12 @@ class Setting(Model):
     @setting_utilities.validator(SettingKey.ROUTE_TABLE)
     def validateCoreRouteTable(doc):
         nonEmptyRoutes = [route for route in doc['value'].values() if route]
-        for key in [GIRDER_ROUTE_ID, GIRDER_STATIC_ROUTE_ID]:
-            if key not in doc['value'] or not doc['value'][key]:
-                raise ValidationException('Girder and static root must be routable.')
+        if GIRDER_ROUTE_ID not in doc['value'] or not doc['value'][GIRDER_ROUTE_ID]:
+            raise ValidationException('Girder root must be routable.')
 
         for key in doc['value']:
-            if (key != GIRDER_STATIC_ROUTE_ID and doc['value'][key] and
-                    not doc['value'][key].startswith('/')):
+            if (doc['value'][key] and not doc['value'][key].startswith('/')):
                 raise ValidationException('Routes must begin with a forward slash.')
-        if doc['value'].get(GIRDER_STATIC_ROUTE_ID):
-            if (not doc['value'][GIRDER_STATIC_ROUTE_ID].startswith('/') and
-                    '://' not in doc['value'][GIRDER_STATIC_ROUTE_ID]):
-                raise ValidationException(
-                    'Static root must begin with a forward slash or contain a URL scheme.')
 
         if len(nonEmptyRoutes) > len(set(nonEmptyRoutes)):
             raise ValidationException('Routes must be unique.')
@@ -401,8 +402,7 @@ class Setting(Model):
     @setting_utilities.default(SettingKey.ROUTE_TABLE)
     def defaultCoreRouteTable():
         return {
-            GIRDER_ROUTE_ID: '/',
-            GIRDER_STATIC_ROUTE_ID: '/static'
+            GIRDER_ROUTE_ID: '/'
         }
 
     @staticmethod
diff --git a/girder/utility/server.py b/girder/utility/server.py
index ebc5224ff..c0c4ec15d 100644
--- a/girder/utility/server.py
+++ b/girder/utility/server.py
@@ -51,11 +51,6 @@ def getApiRoot():
     return config.getConfig()['server']['api_root']
 
 
-def getStaticRoot():
-    routeTable = loadRouteTable()
-    return routeTable[constants.GIRDER_STATIC_ROUTE_ID]
-
-
 def configureServer(test=False, plugins=None, curConfig=None):
     """
     Function to setup the cherrypy server. It configures it, but does
@@ -93,8 +88,6 @@ def configureServer(test=False, plugins=None, curConfig=None):
         curConfig.update({'server': {
             'mode': 'testing',
             'api_root': 'api/v1',
-            'static_root': 'static',
-            'api_static_root': '../static',
             'cherrypy_server': True
         }})
 
@@ -124,7 +117,6 @@ def configureServer(test=False, plugins=None, curConfig=None):
         'serverRoot': root,
         'serverRootPath': routeTable[constants.GIRDER_ROUTE_ID],
         'apiRoot': root.api.v1,
-        'staticRoot': routeTable[constants.GIRDER_STATIC_ROUTE_ID]
     }
 
     plugin._loadPlugins(plugins, info)
@@ -150,6 +142,11 @@ def loadRouteTable(reconcileRoutes=False):
     def reconcileRouteTable(routeTable):
         hasChanged = False
 
+        # Migration for the removed static root setting
+        if 'core_static_root' in routeTable:
+            del routeTable['core_static_root']
+            hasChanged = True
+
         for name in pluginWebroots.keys():
             if name not in routeTable:
                 routeTable[name] = os.path.join('/', name)
@@ -188,11 +185,10 @@ def setup(test=False, plugins=None, curConfig=None):
         girderWebroot, str(routeTable[constants.GIRDER_ROUTE_ID]), appconf)
 
     # Mount static files
-    cherrypy.tree.mount(None, routeTable[constants.GIRDER_STATIC_ROUTE_ID],
+    cherrypy.tree.mount(None, '/static',
                         {'/':
-                         # Only turn on if something has been created by 'girder build'
-                         {'tools.staticdir.on': os.path.exists(constants.STATIC_ROOT_DIR),
-                          'tools.staticdir.dir': constants.STATIC_ROOT_DIR,
+                         {'tools.staticdir.on': True,
+                          'tools.staticdir.dir': os.path.join(constants.STATIC_ROOT_DIR),
                           'request.show_tracebacks': appconf['/']['request.show_tracebacks'],
                           'response.headers.server': 'Girder %s' % __version__,
                           'error_page.default': _errorDefault}})
diff --git a/girder/utility/webroot.mako b/girder/utility/webroot.mako
index b404dfb43..64f1d1fa9 100644
--- a/girder/utility/webroot.mako
+++ b/girder/utility/webroot.mako
@@ -3,16 +3,16 @@
   <head>
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>${brandName | h}</title>
-    <link rel="stylesheet" href="${staticRoot}/built/girder_lib.min.css">
-    <link rel="icon" type="image/png" href="${staticRoot}/built/Girder_Favicon.png">
+    <link rel="stylesheet" href="${staticPublicPath}/built/girder_lib.min.css">
+    <link rel="icon" type="image/png" href="${staticPublicPath}/built/Girder_Favicon.png">
     % for plugin in pluginCss:
-    <link rel="stylesheet" href="${staticRoot}/built/plugins/${plugin}/plugin.min.css">
+    <link rel="stylesheet" href="${staticPublicPath}/built/plugins/${plugin}/plugin.min.css">
     % endfor
   </head>
   <body>
     <div id="g-global-info-apiroot" class="hide">${apiRoot}</div>
-    <script src="${staticRoot}/built/girder_lib.min.js"></script>
-    <script src="${staticRoot}/built/girder_app.min.js"></script>
+    <script src="${staticPublicPath}/built/girder_lib.min.js"></script>
+    <script src="${staticPublicPath}/built/girder_app.min.js"></script>
     <script type="text/javascript">
         $(function () {
             girder.events.trigger('g:appload.before');
@@ -30,7 +30,7 @@
         });
     </script>
     % for plugin in pluginJs:
-    <script src="${staticRoot}/built/plugins/${plugin}/plugin.min.js"></script>
+    <script src="${staticPublicPath}/built/plugins/${plugin}/plugin.min.js"></script>
     % endfor
   </body>
 </html>
diff --git a/girder/utility/webroot.py b/girder/utility/webroot.py
index 6b09a5910..8ded85469 100644
--- a/girder/utility/webroot.py
+++ b/girder/utility/webroot.py
@@ -135,7 +135,7 @@ class Webroot(WebrootBase):
                 self.vars['pluginJs'].append(plugin)
 
         self.vars['apiRoot'] = server.getApiRoot()
-        self.vars['staticRoot'] = server.getStaticRoot()
+        self.vars['staticPublicPath'] = Setting().get(SettingKey.STATIC_PUBLIC_PATH)
         self.vars['brandName'] = Setting().get(SettingKey.BRAND_NAME)
         self.vars['contactEmail'] = Setting().get(
             SettingKey.CONTACT_EMAIL_ADDRESS)
diff --git a/girder/web_client/Gruntfile.js b/girder/web_client/Gruntfile.js
index fc8828c4e..67fc2ffa2 100644
--- a/girder/web_client/Gruntfile.js
+++ b/girder/web_client/Gruntfile.js
@@ -53,7 +53,7 @@ module.exports = function (grunt) {
         pkg: grunt.file.readJSON('package.json'),
         staticDir: '.',
         builtPath: path.resolve(grunt.option('static-path') || '.', 'built'),
-        staticUrl: grunt.option('static-url') || '/static',
+        staticPublicPath: grunt.option('static-public-path') || '/static',
         default: {}
     });
 
diff --git a/girder/web_client/grunt_tasks/build.js b/girder/web_client/grunt_tasks/build.js
index 92889ced2..3129d9098 100644
--- a/girder/web_client/grunt_tasks/build.js
+++ b/girder/web_client/grunt_tasks/build.js
@@ -151,10 +151,10 @@ module.exports = function (grunt) {
         progress: progress
     });
 
-    // Set the publicPath based on Girder's static root
+    // Set the publicPath based on Girder's static public path
     updateWebpackConfig({
         output: {
-            publicPath: path.join(grunt.config.get('staticUrl'), 'built/')
+            publicPath: path.join(grunt.config.get('staticPublicPath'), 'built/')
         }
     });
 
diff --git a/girder/web_client/src/templates/body/systemConfiguration.pug b/girder/web_client/src/templates/body/systemConfiguration.pug
index 2293cebd1..57e94ab8a 100644
--- a/girder/web_client/src/templates/body/systemConfiguration.pug
+++ b/girder/web_client/src/templates/body/systemConfiguration.pug
@@ -167,6 +167,17 @@ form.g-settings-form(role="form")
           type="password", value=settings['core.smtp.password'] || '',
           placeholder=`Default: ${defaults['core.smtp.password'] || 'none'}`,
           title="The password, if any, used to authenticate to the SMTP server used to send emails.")
+
+  .g-settings-form-container
+    h4 Client configuration
+    .form-group
+      label(for="g-core-static-public-path") Static public path
+      input#g-core-static-public-path.form-control.input-sm(
+          type="text",
+          value=settings['core.static_public_path'] || '',
+          placeholder=`Default: ${defaults['core.static_public_path']}`,
+          title="The path where clients should expect to fetch static content.")
+
   .g-settings-form-container
     h4 Routing
     p.
@@ -179,12 +190,6 @@ form.g-settings-form(role="form")
           value=routes['core_girder'] || '',
           data-webroot-name="core_girder",
           title="The path to place core_girder.")
-      label core_static_root
-      input.g-core-route-table.form-control.input-sm(
-          type="text",
-          value=routes['core_static_root'] || '',
-          data-webroot-name="core_static_root",
-          title="The path to place core_static_root.")
     if routeKeys.length > 2
       .form-group
         h5 Other Routes
diff --git a/girder/web_client/src/views/body/SystemConfigurationView.js b/girder/web_client/src/views/body/SystemConfigurationView.js
index 104f300d8..10deb1464 100644
--- a/girder/web_client/src/views/body/SystemConfigurationView.js
+++ b/girder/web_client/src/views/body/SystemConfigurationView.js
@@ -113,7 +113,8 @@ var SystemConfigurationView = View.extend({
             'core.add_to_group_policy',
             'core.collection_create_policy',
             'core.user_default_folders',
-            'core.route_table'
+            'core.route_table',
+            'core.static_public_path'
         ];
         this.settingsKeys = keys;
         restRequest({
diff --git a/scripts/publicNames.txt b/scripts/publicNames.txt
index 141442a63..4f57bfd8c 100644
--- a/scripts/publicNames.txt
+++ b/scripts/publicNames.txt
@@ -389,7 +389,6 @@ girder
             USER_SELF_ACCESS
             WEBROOT_SETTING_CHANGE
         GIRDER_ROUTE_ID
-        GIRDER_STATIC_ROUTE_ID
         LOG_BACKUP_COUNT
         LOG_ROOT
         MAX_LOG_SIZE
@@ -428,6 +427,7 @@ girder
             SMTP_PASSWORD
             SMTP_PORT
             SMTP_USERNAME
+            STATIC_PUBLIC_PATH
             UPLOAD_MINIMUM_CHUNK_SIZE
             USER_DEFAULT_FOLDERS
         SortDir
@@ -726,6 +726,7 @@ girder
                 validateCoreSmtpPassword
                 validateCoreSmtpPort
                 validateCoreSmtpUsername
+                validateCoreStaticRoot
                 validateCoreUploadMinimumChunkSize
                 validateCoreUserDefaultFolders
                 validateEnableNotificationStream
@@ -944,7 +945,6 @@ girder
             configureServer
             getApiRoot
             getPlugins
-            getStaticRoot
             loadRouteTable
             setup
             staticFile
diff --git a/test/test_route_table.py b/test/test_route_table.py
index dcee16d8b..9d903d3e6 100644
--- a/test/test_route_table.py
+++ b/test/test_route_table.py
@@ -23,7 +23,7 @@ import pytest
 from pytest_girder.assertions import assertStatusOk
 from pytest_girder.utils import getResponseBody
 
-from girder.constants import GIRDER_ROUTE_ID, GIRDER_STATIC_ROUTE_ID, SettingKey
+from girder.constants import GIRDER_ROUTE_ID, SettingKey
 from girder.exceptions import ValidationException
 from girder.models.setting import Setting
 from girder.plugin import GirderPlugin, registerPluginWebroot
@@ -42,22 +42,16 @@ class HasWebroot(GirderPlugin):
 
 
 @pytest.mark.parametrize('value,err', [
-    ({}, r'Girder and static root must be routable\.$'),
-    ({GIRDER_ROUTE_ID: '/'}, r'Girder and static root must be routable\.$'),
+    ({}, r'Girder root must be routable\.$'),
+    ({'other': '/some_route'}, r'Girder root must be routable\.$'),
     ({
         GIRDER_ROUTE_ID: '/some_route',
-        GIRDER_STATIC_ROUTE_ID: '/static',
         'other': '/some_route'
     }, r'Routes must be unique\.$'),
     ({
         GIRDER_ROUTE_ID: '/',
-        GIRDER_STATIC_ROUTE_ID: '/static',
         'other': 'route_without_a_leading_slash'
-    }, r'Routes must begin with a forward slash\.$'),
-    ({
-        GIRDER_ROUTE_ID: '/',
-        GIRDER_STATIC_ROUTE_ID: 'relative/static'
-    }, r'Static root must begin with a forward slash or contain a URL scheme\.$')
+    }, r'Routes must begin with a forward slash\.$')
 ])
 def testRouteTableValidationFailure(value, err):
     with pytest.raises(ValidationException, match=err):
@@ -70,7 +64,6 @@ def testRouteTableValidationFailure(value, err):
 @pytest.mark.parametrize('value', [
     {
         GIRDER_ROUTE_ID: '/',
-        GIRDER_STATIC_ROUTE_ID: 'http://127.0.0.1/static'
     }
 ])
 def testRouteTableValidationSuccess(value):
@@ -84,7 +77,6 @@ def testRouteTableValidationSuccess(value):
 def testRouteTableBehavior(server, admin):
     Setting().set(SettingKey.ROUTE_TABLE, {
         GIRDER_ROUTE_ID: '/',
-        GIRDER_STATIC_ROUTE_ID: '/static',
         'has_webroot': '/has_webroot'
     })
 
diff --git a/test/test_webroot.py b/test/test_webroot.py
index 95733e00a..261b339f5 100644
--- a/test/test_webroot.py
+++ b/test/test_webroot.py
@@ -17,7 +17,7 @@
 #  limitations under the License.
 ###############################################################################
 
-from girder.constants import GIRDER_ROUTE_ID, GIRDER_STATIC_ROUTE_ID, SettingKey
+from girder.constants import GIRDER_ROUTE_ID, SettingKey
 from girder.models.setting import Setting
 from pytest_girder.assertions import assertStatusOk
 from pytest_girder.utils import getResponseBody
@@ -72,10 +72,7 @@ def testAccessWebRoot(server, db):
 
 
 def testWebRootProperlyHandlesStaticRouteUrls(server, db):
-    Setting().set(SettingKey.ROUTE_TABLE, {
-        GIRDER_ROUTE_ID: '/',
-        GIRDER_STATIC_ROUTE_ID: 'http://my-cdn-url.com/static'
-    })
+    Setting().set(SettingKey.STATIC_PUBLIC_PATH, 'http://my-cdn-url.com/static')
 
     resp = server.request(path='/', method='GET', isJson=False, prefix='')
     assertStatusOk(resp)
['girder/web_client/src/views/body/SystemConfigurationView.js', 'scripts/publicNames.txt', 'girder/web_client/src/templates/body/systemConfiguration.pug', 'girder/models/setting.py', 'girder/api/describe.py', 'girder/web_client/Gruntfile.js', 'Dockerfile', 'devops/ansible/roles/girder/library/test/test_setting.yml', 'girder/utility/webroot.mako', 'docs/migration-guide.rst', 'girder/api/api_docs.mako', 'girder/utility/server.py', 'test/test_route_table.py', 'girder/utility/webroot.py', 'girder/constants.py', 'girder/api/v1/system.py', 'docs/deploy.rst', 'girder/web_client/grunt_tasks/build.js', 'Dockerfile.py3', 'test/test_webroot.py', 'girder/cli/build.py']