Repository: jupyterhub/jupyterhub

Issue:
jupyterhub-singleuser always falls back to port 80 in version > 4.x 
<!-- Thank you for contributing. These HTML comments will not render in the issue, but you can delete them once you've read them if you prefer! -->

### Bug description

After upgrading from Jupyterhub 3.x to 4.x starting  singleuser server with:  `jupyterhub-singleuser --port=XXX` does not work anymore. According to the logs it does set the port of the ServerApp to the desired port, however eventually it get reset/updated to port 80.


Logs from jupyterhub > 4.x (not working)
```
[I 2023-05-12 14:40:54.032 JupyterHubSingleUser] Starting jupyterhub single-user server extension version 4.0.0
[I 2023-05-12 14:40:54.032 JupyterHubSingleUser] Using default url from environment $JUPYTERHUB_DEFAULT_URL: /lab
[D 2023-05-12 14:40:54.033 ServerApp] Config changed: {'SingleUserNotebookApp': {'default_url': DeferredConfigString('/lab')}, 'ServerApp': {'port': 52257, 'log_level': 'DEBUG', 'ip': '0.0.0.0', 'open_browser': False, 'jpserver_extensions': <LazyConfigValue value={'jupyterhub': True, 'jupyter_server_fileid': True, 'jupyter_server_proxy': True, 'jupyter_server_terminals': True, 'jupyter_server_ydoc': True, 'jupyterlab': True, 'nbclassic': True, 'notebook_shim': True}>}, 'ExtensionApp': {'log_level': 'DEBUG'}, 'InlineBackend': {'figure_formats': {'svg', 'jpeg', 'png', 'pdf'}}, 'FileContentsManager': {'delete_to_trash': False}}


[D 2023-05-12 14:40:54.036 JupyterHubSingleUser] Config changed: {'ServerApp': {'identity_provider_class': <class 'jupyterhub.singleuser.extension.JupyterHubIdentityProvider'>, 'allow_remote_access': True, 'open_browser': False, 'trust_xheaders': True, 'quit_button': False, 'port_retries': 0, 'answer_yes': True, 'port': 80, 'ip': '0.0.0.0', 'base_url': '/user/uemit.seren/codesever2/', 'default_url': '/lab', 'keyfile': '', 'certfile': '', 'client_ca': '', 'log_level': 'DEBUG', 'jpserver_extensions': <LazyConfigValue value={'jupyterhub': True, 'jupyter_server_fileid': True, 'jupyter_server_proxy': True, 'jupyter_server_terminals': True, 'jupyter_server_ydoc': True, 'jupyterlab': True, 'nbclassic': True, 'notebook_shim': True}>}, 'FileContentsManager': {'delete_to_trash': False}, 'NotebookApp': {'extra_template_paths': <LazyConfigValue {'extend': ['/opt/conda/lib/python3.10/site-packages/jupyterhub/singleuser/templates']}>}, 'SingleUserNotebookApp': {'default_url': DeferredConfigString('/lab')}, 'ExtensionApp': {'log_level': 'DEBUG'}, 'InlineBackend': {'figure_formats': {'svg', 'jpeg', 'png', 'pdf'}}}
[D 2023-05-12 14:40:54.037 ServerApp] Config changed: {'SingleUserNotebookApp': {'default_url': DeferredConfigString('/lab')}, 'ServerApp': {'port': 80, 'log_level': 'DEBUG', 'ip': '0.0.0.0', 'open_browser': False, 'jpserver_extensions': <LazyConfigValue value={'jupyterhub': True, 'jupyter_server_fileid': True, 'jupyter_server_proxy': True, 'jupyter_server_terminals': True, 'jupyter_server_ydoc': True, 'jupyterlab': True, 'nbclassic': True, 'notebook_shim': True}>, 'identity_provider_class': <class 'jupyterhub.singleuser.extension.JupyterHubIdentityProvider'>, 'allow_remote_access': True, 'trust_xheaders': True, 'quit_button': False, 'port_retries': 0, 'answer_yes': True, 'base_url': '/user/uemit.seren/codesever2/', 'default_url': '/lab', 'keyfile': '', 'certfile': '', 'client_ca': ''}, 'ExtensionApp': {'log_level': 'DEBUG'}, 'InlineBackend': {'figure_formats': {'svg', 'jpeg', 'png', 'pdf'}}, 'FileContentsManager': {'delete_to_trash': False}}
[I 2023-05-12 14:40:54.038 ServerApp] jupyterhub | extension was successfully linked.
```

