diff --git a/jupyterhub_config.py b/jupyterhub_config.py
index dd28dcc72b93b1d13d25258039bec54127cf0e39..74ac18abf248405306249b20c367ea1c8c4c5d86 100644
--- a/jupyterhub_config.py
+++ b/jupyterhub_config.py
@@ -1000,1481 +1000,114 @@ c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
 #  Default: None
 # c.JupyterHub.user_redirect_hook = None
 
-#------------------------------------------------------------------------------
-# CryptKeeper(SingletonConfigurable) configuration
-#------------------------------------------------------------------------------
-## Encapsulate encryption configuration
-#  
-#      Use via the encryption_config singleton below.
-
-#  Default: []
-# c.CryptKeeper.keys = []
-
-## The number of threads to allocate for encryption
-#  Default: 16
-# c.CryptKeeper.n_threads = 16
-
-#------------------------------------------------------------------------------
-# Authenticator(LoggingConfigurable) configuration
-#------------------------------------------------------------------------------
-## Base class for implementing an authentication provider for JupyterHub
-
-## Set of users that will be granted admin rights on this JupyterHub.
-#  
-#  Note:
-#  
-#      As of JupyterHub 2.0,
-#      full admin rights should not be required,
-#      and more precise permissions can be managed via roles.
-#  
-#  Caution:
-#  
-#      Adding users to `admin_users` can only *grant* admin rights,
-#      removing a username from the admin_users set **DOES NOT** remove admin rights previously granted.
-#  
-#      For an authoritative, restricted set of admins,
-#      assign explicit membership of the `admin` *role*::
-#  
-#          c.JupyterHub.load_roles = [
-#              {
-#                  "name": "admin",
-#                  "users": ["admin1", "..."],
-#              }
-#          ]
-#  
-#  Admin users can take every possible action on behalf of all users, for
-#  example:
-#  
-#  - Use the admin panel to see list of users logged in - Add / remove users in
-#  some authenticators - Restart / halt the hub - Start / stop users' single-user
-#  servers - Can access each individual users' single-user server
-#  
-#  Admin access should be treated the same way root access is.
-#  
-#  Defaults to an empty set, in which case no user has admin access.
-#  Default: set()
-# c.Authenticator.admin_users = set()
-
-## Allow every user who can successfully authenticate to access JupyterHub.
-#  
-#  False by default, which means for most Authenticators, _some_ allow-related
-#  configuration is required to allow users to log in.
-#  
-#  Authenticator subclasses may override the default with e.g.::
-#  
-#      @default("allow_all")
-#      def _default_allow_all(self):
-#          # if _any_ auth config (depends on the Authenticator)
-#          if self.allowed_users or self.allowed_groups or self.allow_existing_users:
-#              return False
-#          else:
-#              return True
-#  
-#  .. versionadded:: 5.0
-#  
-#  .. versionchanged:: 5.0
-#      Prior to 5.0, `allow_all` wasn't defined on its own,
-#      and was instead implicitly True when no allow config was provided,
-#      i.e. `allowed_users` unspecified or empty on the base Authenticator class.
-#  
-#      To preserve pre-5.0 behavior,
-#      set `allow_all = True` if you have no other allow configuration.
-#  Default: False
-c.Authenticator.allow_all = True
-
-## Allow existing users to login.
-#  
-#  Defaults to True if `allowed_users` is set for historical reasons, and False
-#  otherwise.
-#  
-#  With this enabled, all users present in the JupyterHub database are allowed to
-#  login. This has the effect of any user who has _previously_ been allowed to
-#  login via any means will continue to be allowed until the user is deleted via
-#  the /hub/admin page or REST API.
-#  
-#  .. warning::
-#  
-#     Before enabling this you should review the existing users in the
-#     JupyterHub admin panel at `/hub/admin`. You may find users existing
-#     there because they have previously been declared in config such as
-#     `allowed_users` or allowed to sign in.
-#  
-#  .. warning::
-#  
-#     When this is enabled and you wish to remove access for one or more
-#     users previously allowed, you must make sure that they
-#     are removed from the jupyterhub database. This can be tricky to do
-#     if you stop allowing an externally managed group of users for example.
-#  
-#  With this enabled, JupyterHub admin users can visit `/hub/admin` or use
-#  JupyterHub's REST API to add and remove users to manage who can login.
-#  
-#  .. versionadded:: 5.0
-#  Default: False
-# c.Authenticator.allow_existing_users = False
-
-## Set of usernames that are allowed to log in.
-#  
-#  Use this to limit which authenticated users may login. Default behavior: only
-#  users in this set are allowed.
-#  
-#  If empty, does not perform any restriction, in which case any authenticated
-#  user is allowed.
-#  
-#  Authenticators may extend :meth:`.Authenticator.check_allowed` to combine
-#  `allowed_users` with other configuration to either expand or restrict access.
-#  
-#  .. versionchanged:: 1.2
-#      `Authenticator.whitelist` renamed to `allowed_users`
-#  Default: set()
-# c.Authenticator.allowed_users = set()
-
-## Is there any allow config?
-#  
-#          Used to show a warning if it looks like nobody can access the Hub,
-#          which can happen when upgrading to JupyterHub 5,
-#          now that `allow_all` defaults to False.
-#  
-#          Deployments can set this explicitly to True to suppress
-#          the "No allow config found" warning.
-#  
-#          Will be True if any config tagged with `.tag(allow_config=True)`
-#          or starts with `allow` is truthy.
-#  
-#          .. versionadded:: 5.0
-#  Default: False
-# c.Authenticator.any_allow_config = False
-
-## The max age (in seconds) of authentication info
-#          before forcing a refresh of user auth info.
-#  
-#          Refreshing auth info allows, e.g. requesting/re-validating auth
-#  tokens.
-#  
-#          See :meth:`.refresh_user` for what happens when user auth info is refreshed
-#          (nothing by default).
-#  Default: 300
-# c.Authenticator.auth_refresh_age = 300
-
-## Automatically begin the login process
-#  
-#          rather than starting with a "Login with..." link at `/hub/login`
-#  
-#          To work, `.login_url()` must give a URL other than the default `/hub/login`,
-#          such as an oauth handler or another automatic login handler,
-#          registered with `.get_handlers()`.
-#  
-#          .. versionadded:: 0.8
-#  Default: False
-# c.Authenticator.auto_login = False
-
-## Automatically begin login process for OAuth2 authorization requests
-#  
-#  When another application is using JupyterHub as OAuth2 provider, it sends
-#  users to `/hub/api/oauth2/authorize`. If the user isn't logged in already, and
-#  auto_login is not set, the user will be dumped on the hub's home page, without
-#  any context on what to do next.
-#  
-#  Setting this to true will automatically redirect users to login if they aren't
-#  logged in *only* on the `/hub/api/oauth2/authorize` endpoint.
-#  
-#  .. versionadded:: 1.5
-#  Default: False
-# c.Authenticator.auto_login_oauth2_authorize = False
-
-## Set of usernames that are not allowed to log in.
-#  
-#  Use this with supported authenticators to restrict which users can not log in.
-#  This is an additional block list that further restricts users, beyond whatever
-#  restrictions the authenticator has in place.
-#  
-#  If empty, does not perform any additional restriction.
-#  
-#  .. versionadded: 0.9
-#  
-#  .. versionchanged:: 5.2
-#      Users blocked via `blocked_users` that may have logged in in the past
-#      have all permissions and group membership revoked
-#      and all servers stopped at JupyterHub startup.
-#      Previously, User permissions (e.g. API tokens)
-#      and servers were unaffected and required additional
-#      administrator operations to block after a user is added to `blocked_users`.
-#  
-#  .. versionchanged:: 1.2
-#      `Authenticator.blacklist` renamed to `blocked_users`
-#  Default: set()
-# c.Authenticator.blocked_users = set()
-
-## Delete any users from the database that do not pass validation
-#  
-#          When JupyterHub starts, `.add_user` will be called
-#          on each user in the database to verify that all users are still valid.
-#  
-#          If `delete_invalid_users` is True,
-#          any users that do not pass validation will be deleted from the database.
-#          Use this if users might be deleted from an external system,
-#          such as local user accounts.
-#  
-#          If False (default), invalid users remain in the Hub's database
-#          and a warning will be issued.
-#          This is the default to avoid data loss due to config changes.
-#  Default: False
-# c.Authenticator.delete_invalid_users = False
-
-## Enable persisting auth_state (if available).
-#  
-#          auth_state will be encrypted and stored in the Hub's database.
-#          This can include things like authentication tokens, etc.
-#          to be passed to Spawners as environment variables.
-#  
-#          Encrypting auth_state requires the cryptography package.
-#  
-#          Additionally, the JUPYTERHUB_CRYPT_KEY environment variable must
-#          contain one (or more, separated by ;) 32B encryption keys.
-#          These can be either base64 or hex-encoded.
-#  
-#          If encryption is unavailable, auth_state cannot be persisted.
-#  
-#          New in JupyterHub 0.8
-#  Default: False
-# c.Authenticator.enable_auth_state = False
-
-## Let authenticator manage user groups
-#  
-#          If True, Authenticator.authenticate and/or .refresh_user
-#          may return a list of group names in the 'groups' field,
-#          which will be assigned to the user.
-#  
-#          All group-assignment APIs are disabled if this is True.
-#  Default: False
-# c.Authenticator.manage_groups = False
-
-## Let authenticator manage roles
-#  
-#          If True, Authenticator.authenticate and/or .refresh_user
-#          may return a list of roles in the 'roles' field,
-#          which will be added to the database.
-#  
-#          When enabled, all role management will be handled by the
-#          authenticator; in particular, assignment of roles via
-#          `JupyterHub.load_roles` traitlet will not be possible.
-#  
-#          .. versionadded:: 5.0
-#  Default: False
-# c.Authenticator.manage_roles = False
-
-## The prompt string for the extra OTP (One Time Password) field.
-#  
-#  .. versionadded:: 5.0
-#  Default: 'OTP:'
-# c.Authenticator.otp_prompt = 'OTP:'
-
-## An optional hook function that you can implement to do some bootstrapping work
-#  during authentication. For example, loading user account details from an
-#  external system.
-#  
-#  This function is called after the user has passed all authentication checks
-#  and is ready to successfully authenticate. This function must return the
-#  auth_model dict reguardless of changes to it. The hook is called with 3
-#  positional arguments: `(authenticator, handler, auth_model)`.
-#  
-#  This may be a coroutine.
-#  
-#  .. versionadded: 1.0
-#  
-#  Example::
-#  
-#      import os
-#      import pwd
-#      def my_hook(authenticator, handler, auth_model):
-#          user_data = pwd.getpwnam(auth_model['name'])
-#          spawn_data = {
-#              'pw_data': user_data
-#              'gid_list': os.getgrouplist(auth_model['name'], user_data.pw_gid)
-#          }
-#  
-#          if auth_model['auth_state'] is None:
-#              auth_model['auth_state'] = {}
-#          auth_model['auth_state']['spawn_data'] = spawn_data
-#  
-#          return auth_model
-#  
-#      c.Authenticator.post_auth_hook = my_hook
-#  Default: None
-# c.Authenticator.post_auth_hook = None
-
-## Force refresh of auth prior to spawn.
-#  
-#          This forces :meth:`.refresh_user` to be called prior to launching
-#          a server, to ensure that auth state is up-to-date.
-#  
-#          This can be important when e.g. auth tokens that may have expired
-#          are passed to the spawner via environment variables from auth_state.
-#  
-#          If refresh_user cannot refresh the user auth data,
-#          launch will fail until the user logs in again.
-#  Default: False
-# c.Authenticator.refresh_pre_spawn = False
-
-## Prompt for OTP (One Time Password) in the login form.
-#  
-#  .. versionadded:: 5.0
-#  Default: False
-# c.Authenticator.request_otp = False
-
-## Reset managed roles to result of `load_managed_roles()` on startup.
-#  
-#          If True:
-#            - stale managed roles will be removed,
-#            - stale assignments to managed roles will be removed.
-#  
-#          Any role not present in `load_managed_roles()` will be considered
-#  'stale'.
-#  
-#          The 'stale' status for role assignments is also determined from
-#  `load_managed_roles()` result:
-#  
-#          - user role assignments status will depend on whether the `users` key
-#  is defined or not:
-#  
-#            * if a list is defined under the `users` key and the user is not listed, then the user role assignment will be considered 'stale',
-#            * if the `users` key is not provided, the user role assignment will be preserved;
-#          - service and group role assignments will be considered 'stale':
-#  
-#            * if not included in the `services` and `groups` list,
-#            * if the `services` and `groups` keys are not provided.
-#  
-#          .. versionadded:: 5.0
-#  Default: False
-# c.Authenticator.reset_managed_roles_on_startup = False
-
-## Dictionary mapping authenticator usernames to JupyterHub users.
-#  
-#          Primarily used to normalize OAuth user names to local users.
-#  Default: {}
-# c.Authenticator.username_map = {}
-
-## Regular expression pattern that all valid usernames must match.
-#  
-#  If a username does not match the pattern specified here, authentication will
-#  not be attempted.
-#  
-#  If not set, allow any username.
-#  Default: ''
-# c.Authenticator.username_pattern = ''
-
-## Deprecated, use `Authenticator.allowed_users`
-#  Default: set()
-# c.Authenticator.whitelist = set()
-
-#------------------------------------------------------------------------------
-# DummyAuthenticator(Authenticator) configuration
-#------------------------------------------------------------------------------
-## Dummy Authenticator for testing
-#  
-#      By default, any username + password is allowed
-#      If a non-empty password is set, any username will be allowed
-#      if it logs in with that password.
-#  
-#      .. versionadded:: 1.0
-#  
-#      .. versionadded:: 5.0
-#          `allow_all` defaults to True,
-#          preserving default behavior.
-
-## 
-#  See also: Authenticator.admin_users
-# c.DummyAuthenticator.admin_users = set()
-
-## 
-#  See also: Authenticator.allow_all
-# c.DummyAuthenticator.allow_all = False
-
-## 
-#  See also: Authenticator.allow_existing_users
-# c.DummyAuthenticator.allow_existing_users = False
-
-## 
-#  See also: Authenticator.allowed_users
-# c.DummyAuthenticator.allowed_users = set()
-
-## Is there any allow config?
-#  See also: Authenticator.any_allow_config
-# c.DummyAuthenticator.any_allow_config = False
-
-## The max age (in seconds) of authentication info
-#  See also: Authenticator.auth_refresh_age
-# c.DummyAuthenticator.auth_refresh_age = 300
-
-## Automatically begin the login process
-#  See also: Authenticator.auto_login
-# c.DummyAuthenticator.auto_login = False
-
-## 
-#  See also: Authenticator.auto_login_oauth2_authorize
-# c.DummyAuthenticator.auto_login_oauth2_authorize = False
-
-## 
-#  See also: Authenticator.blocked_users
-# c.DummyAuthenticator.blocked_users = set()
-
-## Delete any users from the database that do not pass validation
-#  See also: Authenticator.delete_invalid_users
-# c.DummyAuthenticator.delete_invalid_users = False
-
-## Enable persisting auth_state (if available).
-#  See also: Authenticator.enable_auth_state
-# c.DummyAuthenticator.enable_auth_state = False
-
-## Let authenticator manage user groups
-#  See also: Authenticator.manage_groups
-# c.DummyAuthenticator.manage_groups = False
-
-## Let authenticator manage roles
-#  See also: Authenticator.manage_roles
-# c.DummyAuthenticator.manage_roles = False
-
-## 
-#  See also: Authenticator.otp_prompt
-# c.DummyAuthenticator.otp_prompt = 'OTP:'
-
-## Set a global password for all users wanting to log in.
-#  
-#  This allows users with any username to log in with the same static password.
-#  Default: ''
-# c.DummyAuthenticator.password = ''
-
-## 
-#  See also: Authenticator.post_auth_hook
-# c.DummyAuthenticator.post_auth_hook = None
-
-## Force refresh of auth prior to spawn.
-#  See also: Authenticator.refresh_pre_spawn
-# c.DummyAuthenticator.refresh_pre_spawn = False
-
-## 
-#  See also: Authenticator.request_otp
-# c.DummyAuthenticator.request_otp = False
-
-## Reset managed roles to result of `load_managed_roles()` on startup.
-#  See also: Authenticator.reset_managed_roles_on_startup
-# c.DummyAuthenticator.reset_managed_roles_on_startup = False
-
-## Dictionary mapping authenticator usernames to JupyterHub users.
-#  See also: Authenticator.username_map
-# c.DummyAuthenticator.username_map = {}
-
-## 
-#  See also: Authenticator.username_pattern
-# c.DummyAuthenticator.username_pattern = ''
-
-## Deprecated, use `Authenticator.allowed_users`
-#  See also: Authenticator.whitelist
-# c.DummyAuthenticator.whitelist = set()
-
-#------------------------------------------------------------------------------
-# Proxy(LoggingConfigurable) configuration
-#------------------------------------------------------------------------------
-## Base class for configurable proxies that JupyterHub can use.
-#  
-#      A proxy implementation should subclass this and must define the following
-#  methods:
-#  
-#      - :meth:`.get_all_routes` return a dictionary of all JupyterHub-related routes
-#      - :meth:`.add_route` adds a route
-#      - :meth:`.delete_route` deletes a route
-#  
-#      In addition to these, the following method(s) may need to be implemented:
-#  
-#      - :meth:`.start` start the proxy, if it should be launched by the Hub
-#        instead of externally managed.
-#        If the proxy is externally managed, it should set :attr:`should_start` to False.
-#      - :meth:`.stop` stop the proxy. Only used if :meth:`.start` is also used.
-#  
-#      And the following method(s) are optional, but can be provided:
-#  
-#      - :meth:`.get_route` gets a single route.
-#        There is a default implementation that extracts data from :meth:`.get_all_routes`,
-#        but implementations may choose to provide a more efficient implementation
-#        of fetching a single route.
-
-## Additional routes to be maintained in the proxy.
-#  
-#  A dictionary with a route specification as key, and a URL as target. The hub
-#  will ensure this route is present in the proxy.
-#  
-#  If the hub is running in host based mode (with JupyterHub.subdomain_host set),
-#  the routespec *must* have a domain component (example.com/my-url/). If the hub
-#  is not running in host based mode, the routespec *must not* have a domain
-#  component (/my-url/).
-#  
-#  Helpful when the hub is running in API-only mode.
-#  Default: {}
-# c.Proxy.extra_routes = {}
-
-## Should the Hub start the proxy
-#  
-#          If True, the Hub will start the proxy and stop it.
-#          Set to False if the proxy is managed externally,
-#          such as by systemd, docker, or another service manager.
-#  Default: True
-# c.Proxy.should_start = True
-
-#------------------------------------------------------------------------------
-# ConfigurableHTTPProxy(Proxy) configuration
-#------------------------------------------------------------------------------
-## Proxy implementation for the default configurable-http-proxy.
-#  
-#      This is the default proxy implementation
-#      for running the nodejs proxy `configurable-http-proxy`.
-#  
-#      If the proxy should not be run as a subprocess of the Hub,
-#      (e.g. in a separate container),
-#      set::
-#  
-#          c.ConfigurableHTTPProxy.should_start = False
-
-## The ip (or hostname) of the proxy's API endpoint
-#  Default: ''
-# c.ConfigurableHTTPProxy.api_url = ''
-
-## The Proxy auth token
-#  
-#          Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
-#  Default: ''
-# c.ConfigurableHTTPProxy.auth_token = ''
-
-## Interval (in seconds) at which to check if the proxy is running.
-#  Default: 5
-# c.ConfigurableHTTPProxy.check_running_interval = 5
-
-## The command to start the proxy
-#  Default: ['configurable-http-proxy']
-# c.ConfigurableHTTPProxy.command = ['configurable-http-proxy']
-
-## The number of requests allowed to be concurrently outstanding to the proxy
-#  
-#  Limiting this number avoids potential timeout errors by sending too many
-#  requests to update the proxy at once
-#  Default: 10
-# c.ConfigurableHTTPProxy.concurrency = 10
-
-## Add debug-level logging to the Proxy.
-#  Default: False
-# c.ConfigurableHTTPProxy.debug = False
-
-## 
-#  See also: Proxy.extra_routes
-# c.ConfigurableHTTPProxy.extra_routes = {}
-
-## Proxy log level
-#  Choices: any of ['debug', 'info', 'warn', 'error'] (case-insensitive)
-#  Default: 'info'
-# c.ConfigurableHTTPProxy.log_level = 'info'
-
-## File in which to write the PID of the proxy process.
-#  Default: 'jupyterhub-proxy.pid'
-# c.ConfigurableHTTPProxy.pid_file = 'jupyterhub-proxy.pid'
-
-## Should the Hub start the proxy
-#  See also: Proxy.should_start
-# c.ConfigurableHTTPProxy.should_start = True
-
-#------------------------------------------------------------------------------
-# NullAuthenticator(Authenticator) configuration
-#------------------------------------------------------------------------------
-## Null Authenticator for JupyterHub
-#  
-#      For cases where authentication should be disabled,
-#      e.g. only allowing access via API tokens.
-#  
-#      .. versionadded:: 2.0
-
-## 
-#  See also: Authenticator.admin_users
-# c.NullAuthenticator.admin_users = set()
-
-## 
-#  See also: Authenticator.allow_all
-# c.NullAuthenticator.allow_all = False
-
-## 
-#  See also: Authenticator.allow_existing_users
-# c.NullAuthenticator.allow_existing_users = False
-
-## 
-#  See also: Authenticator.allowed_users
-# c.NullAuthenticator.allowed_users = set()
-
-## Is there any allow config?
-#  See also: Authenticator.any_allow_config
-# c.NullAuthenticator.any_allow_config = False
-
-## The max age (in seconds) of authentication info
-#  See also: Authenticator.auth_refresh_age
-# c.NullAuthenticator.auth_refresh_age = 300
-
-## 
-#  See also: Authenticator.auto_login_oauth2_authorize
-# c.NullAuthenticator.auto_login_oauth2_authorize = False
-
-## 
-#  See also: Authenticator.blocked_users
-# c.NullAuthenticator.blocked_users = set()
-
-## Delete any users from the database that do not pass validation
-#  See also: Authenticator.delete_invalid_users
-# c.NullAuthenticator.delete_invalid_users = False
-
-## Enable persisting auth_state (if available).
-#  See also: Authenticator.enable_auth_state
-# c.NullAuthenticator.enable_auth_state = False
-
-## Let authenticator manage user groups
-#  See also: Authenticator.manage_groups
-# c.NullAuthenticator.manage_groups = False
-
-## Let authenticator manage roles
-#  See also: Authenticator.manage_roles
-# c.NullAuthenticator.manage_roles = False
-
-## 
-#  See also: Authenticator.otp_prompt
-# c.NullAuthenticator.otp_prompt = 'OTP:'
-
-## 
-#  See also: Authenticator.post_auth_hook
-# c.NullAuthenticator.post_auth_hook = None
-
-## Force refresh of auth prior to spawn.
-#  See also: Authenticator.refresh_pre_spawn
-# c.NullAuthenticator.refresh_pre_spawn = False
-
-## 
-#  See also: Authenticator.request_otp
-# c.NullAuthenticator.request_otp = False
-
-## Reset managed roles to result of `load_managed_roles()` on startup.
-#  See also: Authenticator.reset_managed_roles_on_startup
-# c.NullAuthenticator.reset_managed_roles_on_startup = False
-
-## Dictionary mapping authenticator usernames to JupyterHub users.
-#  See also: Authenticator.username_map
-# c.NullAuthenticator.username_map = {}
-
-## 
-#  See also: Authenticator.username_pattern
-# c.NullAuthenticator.username_pattern = ''
-
-## Deprecated, use `Authenticator.allowed_users`
-#  See also: Authenticator.whitelist
-# c.NullAuthenticator.whitelist = set()
-
-#------------------------------------------------------------------------------
-# Spawner(LoggingConfigurable) configuration
-#------------------------------------------------------------------------------
-## Base class for spawning single-user notebook servers.
-#  
-#      Subclass this, and override the following methods:
-#  
-#      - load_state
-#      - get_state
-#      - start
-#      - stop
-#      - poll
-#  
-#      As JupyterHub supports multiple users, an instance of the Spawner subclass
-#      is created for each user. If there are 20 JupyterHub users, there will be 20
-#      instances of the subclass.
-
-## Extra arguments to be passed to the single-user server.
-#  
-#  Some spawners allow shell-style expansion here, allowing you to use
-#  environment variables here. Most, including the default, do not. Consult the
-#  documentation for your spawner to verify!
-#  Default: []
-# c.Spawner.args = []
-
-## An optional hook function that you can implement to pass `auth_state` to the
-#  spawner after it has been initialized but before it starts. The `auth_state`
-#  dictionary may be set by the `.authenticate()` method of the authenticator.
-#  This hook enables you to pass some or all of that information to your spawner.
-#  
-#  Example::
-#  
-#      def userdata_hook(spawner, auth_state):
-#          spawner.userdata = auth_state["userdata"]
-#  
-#      c.Spawner.auth_state_hook = userdata_hook
-#  Default: None
-# c.Spawner.auth_state_hook = None
-
-## The command used for starting the single-user server.
-#  
-#  Provide either a string or a list containing the path to the startup script
-#  command. Extra arguments, other than this path, should be provided via `args`.
-#  
-#  This is usually set if you want to start the single-user server in a different
-#  python environment (with virtualenv/conda) than JupyterHub itself.
-#  
-#  Some spawners allow shell-style expansion here, allowing you to use
-#  environment variables. Most, including the default, do not. Consult the
-#  documentation for your spawner to verify!
-#  Default: ['jupyterhub-singleuser']
-# c.Spawner.cmd = ['jupyterhub-singleuser']
-
-## Maximum number of consecutive failures to allow before shutting down
-#  JupyterHub.
-#  
-#  This helps JupyterHub recover from a certain class of problem preventing
-#  launch in contexts where the Hub is automatically restarted (e.g. systemd,
-#  docker, kubernetes).
-#  
-#  A limit of 0 means no limit and consecutive failures will not be tracked.
-#  Default: 0
-# c.Spawner.consecutive_failure_limit = 0
-
-## Minimum number of cpu-cores a single-user notebook server is guaranteed to
-#  have available.
-#  
-#  If this value is set to 0.5, allows use of 50% of one CPU. If this value is
-#  set to 2, allows use of up to 2 CPUs.
-#  
-#  **This is a configuration setting. Your spawner must implement support for the
-#  limit to work.** The default spawner, `LocalProcessSpawner`, does **not**
-#  implement this support. A custom spawner **must** add support for this setting
-#  for it to be enforced.
-#  Default: None
-# c.Spawner.cpu_guarantee = None
-
-## Maximum number of cpu-cores a single-user notebook server is allowed to use.
-#  
-#  If this value is set to 0.5, allows use of 50% of one CPU. If this value is
-#  set to 2, allows use of up to 2 CPUs.
-#  
-#  The single-user notebook server will never be scheduled by the kernel to use
-#  more cpu-cores than this. There is no guarantee that it can access this many
-#  cpu-cores.
-#  
-#  **This is a configuration setting. Your spawner must implement support for the
-#  limit to work.** The default spawner, `LocalProcessSpawner`, does **not**
-#  implement this support. A custom spawner **must** add support for this setting
-#  for it to be enforced.
-#  Default: None
-# c.Spawner.cpu_limit = None
-
-## Enable debug-logging of the single-user server
-#  Default: False
-# c.Spawner.debug = False
-
-## The URL the single-user server should start in.
-#  
-#  `{username}` will be expanded to the user's username
-#  
-#  Example uses:
-#  
-#  - You can set `notebook_dir` to `/` and `default_url` to `/tree/home/{username}` to allow people to
-#    navigate the whole filesystem from their notebook server, but still start in their home directory.
-#  - Start with `/notebooks` instead of `/tree` if `default_url` points to a notebook instead of a directory.
-#  - You can set this to `/lab` to have JupyterLab start by default, rather than Jupyter Notebook.
-#  Default: ''
-# c.Spawner.default_url = ''
-
-## Disable per-user configuration of single-user servers.
-#  
-#  When starting the user's single-user server, any config file found in the
-#  user's $HOME directory will be ignored.
-#  
-#  Note: a user could circumvent this if the user modifies their Python
-#  environment, such as when they have their own conda environments / virtualenvs
-#  / containers.
-#  Default: False
-# c.Spawner.disable_user_config = False
-
-## List of environment variables for the single-user server to inherit from the
-#  JupyterHub process.
-#  
-#  This list is used to ensure that sensitive information in the JupyterHub
-#  process's environment (such as `CONFIGPROXY_AUTH_TOKEN`) is not passed to the
-#  single-user server's process.
-#  Default: ['JUPYTERHUB_SINGLEUSER_APP']
-# c.Spawner.env_keep = ['JUPYTERHUB_SINGLEUSER_APP']
-
-## Extra environment variables to set for the single-user server's process.
-#  
-#  Environment variables that end up in the single-user server's process come from 3 sources:
-#    - This `environment` configurable
-#    - The JupyterHub process' environment variables that are listed in `env_keep`
-#    - Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN)
-#  
-#  The `environment` configurable should be set by JupyterHub administrators to
-#  add installation specific environment variables. It is a dict where the key is
-#  the name of the environment variable, and the value can be a string or a
-#  callable. If it is a callable, it will be called with one parameter (the
-#  spawner instance), and should return a string fairly quickly (no blocking
-#  operations please!).
-#  
-#  Note that the spawner class' interface is not guaranteed to be exactly same
-#  across upgrades, so if you are using the callable take care to verify it
-#  continues to work after upgrades!
-#  
-#  .. versionchanged:: 1.2
-#      environment from this configuration has highest priority,
-#      allowing override of 'default' env variables,
-#      such as JUPYTERHUB_API_URL.
-#  Default: {}
-# c.Spawner.environment = {}
-
-## Override specific traitlets based on group membership of the user.
-#  
-#  This can be a dict, or a callable that returns a dict. The keys of the dict
-#  are *only* used for lexicographical sorting, to guarantee consistent ordering
-#  of the overrides. If it is a callable, it may be async, and will be passed one
-#  parameter - the spawner instance. It should return a dictionary.
-#  
-#  The values of the dict are dicts with the following keys:
-#  
-#  - `"groups"` - If the user belongs to *any* of these groups, these overrides are
-#    applied to their server before spawning.
-#  - `"spawner_override"` - a dictionary with overrides to apply to the Spawner
-#    settings. Each value can be either the final value to change or a callable that
-#    take the `Spawner` instance as parameter and returns the final value.
-#    If the traitlet being overriden is a *dictionary*, the dictionary
-#    will be *recursively updated*, rather than overriden. If you want to
-#    remove a key, set its value to `None`.
-#  
-#  Example:
-#  
-#      The following example config will:
-#  
-#      1. Add the environment variable "AM_I_GROUP_ALPHA" to everyone in the "group-alpha" group
-#      2. Add the environment variable "AM_I_GROUP_BETA" to everyone in the "group-beta" group.
-#         If a user is part of both "group-beta" and "group-alpha", they will get *both* these env
-#         vars, due to the dictionary merging functionality.
-#      3. Add a higher memory limit for everyone in the "group-beta" group.
-#  
-#      ::
-#  
-#          c.Spawner.group_overrides = {
-#              "01-group-alpha-env-add": {
-#                  "groups": ["group-alpha"],
-#                  "spawner_override": {"environment": {"AM_I_GROUP_ALPHA": "yes"}},
-#              },
-#              "02-group-beta-env-add": {
-#                  "groups": ["group-beta"],
-#                  "spawner_override": {"environment": {"AM_I_GROUP_BETA": "yes"}},
-#              },
-#              "03-group-beta-mem-limit": {
-#                  "groups": ["group-beta"],
-#                  "spawner_override": {"mem_limit": "2G"}
-#              }
-#          }
-#  Default: traitlets.Undefined
-# c.Spawner.group_overrides = traitlets.Undefined
-
-## Timeout (in seconds) before giving up on a spawned HTTP server
-#  
-#  Once a server has successfully been spawned, this is the amount of time we
-#  wait before assuming that the server is unable to accept connections.
-#  Default: 30
-# c.Spawner.http_timeout = 30
-
-## The URL the single-user server should connect to the Hub.
-#  
-#  If the Hub URL set in your JupyterHub config is not reachable from spawned
-#  notebooks, you can set differnt URL by this config.
-#  
-#  Is None if you don't need to change the URL.
-#  Default: None
-# c.Spawner.hub_connect_url = None
-
-## The IP address (or hostname) the single-user server should listen on.
-#  
-#  Usually either '127.0.0.1' (default) or '0.0.0.0'.
-#  
-#  The JupyterHub proxy implementation should be able to send packets to this
-#  interface.
-#  
-#  Subclasses which launch remotely or in containers should override the default
-#  to '0.0.0.0'.
-#  
-#  .. versionchanged:: 2.0
-#      Default changed to '127.0.0.1', from ''.
-#      In most cases, this does not result in a change in behavior,
-#      as '' was interpreted as 'unspecified',
-#      which used the subprocesses' own default, itself usually '127.0.0.1'.
-#  Default: '127.0.0.1'
-# c.Spawner.ip = '127.0.0.1'
-
-## Minimum number of bytes a single-user notebook server is guaranteed to have
-#  available.
-#  
-#  Allows the following suffixes:
-#    - K -> Kilobytes
-#    - M -> Megabytes
-#    - G -> Gigabytes
-#    - T -> Terabytes
-#  
-#  **This is a configuration setting. Your spawner must implement support for the
-#  limit to work.** The default spawner, `LocalProcessSpawner`, does **not**
-#  implement this support. A custom spawner **must** add support for this setting
-#  for it to be enforced.
-#  Default: None
-# c.Spawner.mem_guarantee = None
-
-## Maximum number of bytes a single-user notebook server is allowed to use.
-#  
-#  Allows the following suffixes:
-#    - K -> Kilobytes
-#    - M -> Megabytes
-#    - G -> Gigabytes
-#    - T -> Terabytes
-#  
-#  If the single user server tries to allocate more memory than this, it will
-#  fail. There is no guarantee that the single-user notebook server will be able
-#  to allocate this much memory - only that it can not allocate more than this.
-#  
-#  **This is a configuration setting. Your spawner must implement support for the
-#  limit to work.** The default spawner, `LocalProcessSpawner`, does **not**
-#  implement this support. A custom spawner **must** add support for this setting
-#  for it to be enforced.
-#  Default: None
-# c.Spawner.mem_limit = None
-
-## Path to the notebook directory for the single-user server.
-#  
-#  The user sees a file listing of this directory when the notebook interface is
-#  started. The current interface does not easily allow browsing beyond the
-#  subdirectories in this directory's tree.
-#  
-#  `~` will be expanded to the home directory of the user, and {username} will be
-#  replaced with the name of the user.
-#  
-#  Note that this does *not* prevent users from accessing files outside of this
-#  path! They can do so with many other means.
-#  Default: ''
-c.Spawner.notebook_dir = ''
-
-## Allowed scopes for oauth tokens issued by this server's oauth client.
-#  
-#          This sets the maximum and default scopes
-#          assigned to oauth tokens issued by a single-user server's
-#          oauth client (i.e. tokens stored in browsers after authenticating with the server),
-#          defining what actions the server can take on behalf of logged-in users.
-#  
-#          Default is an empty list, meaning minimal permissions to identify users,
-#          no actions can be taken on their behalf.
-#  
-#          If callable, will be called with the Spawner as a single argument.
-#          Callables may be async.
-#  Default: traitlets.Undefined
-# c.Spawner.oauth_client_allowed_scopes = traitlets.Undefined
-
-## Allowed roles for oauth tokens.
-#  
-#          Deprecated in 3.0: use oauth_client_allowed_scopes
-#  
-#          This sets the maximum and default roles
-#          assigned to oauth tokens issued by a single-user server's
-#          oauth client (i.e. tokens stored in browsers after authenticating with the server),
-#          defining what actions the server can take on behalf of logged-in users.
-#  
-#          Default is an empty list, meaning minimal permissions to identify users,
-#          no actions can be taken on their behalf.
-#  Default: traitlets.Undefined
-# c.Spawner.oauth_roles = traitlets.Undefined
-
-## An HTML form for options a user can specify on launching their server.
-#  
-#  The surrounding `<form>` element and the submit button are already provided.
-#  
-#  For example:
-#  
-#  .. code:: html
-#  
-#      Set your key:
-#      <input name="key" val="default_key"></input>
-#      <br>
-#      Choose a letter:
-#      <select name="letter" multiple="true">
-#        <option value="A">The letter A</option>
-#        <option value="B">The letter B</option>
-#      </select>
-#  
-#  The data from this form submission will be passed on to your spawner in
-#  `self.user_options`
-#  
-#  Instead of a form snippet string, this could also be a callable that takes as
-#  one parameter the current spawner instance and returns a string. The callable
-#  will be called asynchronously if it returns a future, rather than a str. Note
-#  that the interface of the spawner class is not deemed stable across versions,
-#  so using this functionality might cause your JupyterHub upgrades to break.
-#  Default: traitlets.Undefined
-# c.Spawner.options_form = traitlets.Undefined
-
-## Interpret HTTP form data
-#  
-#  Form data will always arrive as a dict of lists of strings. Override this
-#  function to understand single-values, numbers, etc.
-#  
-#  This should coerce form data into the structure expected by self.user_options,
-#  which must be a dict, and should be JSON-serializeable, though it can contain
-#  bytes in addition to standard JSON data types.
-#  
-#  This method should not have any side effects. Any handling of `user_options`
-#  should be done in `.start()` to ensure consistent behavior across servers
-#  spawned via the API and form submission page.
-#  
-#  Instances will receive this data on self.user_options, after passing through
-#  this function, prior to `Spawner.start`.
-#  
-#  .. versionchanged:: 1.0
-#      user_options are persisted in the JupyterHub database to be reused
-#      on subsequent spawns if no options are given.
-#      user_options is serialized to JSON as part of this persistence
-#      (with additional support for bytes in case of uploaded file data),
-#      and any non-bytes non-jsonable values will be replaced with None
-#      if the user_options are re-used.
-#  Default: traitlets.Undefined
-# c.Spawner.options_from_form = traitlets.Undefined
-
-## Interval (in seconds) on which to poll the spawner for single-user server's
-#  status.
-#  
-#  At every poll interval, each spawner's `.poll` method is called, which checks
-#  if the single-user server is still running. If it isn't running, then
-#  JupyterHub modifies its own state accordingly and removes appropriate routes
-#  from the configurable proxy.
-#  Default: 30
-# c.Spawner.poll_interval = 30
-
-## Jitter fraction for poll_interval.
-#  
-#  Avoids alignment of poll calls for many Spawners, e.g. when restarting
-#  JupyterHub, which restarts all polls for running Spawners.
-#  
-#  `poll_jitter=0` means no jitter, 0.1 means 10%, etc.
-#  Default: 0.1
-# c.Spawner.poll_jitter = 0.1
-
-## The port for single-user servers to listen on.
-#  
-#  Defaults to `0`, which uses a randomly allocated port number each time.
-#  
-#  If set to a non-zero value, all Spawners will use the same port, which only
-#  makes sense if each server is on a different address, e.g. in containers.
-#  
-#  New in version 0.7.
-#  Default: 0
-# c.Spawner.port = 0
-
-## An optional hook function that you can implement to do work after the spawner
-#  stops.
-#  
-#  This can be set independent of any concrete spawner implementation.
-#  Default: None
-# c.Spawner.post_stop_hook = None
-
-## An optional hook function that you can implement to do some bootstrapping work
-#  before the spawner starts. For example, create a directory for your user or
-#  load initial content.
-#  
-#  This can be set independent of any concrete spawner implementation.
-#  
-#  This maybe a coroutine.
-#  
-#  Example::
-#  
-#      def my_hook(spawner):
-#          username = spawner.user.name
-#          spawner.environment["GREETING"] = f"Hello {username}"
-#  
-#      c.Spawner.pre_spawn_hook = my_hook
-#  Default: None
-# c.Spawner.pre_spawn_hook = None
-
-## An optional hook function that you can implement to modify the ready event,
-#  which will be shown to the user on the spawn progress page when their server
-#  is ready.
-#  
-#  This can be set independent of any concrete spawner implementation.
-#  
-#  This maybe a coroutine.
-#  
-#  Example::
-#  
-#      async def my_ready_hook(spawner, ready_event):
-#          ready_event["html_message"] = f"Server {spawner.name} is ready for {spawner.user.name}"
-#          return ready_event
-#  
-#      c.Spawner.progress_ready_hook = my_ready_hook
-#  Default: None
-# c.Spawner.progress_ready_hook = None
-
-## The list of scopes to request for $JUPYTERHUB_API_TOKEN
-#  
-#          If not specified, the scopes in the `server` role will be used
-#          (unchanged from pre-4.0).
-#  
-#          If callable, will be called with the Spawner instance as its sole argument
-#          (JupyterHub user available as spawner.user).
-#  
-#          JUPYTERHUB_API_TOKEN will be assigned the _subset_ of these scopes
-#          that are held by the user (as in oauth_client_allowed_scopes).
-#  
-#          .. versionadded:: 4.0
-#  Default: traitlets.Undefined
-# c.Spawner.server_token_scopes = traitlets.Undefined
-
-## List of SSL alt names
-#  
-#          May be set in config if all spawners should have the same value(s),
-#          or set at runtime by Spawner that know their names.
-#  Default: []
-# c.Spawner.ssl_alt_names = []
-
-## Whether to include `DNS:localhost`, `IP:127.0.0.1` in alt names
-#  Default: True
-# c.Spawner.ssl_alt_names_include_local = True
-
-## Timeout (in seconds) before giving up on starting of single-user server.
-#  
-#  This is the timeout for start to return, not the timeout for the server to
-#  respond. Callers of spawner.start will assume that startup has failed if it
-#  takes longer than this. start should return when the server process is started
-#  and its location is known.
-#  Default: 60
-# c.Spawner.start_timeout = 60
-
-#------------------------------------------------------------------------------
-# LocalProcessSpawner(Spawner) configuration
-#------------------------------------------------------------------------------
-## A Spawner that uses `subprocess.Popen` to start single-user servers as local
-#  processes.
-#  
-#  Requires local UNIX users matching the authenticated users to exist. Does not
-#  work on Windows.
-#  
-#  This is the default spawner for JupyterHub.
-#  
-#  Note: This spawner does not implement CPU / memory guarantees and limits.
-
-## 
-#  See also: Spawner.args
-# c.LocalProcessSpawner.args = []
-
-## 
-#  See also: Spawner.auth_state_hook
-# c.LocalProcessSpawner.auth_state_hook = None
-
-## 
-#  See also: Spawner.cmd
-# c.LocalProcessSpawner.cmd = ['jupyterhub-singleuser']
-
-## 
-#  See also: Spawner.consecutive_failure_limit
-# c.LocalProcessSpawner.consecutive_failure_limit = 0
-
-## 
-#  See also: Spawner.cpu_guarantee
-# c.LocalProcessSpawner.cpu_guarantee = None
-
-## 
-#  See also: Spawner.cpu_limit
-# c.LocalProcessSpawner.cpu_limit = None
-
-## Enable debug-logging of the single-user server
-#  See also: Spawner.debug
-# c.LocalProcessSpawner.debug = False
-
-## 
-#  See also: Spawner.default_url
-# c.LocalProcessSpawner.default_url = ''
-
-## 
-#  See also: Spawner.disable_user_config
-# c.LocalProcessSpawner.disable_user_config = False
-
-## 
-#  See also: Spawner.env_keep
-# c.LocalProcessSpawner.env_keep = ['JUPYTERHUB_SINGLEUSER_APP']
-
-## 
-#  See also: Spawner.environment
-# c.LocalProcessSpawner.environment = {}
-
-## 
-#  See also: Spawner.group_overrides
-# c.LocalProcessSpawner.group_overrides = traitlets.Undefined
-
-## 
-#  See also: Spawner.http_timeout
-# c.LocalProcessSpawner.http_timeout = 30
-
-## 
-#  See also: Spawner.hub_connect_url
-# c.LocalProcessSpawner.hub_connect_url = None
-
-## Seconds to wait for single-user server process to halt after SIGINT.
-#  
-#  If the process has not exited cleanly after this many seconds, a SIGTERM is
-#  sent.
-#  Default: 10
-# c.LocalProcessSpawner.interrupt_timeout = 10
-
-## 
-#  See also: Spawner.ip
-# c.LocalProcessSpawner.ip = '127.0.0.1'
-
-## Seconds to wait for process to halt after SIGKILL before giving up.
-#  
-#  If the process does not exit cleanly after this many seconds of SIGKILL, it
-#  becomes a zombie process. The hub process will log a warning and then give up.
-#  Default: 5
-# c.LocalProcessSpawner.kill_timeout = 5
-
-## 
-#  See also: Spawner.mem_guarantee
-# c.LocalProcessSpawner.mem_guarantee = None
-
-## 
-#  See also: Spawner.mem_limit
-# c.LocalProcessSpawner.mem_limit = None
-
-## 
-#  See also: Spawner.notebook_dir
-# c.LocalProcessSpawner.notebook_dir = ''
-
-## Allowed scopes for oauth tokens issued by this server's oauth client.
-#  See also: Spawner.oauth_client_allowed_scopes
-# c.LocalProcessSpawner.oauth_client_allowed_scopes = traitlets.Undefined
-
-## Allowed roles for oauth tokens.
-#  See also: Spawner.oauth_roles
-# c.LocalProcessSpawner.oauth_roles = traitlets.Undefined
-
-## 
-#  See also: Spawner.options_form
-# c.LocalProcessSpawner.options_form = traitlets.Undefined
-
-## 
-#  See also: Spawner.options_from_form
-# c.LocalProcessSpawner.options_from_form = traitlets.Undefined
-
-## 
-#  See also: Spawner.poll_interval
-# c.LocalProcessSpawner.poll_interval = 30
-
-## 
-#  See also: Spawner.poll_jitter
-# c.LocalProcessSpawner.poll_jitter = 0.1
 
-## Extra keyword arguments to pass to Popen
+#------------------------------------------------------------------------------
+# Proxy(LoggingConfigurable) configuration
+#------------------------------------------------------------------------------
+## Base class for configurable proxies that JupyterHub can use.
 #  
-#          when spawning single-user servers.
+#      A proxy implementation should subclass this and must define the following
+#  methods:
 #  
-#          For example::
+#      - :meth:`.get_all_routes` return a dictionary of all JupyterHub-related routes
+#      - :meth:`.add_route` adds a route
+#      - :meth:`.delete_route` deletes a route
 #  
-#              popen_kwargs = dict(shell=True)
-#  Default: {}
-# c.LocalProcessSpawner.popen_kwargs = {}
-
-## 
-#  See also: Spawner.port
-# c.LocalProcessSpawner.port = 0
-
-## 
-#  See also: Spawner.post_stop_hook
-# c.LocalProcessSpawner.post_stop_hook = None
-
-## 
-#  See also: Spawner.pre_spawn_hook
-# c.LocalProcessSpawner.pre_spawn_hook = None
-
-## 
-#  See also: Spawner.progress_ready_hook
-# c.LocalProcessSpawner.progress_ready_hook = None
-
-## The list of scopes to request for $JUPYTERHUB_API_TOKEN
-#  See also: Spawner.server_token_scopes
-# c.LocalProcessSpawner.server_token_scopes = traitlets.Undefined
-
-## Specify a shell command to launch.
+#      In addition to these, the following method(s) may need to be implemented:
 #  
-#          The single-user command will be appended to this list,
-#          so it sould end with `-c` (for bash) or equivalent.
+#      - :meth:`.start` start the proxy, if it should be launched by the Hub
+#        instead of externally managed.
+#        If the proxy is externally managed, it should set :attr:`should_start` to False.
+#      - :meth:`.stop` stop the proxy. Only used if :meth:`.start` is also used.
 #  
-#          For example::
+#      And the following method(s) are optional, but can be provided:
 #  
-#              c.LocalProcessSpawner.shell_cmd = ['bash', '-l', '-c']
+#      - :meth:`.get_route` gets a single route.
+#        There is a default implementation that extracts data from :meth:`.get_all_routes`,
+#        but implementations may choose to provide a more efficient implementation
+#        of fetching a single route.
+
+## Additional routes to be maintained in the proxy.
 #  
-#          to launch with a bash login shell, which would set up the user's own
-#  complete environment.
+#  A dictionary with a route specification as key, and a URL as target. The hub
+#  will ensure this route is present in the proxy.
 #  
-#          .. warning::
+#  If the hub is running in host based mode (with JupyterHub.subdomain_host set),
+#  the routespec *must* have a domain component (example.com/my-url/). If the hub
+#  is not running in host based mode, the routespec *must not* have a domain
+#  component (/my-url/).
 #  
-#              Using shell_cmd gives users control over PATH, etc.,
-#              which could change what the jupyterhub-singleuser launch command does.
-#              Only use this for trusted users.
-#  Default: []
-# c.LocalProcessSpawner.shell_cmd = []
-
-## List of SSL alt names
-#  See also: Spawner.ssl_alt_names
-# c.LocalProcessSpawner.ssl_alt_names = []
-
-## Whether to include `DNS:localhost`, `IP:127.0.0.1` in alt names
-#  See also: Spawner.ssl_alt_names_include_local
-# c.LocalProcessSpawner.ssl_alt_names_include_local = True
-
-## 
-#  See also: Spawner.start_timeout
-# c.LocalProcessSpawner.start_timeout = 60
+#  Helpful when the hub is running in API-only mode.
+#  Default: {}
+# c.Proxy.extra_routes = {}
 
-## Seconds to wait for single-user server process to halt after SIGTERM.
+## Should the Hub start the proxy
 #  
-#  If the process does not exit cleanly after this many seconds of SIGTERM, a
-#  SIGKILL is sent.
-#  Default: 5
-# c.LocalProcessSpawner.term_timeout = 5
+#          If True, the Hub will start the proxy and stop it.
+#          Set to False if the proxy is managed externally,
+#          such as by systemd, docker, or another service manager.
+#  Default: True
+# c.Proxy.should_start = True
 
 #------------------------------------------------------------------------------
-# LocalAuthenticator(Authenticator) configuration
+# ConfigurableHTTPProxy(Proxy) configuration
 #------------------------------------------------------------------------------
-## Base class for Authenticators that work with local Linux/UNIX users
-#  
-#      Checks for local users, and can attempt to create them if they exist.
-
-## The command to use for creating users as a list of strings
-#  
-#  For each element in the list, the string USERNAME will be replaced with the
-#  user's username. The username will also be appended as the final argument.
-#  
-#  For Linux, the default value is:
-#  
-#      ['adduser', '-q', '--gecos', '""', '--disabled-password']
-#  
-#  To specify a custom home directory, set this to:
-#  
-#      ['adduser', '-q', '--gecos', '""', '--home', '/customhome/USERNAME', '--
-#  disabled-password']
+## Proxy implementation for the default configurable-http-proxy.
 #  
-#  This will run the command:
+#      This is the default proxy implementation
+#      for running the nodejs proxy `configurable-http-proxy`.
 #  
-#      adduser -q --gecos "" --home /customhome/river --disabled-password river
+#      If the proxy should not be run as a subprocess of the Hub,
+#      (e.g. in a separate container),
+#      set::
 #  
-#  when the user 'river' is created.
-#  Default: []
-# c.LocalAuthenticator.add_user_cmd = []
-
-## 
-#  See also: Authenticator.admin_users
-# c.LocalAuthenticator.admin_users = set()
-
-## 
-#  See also: Authenticator.allow_all
-# c.LocalAuthenticator.allow_all = False
+#          c.ConfigurableHTTPProxy.should_start = False
 
-## 
-#  See also: Authenticator.allow_existing_users
-# c.LocalAuthenticator.allow_existing_users = False
+## The ip (or hostname) of the proxy's API endpoint
+#  Default: ''
+# c.ConfigurableHTTPProxy.api_url = ''
 
-## Allow login from all users in these UNIX groups.
+## The Proxy auth token
 #  
-#  .. versionchanged:: 5.0
-#      `allowed_groups` may be specified together with allowed_users,
-#      to grant access by group OR name.
-#  Default: set()
-# c.LocalAuthenticator.allowed_groups = set()
-
-## 
-#  See also: Authenticator.allowed_users
-# c.LocalAuthenticator.allowed_users = set()
-
-## Is there any allow config?
-#  See also: Authenticator.any_allow_config
-# c.LocalAuthenticator.any_allow_config = False
-
-## The max age (in seconds) of authentication info
-#  See also: Authenticator.auth_refresh_age
-# c.LocalAuthenticator.auth_refresh_age = 300
-
-## Automatically begin the login process
-#  See also: Authenticator.auto_login
-# c.LocalAuthenticator.auto_login = False
+#          Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
+#  Default: ''
+# c.ConfigurableHTTPProxy.auth_token = ''
 
-## 
-#  See also: Authenticator.auto_login_oauth2_authorize
-# c.LocalAuthenticator.auto_login_oauth2_authorize = False
+## Interval (in seconds) at which to check if the proxy is running.
+#  Default: 5
+# c.ConfigurableHTTPProxy.check_running_interval = 5
 
-## 
-#  See also: Authenticator.blocked_users
-# c.LocalAuthenticator.blocked_users = set()
+## The command to start the proxy
+#  Default: ['configurable-http-proxy']
+# c.ConfigurableHTTPProxy.command = ['configurable-http-proxy']
 
-## If set to True, will attempt to create local system users if they do not exist
-#  already.
+## The number of requests allowed to be concurrently outstanding to the proxy
 #  
-#  Supports Linux and BSD variants only.
-#  Default: False
-# c.LocalAuthenticator.create_system_users = False
-
-## Delete any users from the database that do not pass validation
-#  See also: Authenticator.delete_invalid_users
-# c.LocalAuthenticator.delete_invalid_users = False
-
-## Enable persisting auth_state (if available).
-#  See also: Authenticator.enable_auth_state
-# c.LocalAuthenticator.enable_auth_state = False
-
-## DEPRECATED: use allowed_groups
-#  Default: set()
-# c.LocalAuthenticator.group_whitelist = set()
-
-## Let authenticator manage user groups
-#  See also: Authenticator.manage_groups
-# c.LocalAuthenticator.manage_groups = False
-
-## Let authenticator manage roles
-#  See also: Authenticator.manage_roles
-# c.LocalAuthenticator.manage_roles = False
-
-## 
-#  See also: Authenticator.otp_prompt
-# c.LocalAuthenticator.otp_prompt = 'OTP:'
-
-## 
-#  See also: Authenticator.post_auth_hook
-# c.LocalAuthenticator.post_auth_hook = None
+#  Limiting this number avoids potential timeout errors by sending too many
+#  requests to update the proxy at once
+#  Default: 10
+# c.ConfigurableHTTPProxy.concurrency = 10
 
-## Force refresh of auth prior to spawn.
-#  See also: Authenticator.refresh_pre_spawn
-# c.LocalAuthenticator.refresh_pre_spawn = False
+## Add debug-level logging to the Proxy.
+#  Default: False
+# c.ConfigurableHTTPProxy.debug = False
 
 ## 
-#  See also: Authenticator.request_otp
-# c.LocalAuthenticator.request_otp = False
-
-## Reset managed roles to result of `load_managed_roles()` on startup.
-#  See also: Authenticator.reset_managed_roles_on_startup
-# c.LocalAuthenticator.reset_managed_roles_on_startup = False
-
-## Dictionary of uids to use at user creation time. This helps ensure that users
-#  created from the database get the same uid each time they are created in
-#  temporary deployments or containers.
-#  Default: {}
-# c.LocalAuthenticator.uids = {}
+#  See also: Proxy.extra_routes
+# c.ConfigurableHTTPProxy.extra_routes = {}
 
-## Dictionary mapping authenticator usernames to JupyterHub users.
-#  See also: Authenticator.username_map
-# c.LocalAuthenticator.username_map = {}
+## Proxy log level
+#  Choices: any of ['debug', 'info', 'warn', 'error'] (case-insensitive)
+#  Default: 'info'
+# c.ConfigurableHTTPProxy.log_level = 'info'
 
-## 
-#  See also: Authenticator.username_pattern
-# c.LocalAuthenticator.username_pattern = ''
+## File in which to write the PID of the proxy process.
+#  Default: 'jupyterhub-proxy.pid'
+# c.ConfigurableHTTPProxy.pid_file = 'jupyterhub-proxy.pid'
 
-## Deprecated, use `Authenticator.allowed_users`
-#  See also: Authenticator.whitelist
-# c.LocalAuthenticator.whitelist = set()
+## Should the Hub start the proxy
+#  See also: Proxy.should_start
+# c.ConfigurableHTTPProxy.should_start = Tru
 
 #------------------------------------------------------------------------------
 # PAMAuthenticator(LocalAuthenticator) configuration
@@ -2649,166 +1282,32 @@ c.Spawner.notebook_dir = ''
 #  See also: Authenticator.whitelist
 # c.PAMAuthenticator.whitelist = set()
 
-#------------------------------------------------------------------------------
-# SimpleLocalProcessSpawner(LocalProcessSpawner) configuration
-#------------------------------------------------------------------------------
-## A version of LocalProcessSpawner that doesn't require users to exist on the
-#  system beforehand.
-#  
-#  Only use this for testing.
-#  
-#  Note: DO NOT USE THIS FOR PRODUCTION USE CASES! It is very insecure, and
-#  provides absolutely no isolation between different users!
-
-## 
-#  See also: Spawner.args
-# c.SimpleLocalProcessSpawner.args = []
-
-## 
-#  See also: Spawner.auth_state_hook
-# c.SimpleLocalProcessSpawner.auth_state_hook = None
-
-## 
-#  See also: Spawner.cmd
-# c.SimpleLocalProcessSpawner.cmd = ['jupyterhub-singleuser']
-
-## 
-#  See also: Spawner.consecutive_failure_limit
-# c.SimpleLocalProcessSpawner.consecutive_failure_limit = 0
-
-## 
-#  See also: Spawner.cpu_guarantee
-# c.SimpleLocalProcessSpawner.cpu_guarantee = None
-
-## 
-#  See also: Spawner.cpu_limit
-# c.SimpleLocalProcessSpawner.cpu_limit = None
-
-## Enable debug-logging of the single-user server
-#  See also: Spawner.debug
-# c.SimpleLocalProcessSpawner.debug = False
-
-## 
-#  See also: Spawner.default_url
-# c.SimpleLocalProcessSpawner.default_url = ''
-
-## 
-#  See also: Spawner.disable_user_config
-# c.SimpleLocalProcessSpawner.disable_user_config = False
-
-## 
-#  See also: Spawner.env_keep
-# c.SimpleLocalProcessSpawner.env_keep = ['JUPYTERHUB_SINGLEUSER_APP']
-
-## 
-#  See also: Spawner.environment
-# c.SimpleLocalProcessSpawner.environment = {}
-
-## 
-#  See also: Spawner.group_overrides
-# c.SimpleLocalProcessSpawner.group_overrides = traitlets.Undefined
-
-## Template to expand to set the user home. {username} is expanded to the
-#  jupyterhub username.
-#  Default: '/tmp/{username}'
-# c.SimpleLocalProcessSpawner.home_dir_template = '/tmp/{username}'
-
-## 
-#  See also: Spawner.http_timeout
-# c.SimpleLocalProcessSpawner.http_timeout = 30
-
-## 
-#  See also: Spawner.hub_connect_url
-# c.SimpleLocalProcessSpawner.hub_connect_url = None
-
-## 
-#  See also: LocalProcessSpawner.interrupt_timeout
-# c.SimpleLocalProcessSpawner.interrupt_timeout = 10
-
-## 
-#  See also: Spawner.ip
-# c.SimpleLocalProcessSpawner.ip = '127.0.0.1'
-
-## 
-#  See also: LocalProcessSpawner.kill_timeout
-# c.SimpleLocalProcessSpawner.kill_timeout = 5
-
-## 
-#  See also: Spawner.mem_guarantee
-# c.SimpleLocalProcessSpawner.mem_guarantee = None
-
-## 
-#  See also: Spawner.mem_limit
-# c.SimpleLocalProcessSpawner.mem_limit = None
-
-## 
-#  See also: Spawner.notebook_dir
-# c.SimpleLocalProcessSpawner.notebook_dir = ''
-
-## Allowed scopes for oauth tokens issued by this server's oauth client.
-#  See also: Spawner.oauth_client_allowed_scopes
-# c.SimpleLocalProcessSpawner.oauth_client_allowed_scopes = traitlets.Undefined
-
-## Allowed roles for oauth tokens.
-#  See also: Spawner.oauth_roles
-# c.SimpleLocalProcessSpawner.oauth_roles = traitlets.Undefined
-
-## 
-#  See also: Spawner.options_form
-# c.SimpleLocalProcessSpawner.options_form = traitlets.Undefined
-
-## 
-#  See also: Spawner.options_from_form
-# c.SimpleLocalProcessSpawner.options_from_form = traitlets.Undefined
-
-## 
-#  See also: Spawner.poll_interval
-# c.SimpleLocalProcessSpawner.poll_interval = 30
-
-## 
-#  See also: Spawner.poll_jitter
-# c.SimpleLocalProcessSpawner.poll_jitter = 0.1
-
-## Extra keyword arguments to pass to Popen
-#  See also: LocalProcessSpawner.popen_kwargs
-# c.SimpleLocalProcessSpawner.popen_kwargs = {}
 
-## 
-#  See also: Spawner.port
-# c.SimpleLocalProcessSpawner.port = 0
-
-## 
-#  See also: Spawner.post_stop_hook
-# c.SimpleLocalProcessSpawner.post_stop_hook = None
-
-## 
-#  See also: Spawner.pre_spawn_hook
-# c.SimpleLocalProcessSpawner.pre_spawn_hook = None
+## DockerSpawner Config
 
-## 
-#  See also: Spawner.progress_ready_hook
-# c.SimpleLocalProcessSpawner.progress_ready_hook = None
+import os
 
-## The list of scopes to request for $JUPYTERHUB_API_TOKEN
-#  See also: Spawner.server_token_scopes
-# c.SimpleLocalProcessSpawner.server_token_scopes = traitlets.Undefined
+# Spawn containers from this image
+c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"]
 
-## Specify a shell command to launch.
-#  See also: LocalProcessSpawner.shell_cmd
-# c.SimpleLocalProcessSpawner.shell_cmd = []
+# Connect containers to this Docker network
+network_name = os.environ["DOCKER_NETWORK_NAME"]
+c.DockerSpawner.use_internal_ip = True
+c.DockerSpawner.network_name = network_name
 
-## List of SSL alt names
-#  See also: Spawner.ssl_alt_names
-# c.SimpleLocalProcessSpawner.ssl_alt_names = []
+# Explicitly set notebook directory because we'll be mounting a volume to it.
+# Most `jupyter/docker-stacks` *-notebook images run the Notebook server as
+# user `jovyan`, and set the notebook directory to `/home/jovyan/work`.
+# We follow the same convention.
+notebook_dir = os.environ.get("DOCKER_NOTEBOOK_DIR", "/home/jovyan/work")
+c.DockerSpawner.notebook_dir = notebook_dir
 
-## Whether to include `DNS:localhost`, `IP:127.0.0.1` in alt names
-#  See also: Spawner.ssl_alt_names_include_local
-# c.SimpleLocalProcessSpawner.ssl_alt_names_include_local = True
+# Mount the real user's Docker volume on the host to the notebook user's
+# notebook directory in the container
+c.DockerSpawner.volumes = {"jupyterhub-user-{username}": notebook_dir}
 
-## 
-#  See also: Spawner.start_timeout
-# c.SimpleLocalProcessSpawner.start_timeout = 60
+# Remove containers once they are stopped
+c.DockerSpawner.remove = True
 
-## 
-#  See also: LocalProcessSpawner.term_timeout
-# c.SimpleLocalProcessSpawner.term_timeout = 5
+# For debugging arguments passed to spawned containers
+c.DockerSpawner.debug = True