Logs from jupyterhub < 3.x (working)

```
[D 2023-05-12 14:18:35.219 LabApp] Config changed: {'NotebookApp': {}, 'ServerApp': {'ip': '0.0.0.0', 'open_browser': False, 'jpserver_extensions': <LazyConfigValue value={'jupyter_server_fileid': True, 'jupyter_server_proxy': True, 'jupyter_server_terminals': True, 'jupyter_server_ydoc': True, 'jupyterlab': True, 'nbclassic': True, 'notebook_shim': True}>}, 'FileContentsManager': {'delete_to_trash': False}, 'SingleUserNo
tebookApp': {'default_url': '/lab'}, 'SingleUserLabApp': {'port': 42752, 'log_level': 'DEBUG'}, 'ExtensionApp': {'log_level': 'DEBUG'}, 'InlineBackend': {'figure_formats': {'jpeg', 'svg', 'pdf', 'png'}}}
[W 2023-05-12 14:18:35.220 SingleUserLabApp configurable:200] Config option `open_browser` not recognized by `SingleUserLabApp`.  Did you mean `browser`?
[I 2023-05-12 14:18:35.220 SingleUserLabApp manager:344] jupyterlab | extension was successfully linked.
[W 2023-05-12 14:18:35.223 NotebookApp] 'ip' has moved from NotebookApp to ServerApp. This config will be passed to ServerApp. Be sure to update your config before our next release.
[W 2023-05-12 14:18:35.223 NotebookApp] 'ip' has moved from NotebookApp to ServerApp. This config will be passed to ServerApp. Be sure to update your config before our next release.
[D 2023-05-12 14:18:35.226 NotebookApp] Config changed: {'NotebookApp': {'open_browser': False}, 'ServerApp': {'ip': '0.0.0.0', 'open_browser': False, 'jpserver_extensions': <LazyConfigValue value={'jupyter_server_fileid': True, 'jupyter_server_proxy': True, 'jupyter_server_terminals': True, 'jupyter_server_ydoc': True, 'jupyterlab': True, 'nbclassic': True, 'notebook_shim': True}>}, 'InlineBackend': {'figure_formats': {'
jpeg', 'svg', 'pdf', 'png'}}, 'FileContentsManager': {'delete_to_trash': False}, 'SingleUserNotebookApp': {'default_url': '/lab'}, 'SingleUserLabApp': {'port': 42752, 'log_level': 'DEBUG'}, 'ExtensionApp': {'log_level': 'DEBUG'}}
[W 2023-05-12 14:18:35.226 SingleUserLabApp configurable:200] Config option `open_browser` not recognized by `SingleUserLabApp`.  Did you mean `browser`?


```
<!-- Use this section to clearly and concisely describe the bug. -->



<!-- Tell us what you thought would happen. -->

The jupyterhub-singleuser app should start at the custom port

<!-- Tell us what actually happens. -->

The jupyterhub-singleuser app always tries to start at port 80. 

```
</details>



Discussion:
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:


Resolving patch:
diff --git a/jupyterhub/singleuser/extension.py b/jupyterhub/singleuser/extension.py
--- a/jupyterhub/singleuser/extension.py
+++ b/jupyterhub/singleuser/extension.py
@@ -511,16 +511,25 @@ def load_config_file(self):
 
         # Jupyter Server default: config files have higher priority than extensions,
         # by:
-        # 1. load config files
+        # 1. load config files and CLI
         # 2. load extension config
         # 3. merge file config into extension config
 
         # we invert that by merging our extension config into server config before
         # they get merged the other way
         # this way config from this extension should always have highest priority
+
+        # but this also puts our config above _CLI_ options,
+        # and CLI should come before env,
+        # so merge that into _our_ config before loading
+        if self.serverapp.cli_config:
+            for cls_name, cls_config in self.serverapp.cli_config.items():
+                if cls_name in self.config:
+                    self.config[cls_name].merge(cls_config)
+
         self.serverapp.update_config(self.config)
 
-        # add our custom templates
+        # config below here has _lower_ priority than user config
         self.config.NotebookApp.extra_template_paths.append(SINGLEUSER_TEMPLATES_DIR)
 
     @default("default_url")


Test patch: