diff --git a/README.md b/README.md index ada720e289..49934d917a 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ system when the container exits, but any changes made to the `~/work` directory By default, [jupyter's root_dir](https://jupyter-server.readthedocs.io/en/latest/other/full-config.html) is `/home/jovyan`. So, new notebooks will be saved there, unless you change the directory in the file browser. -To change the default directory, you will need to specify `ServerApp.root_dir` by adding this line to previous command: `start-notebook.sh --ServerApp.root_dir=/home/jovyan/work`. +To change the default directory, you will need to specify `ServerApp.root_dir` by adding this line to previous command: `start-notebook.py --ServerApp.root_dir=/home/jovyan/work`. ``` ## Contributing diff --git a/docs/using/common.md b/docs/using/common.md index dab66fed72..e2fee1f503 100644 --- a/docs/using/common.md +++ b/docs/using/common.md @@ -1,14 +1,14 @@ # Common Features Except for `jupyter/docker-stacks-foundation`, a container launched from any Jupyter Docker Stacks image runs a Jupyter Server with the JupyterLab frontend. -The container does so by executing a `start-notebook.sh` script. +The container does so by executing a `start-notebook.py` script. This script configures the internal container environment and then runs `jupyter lab`, passing any command-line arguments received. This page describes the options supported by the startup script and how to bypass it to run alternative commands. ## Jupyter Server Options -You can pass [Jupyter Server options](https://jupyter-server.readthedocs.io/en/latest/operators/public-server.html) to the `start-notebook.sh` script when launching the container. +You can pass [Jupyter Server options](https://jupyter-server.readthedocs.io/en/latest/operators/public-server.html) to the `start-notebook.py` script when launching the container. 1. For example, to secure the Jupyter Server with a [custom password](https://jupyter-server.readthedocs.io/en/latest/operators/public-server.html#preparing-a-hashed-password) hashed using `jupyter_server.auth.passwd()` instead of the default token, @@ -16,19 +16,19 @@ You can pass [Jupyter Server options](https://jupyter-server.readthedocs.io/en/l ```bash docker run -it --rm -p 8888:8888 jupyter/base-notebook \ - start-notebook.sh --PasswordIdentityProvider.hashed_password='argon2:$argon2id$v=19$m=10240,t=10,p=8$JdAN3fe9J45NvK/EPuGCvA$O/tbxglbwRpOFuBNTYrymAEH6370Q2z+eS1eF4GM6Do' + start-notebook.py --PasswordIdentityProvider.hashed_password='argon2:$argon2id$v=19$m=10240,t=10,p=8$JdAN3fe9J45NvK/EPuGCvA$O/tbxglbwRpOFuBNTYrymAEH6370Q2z+eS1eF4GM6Do' ``` 2. To set the [base URL](https://jupyter-server.readthedocs.io/en/latest/operators/public-server.html#running-the-notebook-with-a-customized-url-prefix) of the Jupyter Server, you can run the following: ```bash docker run -it --rm -p 8888:8888 jupyter/base-notebook \ - start-notebook.sh --ServerApp.base_url=/customized/url/prefix/ + start-notebook.py --ServerApp.base_url=/customized/url/prefix/ ``` ## Docker Options -You may instruct the `start-notebook.sh` script to customize the container environment before launching the Server. +You may instruct the `start-notebook.py` script to customize the container environment before launching the Server. You do so by passing arguments to the `docker run` command. ### User-related configurations @@ -104,7 +104,7 @@ You do so by passing arguments to the `docker run` command. You do **not** need this option to allow the user to `conda` or `pip` install additional packages. This option is helpful for cases when you wish to give `${NB_USER}` the ability to install OS packages with `apt` or modify other root-owned files in the container. You **must** run the container with `--user root` for this option to take effect. - (The `start-notebook.sh` script will `su ${NB_USER}` after adding `${NB_USER}` to sudoers.) + (The `start-notebook.py` script will `su ${NB_USER}` after adding `${NB_USER}` to sudoers.) **You should only enable `sudo` if you trust the user or if the container runs on an isolated host.** ### Additional runtime configurations @@ -147,7 +147,7 @@ For example, to mount a host folder containing a `notebook.key` and `notebook.cr docker run -it --rm -p 8888:8888 \ -v /some/host/folder:/etc/ssl/notebook \ jupyter/base-notebook \ - start-notebook.sh \ + start-notebook.py \ --ServerApp.keyfile=/etc/ssl/notebook/notebook.key \ --ServerApp.certfile=/etc/ssl/notebook/notebook.crt ``` @@ -159,7 +159,7 @@ For example: docker run -it --rm -p 8888:8888 \ -v /some/host/folder/notebook.pem:/etc/ssl/notebook.pem \ jupyter/base-notebook \ - start-notebook.sh \ + start-notebook.py \ --ServerApp.certfile=/etc/ssl/notebook.pem ``` @@ -220,7 +220,7 @@ docker run -it --rm \ ### `start.sh` -The `start-notebook.sh` script inherits most of its option handling capability from a more generic `start.sh` script. +The `start-notebook.py` script inherits most of its option handling capability from a more generic `start.sh` script. The `start.sh` script supports all the features described above but allows you to specify an arbitrary command to execute. For example, to run the text-based `ipython` console in a container, do the following: diff --git a/docs/using/recipes.md b/docs/using/recipes.md index cfeb4cedf5..196cd425be 100644 --- a/docs/using/recipes.md +++ b/docs/using/recipes.md @@ -375,14 +375,14 @@ Credit: [britishbadger](https://github.com/britishbadger) from [docker-stacks/is The default security is very good. There are use cases, encouraged by containers, where the jupyter container and the system it runs within lie inside the security boundary. It is convenient to launch the server without a password or token in these use cases. -In this case, you should use the `start-notebook.sh` script to launch the server with no token: +In this case, you should use the `start-notebook.py` script to launch the server with no token: For JupyterLab: ```bash docker run -it --rm \ jupyter/base-notebook \ - start-notebook.sh --IdentityProvider.token='' + start-notebook.py --IdentityProvider.token='' ``` For Jupyter Notebook: @@ -391,7 +391,7 @@ For Jupyter Notebook: docker run -it --rm \ -e DOCKER_STACKS_JUPYTER_CMD=notebook \ jupyter/base-notebook \ - start-notebook.sh --IdentityProvider.token='' + start-notebook.py --IdentityProvider.token='' ``` ## Enable nbclassic-extension spellchecker for markdown (or any other nbclassic-extension) diff --git a/docs/using/running.md b/docs/using/running.md index 4e764184ce..b888c04309 100644 --- a/docs/using/running.md +++ b/docs/using/running.md @@ -69,7 +69,7 @@ Any other changes made in the container will be lost. By default, [jupyter's root_dir](https://jupyter-server.readthedocs.io/en/latest/other/full-config.html) is `/home/jovyan`. So, new notebooks will be saved there, unless you change the directory in the file browser. -To change the default directory, you will need to specify `ServerApp.root_dir` by adding this line to previous command: `start-notebook.sh --ServerApp.root_dir=/home/jovyan/work`. +To change the default directory, you will need to specify `ServerApp.root_dir` by adding this line to previous command: `start-notebook.py --ServerApp.root_dir=/home/jovyan/work`. ``` ### Example 3 diff --git a/docs/using/selecting.md b/docs/using/selecting.md index 450e463a8c..7f763feeae 100644 --- a/docs/using/selecting.md +++ b/docs/using/selecting.md @@ -56,10 +56,17 @@ It contains: - Everything in `jupyter/docker-stacks-foundation` - Minimally functional Server (e.g., no LaTeX support for saving notebooks as PDFs) - `notebook`, `jupyterhub` and `jupyterlab` packages -- A `start-notebook.sh` script as the default command -- A `start-singleuser.sh` script useful for launching containers in JupyterHub +- A `start-notebook.py` script as the default command +- A `start-singleuser.py` script useful for launching containers in JupyterHub - Options for a self-signed HTTPS certificate +```{warning} +`jupyter/base-notebook` also contains `start-notebook.sh` and `start-singleuser.sh` files to maintain backwards compatibility. +External config that explicitly refers to those files should instead +update to refer to `start-notebook.py` and `start-singleuser.py`. +The shim `.sh` files will be removed at some future date. +``` + ### jupyter/minimal-notebook [Source on GitHub](https://github.com/jupyter/docker-stacks/tree/main/images/minimal-notebook) | diff --git a/examples/docker-compose/notebook/letsencrypt-notebook.yml b/examples/docker-compose/notebook/letsencrypt-notebook.yml index 1c47c99e0f..06bab31966 100644 --- a/examples/docker-compose/notebook/letsencrypt-notebook.yml +++ b/examples/docker-compose/notebook/letsencrypt-notebook.yml @@ -18,7 +18,7 @@ services: USE_HTTPS: "yes" PASSWORD: ${PASSWORD} command: > - start-notebook.sh + start-notebook.py --ServerApp.certfile=/etc/letsencrypt/fullchain.pem --ServerApp.keyfile=/etc/letsencrypt/privkey.pem diff --git a/examples/make-deploy/Makefile b/examples/make-deploy/Makefile index e937621c60..aa62dd0bbf 100644 --- a/examples/make-deploy/Makefile +++ b/examples/make-deploy/Makefile @@ -13,7 +13,7 @@ define RUN_NOTEBOOK --name $(NAME) \ -v $(WORK_VOLUME):/home/jovyan/work \ $(DOCKER_ARGS) \ - $(IMAGE) bash -c "$(PRE_CMD) chown jovyan /home/jovyan/work && start-notebook.sh $(ARGS)" > /dev/null + $(IMAGE) bash -c "$(PRE_CMD) chown jovyan /home/jovyan/work && start-notebook.py $(ARGS)" > /dev/null @echo "DONE: Notebook '$(NAME)' listening on $$(docker-machine ip $$(docker-machine active)):$(PORT)" endef diff --git a/examples/openshift/templates.json b/examples/openshift/templates.json index 6c48e129be..e12036ecb8 100644 --- a/examples/openshift/templates.json +++ b/examples/openshift/templates.json @@ -80,7 +80,7 @@ "name": "jupyter-notebook", "image": "${NOTEBOOK_IMAGE}", "command": [ - "start-notebook.sh", + "start-notebook.py", "--config=/etc/jupyter/openshift/jupyter_server_config.py", "--no-browser", "--ip=0.0.0.0" diff --git a/examples/source-to-image/README.md b/examples/source-to-image/README.md index 1e9d3ba992..8639c10999 100644 --- a/examples/source-to-image/README.md +++ b/examples/source-to-image/README.md @@ -117,7 +117,7 @@ with the extra system packages, and then use that image with the S2I build to co The `run` script in this directory is very simple and just runs the notebook application. ```bash -exec start-notebook.sh "$@" +exec start-notebook.py "$@" ``` ## Integration with OpenShift diff --git a/examples/source-to-image/run b/examples/source-to-image/run index b5b641b8f6..556efdda9d 100755 --- a/examples/source-to-image/run +++ b/examples/source-to-image/run @@ -2,4 +2,4 @@ # Start up the notebook instance. -exec start-notebook.sh "$@" +exec start-notebook.py "$@" diff --git a/examples/source-to-image/templates.json b/examples/source-to-image/templates.json index e335fc6877..8daa0823d0 100644 --- a/examples/source-to-image/templates.json +++ b/examples/source-to-image/templates.json @@ -274,7 +274,7 @@ "name": "jupyter-notebook", "image": "${APPLICATION_NAME}:latest", "command": [ - "start-notebook.sh", + "start-notebook.py", "--config=/etc/jupyter/openshift/jupyter_server_config.py", "--no-browser", "--ip=0.0.0.0" diff --git a/images/base-notebook/Dockerfile b/images/base-notebook/Dockerfile index 5f47a5d425..03c6eed9d9 100644 --- a/images/base-notebook/Dockerfile +++ b/images/base-notebook/Dockerfile @@ -52,10 +52,10 @@ ENV JUPYTER_PORT=8888 EXPOSE $JUPYTER_PORT # Configure container startup -CMD ["start-notebook.sh"] +CMD ["start-notebook.py"] # Copy local files as late as possible to avoid cache busting -COPY start-notebook.sh start-singleuser.sh /usr/local/bin/ +COPY start-notebook.py start-notebook.sh start-singleuser.py start-singleuser.sh /usr/local/bin/ COPY jupyter_server_config.py docker_healthcheck.py /etc/jupyter/ # Fix permissions on /etc/jupyter as root diff --git a/images/base-notebook/start-notebook.py b/images/base-notebook/start-notebook.py new file mode 100755 index 0000000000..db1efc246c --- /dev/null +++ b/images/base-notebook/start-notebook.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +import os +import shlex +import sys + +# If we are in a JupyterHub, we pass on to `start-singleuser.py` instead so it does the right thing +if "JUPYTERHUB_API_TOKEN" in os.environ: + print( + "WARNING: using start-singleuser.py instead of start-notebook.py to start a server associated with JupyterHub." + ) + command = ["/usr/local/bin/start-singleuser.py"] + sys.argv[1:] + os.execvp(command[0], command) + + +# Wrap everything in start.sh, no matter what +command = ["/usr/local/bin/start.sh"] + +# If we want to survive restarts, tell that to start.sh +if os.environ.get("RESTARTABLE") == "yes": + command.append("run-one-constantly") + +# We always launch a jupyter subcommand from this script +command.append("jupyter") + +# Launch the configured subcommand. Note that this should be a single string, so we don't split it +# We default to lab +jupyter_command = os.environ.get("DOCKER_STACKS_JUPYTER_CMD", "lab") +command.append(jupyter_command) + +# Append any optional NOTEBOOK_ARGS we were passed in. This is supposed to be multiple args passed +# on to the notebook command, so we split it correctly with shlex +if "NOTEBOOK_ARGS" in os.environ: + command += shlex.split(os.environ["NOTEBOOK_ARGS"]) + +# Pass through any other args we were passed on the commandline +command += sys.argv[1:] + +# Execute the command! +os.execvp(command[0], command) diff --git a/images/base-notebook/start-notebook.sh b/images/base-notebook/start-notebook.sh index 4f673d22a5..c47ebba334 100755 --- a/images/base-notebook/start-notebook.sh +++ b/images/base-notebook/start-notebook.sh @@ -1,22 +1,5 @@ #!/bin/bash -# Copyright (c) Jupyter Development Team. -# Distributed under the terms of the Modified BSD License. +# Shim to emit warning and call start-notebook.py +echo "WARNING: Use start-notebook.py instead" -set -e - -# The Jupyter command to launch -# JupyterLab by default -DOCKER_STACKS_JUPYTER_CMD="${DOCKER_STACKS_JUPYTER_CMD:=lab}" - -if [[ -n "${JUPYTERHUB_API_TOKEN}" ]]; then - echo "WARNING: using start-singleuser.sh instead of start-notebook.sh to start a server associated with JupyterHub." - exec /usr/local/bin/start-singleuser.sh "$@" -fi - -wrapper="" -if [[ "${RESTARTABLE}" == "yes" ]]; then - wrapper="run-one-constantly" -fi - -# shellcheck disable=SC1091,SC2086 -exec /usr/local/bin/start.sh ${wrapper} jupyter ${DOCKER_STACKS_JUPYTER_CMD} ${NOTEBOOK_ARGS} "$@" +exec /usr/local/bin/start-notebook.py "$@" diff --git a/images/base-notebook/start-singleuser.py b/images/base-notebook/start-singleuser.py new file mode 100755 index 0000000000..2dcf6c091c --- /dev/null +++ b/images/base-notebook/start-singleuser.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +import os +import shlex +import sys + +command = ["/usr/local/bin/start.sh", "jupyterhub-singleuser"] + +# set default ip to 0.0.0.0 +if "--ip=" not in os.environ.get("NOTEBOOK_ARGS", ""): + command.append("--ip=0.0.0.0") + +# Append any optional NOTEBOOK_ARGS we were passed in. This is supposed to be multiple args passed +# on to the notebook command, so we split it correctly with shlex +if "NOTEBOOK_ARGS" in os.environ: + command += shlex.split(os.environ["NOTEBOOK_ARGS"]) + +# Pass any other args we have been passed through +command += sys.argv[1:] + +# Execute the command! +os.execvp(command[0], command) diff --git a/images/base-notebook/start-singleuser.sh b/images/base-notebook/start-singleuser.sh index a2166e2c6d..ecf0e068ae 100755 --- a/images/base-notebook/start-singleuser.sh +++ b/images/base-notebook/start-singleuser.sh @@ -1,13 +1,5 @@ #!/bin/bash -# Copyright (c) Jupyter Development Team. -# Distributed under the terms of the Modified BSD License. +# Shim to emit warning and call start-singleuser.py +echo "WARNING: Use start-singleuser.py instead" -set -e - -# set default ip to 0.0.0.0 -if [[ "${NOTEBOOK_ARGS} $*" != *"--ip="* ]]; then - NOTEBOOK_ARGS="--ip=0.0.0.0 ${NOTEBOOK_ARGS}" -fi - -# shellcheck disable=SC1091,SC2086 -. /usr/local/bin/start.sh jupyterhub-singleuser ${NOTEBOOK_ARGS} "$@" +exec /usr/local/bin/start-singleuser.py "$@" diff --git a/tests/base-notebook/test_container_options.py b/tests/base-notebook/test_container_options.py index 5fa28d8913..1ea501d85c 100644 --- a/tests/base-notebook/test_container_options.py +++ b/tests/base-notebook/test_container_options.py @@ -15,7 +15,7 @@ def test_cli_args(container: TrackedContainer, http_client: requests.Session) -> """Image should respect command line args (e.g., disabling token security)""" host_port = find_free_port() running_container = container.run_detached( - command=["start-notebook.sh", "--IdentityProvider.token=''"], + command=["start-notebook.py", "--IdentityProvider.token=''"], ports={"8888/tcp": host_port}, ) resp = http_client.get(f"http://localhost:{host_port}") @@ -102,7 +102,7 @@ def test_custom_internal_port( host_port = find_free_port() internal_port = env.get("JUPYTER_PORT", 8888) running_container = container.run_detached( - command=["start-notebook.sh", "--IdentityProvider.token=''"], + command=["start-notebook.py", "--IdentityProvider.token=''"], environment=env, ports={internal_port: host_port}, ) diff --git a/tests/base-notebook/test_healthcheck.py b/tests/base-notebook/test_healthcheck.py index 73de100303..50d83c27ad 100644 --- a/tests/base-notebook/test_healthcheck.py +++ b/tests/base-notebook/test_healthcheck.py @@ -22,23 +22,24 @@ (["RESTARTABLE=yes"], None, None), (["JUPYTER_PORT=8171"], None, None), (["JUPYTER_PORT=8117", "DOCKER_STACKS_JUPYTER_CMD=notebook"], None, None), - (None, ["start-notebook.sh", "--ServerApp.base_url=/test"], None), - (None, ["start-notebook.sh", "--ServerApp.base_url=/test/"], None), - (["GEN_CERT=1"], ["start-notebook.sh", "--ServerApp.base_url=/test"], None), + (None, ["start-notebook.sh"], None), + (None, ["start-notebook.py", "--ServerApp.base_url=/test"], None), + (None, ["start-notebook.py", "--ServerApp.base_url=/test/"], None), + (["GEN_CERT=1"], ["start-notebook.py", "--ServerApp.base_url=/test"], None), ( ["GEN_CERT=1", "JUPYTER_PORT=7891"], - ["start-notebook.sh", "--ServerApp.base_url=/test"], + ["start-notebook.py", "--ServerApp.base_url=/test"], None, ), (["NB_USER=testuser", "CHOWN_HOME=1"], None, "root"), ( ["NB_USER=testuser", "CHOWN_HOME=1"], - ["start-notebook.sh", "--ServerApp.base_url=/test"], + ["start-notebook.py", "--ServerApp.base_url=/test"], "root", ), ( ["NB_USER=testuser", "CHOWN_HOME=1", "JUPYTER_PORT=8123"], - ["start-notebook.sh", "--ServerApp.base_url=/test"], + ["start-notebook.py", "--ServerApp.base_url=/test"], "root", ), ], @@ -85,7 +86,7 @@ def test_health( "HTTPS_PROXY=host.docker.internal", "HTTP_PROXY=host.docker.internal", ], - ["start-notebook.sh", "--ServerApp.base_url=/test"], + ["start-notebook.py", "--ServerApp.base_url=/test"], "root", ), ], @@ -122,12 +123,12 @@ def test_health_proxy( (["NB_USER=testuser", "CHOWN_HOME=1"], None, None), ( ["NB_USER=testuser", "CHOWN_HOME=1"], - ["start-notebook.sh", "--ServerApp.base_url=/test"], + ["start-notebook.py", "--ServerApp.base_url=/test"], None, ), ( ["NB_USER=testuser", "CHOWN_HOME=1", "JUPYTER_PORT=8123"], - ["start-notebook.sh", "--ServerApp.base_url=/test"], + ["start-notebook.py", "--ServerApp.base_url=/test"], None, ), ], diff --git a/tests/base-notebook/test_start_container.py b/tests/base-notebook/test_start_container.py index 556f4c2c7c..830b36c7e7 100644 --- a/tests/base-notebook/test_start_container.py +++ b/tests/base-notebook/test_start_container.py @@ -25,7 +25,7 @@ ["JUPYTERHUB_API_TOKEN=my_token"], "jupyterhub-singleuser", False, - ["WARNING: using start-singleuser.sh"], + ["WARNING: using start-singleuser.py"], ), ], ) @@ -37,9 +37,9 @@ def test_start_notebook( expected_start: bool, expected_warnings: list[str], ) -> None: - """Test the notebook start-notebook script""" + """Test the notebook start-notebook.py script""" LOGGER.info( - f"Test that the start-notebook launches the {expected_command} server from the env {env} ..." + f"Test that the start-notebook.py launches the {expected_command} server from the env {env} ..." ) host_port = find_free_port() running_container = container.run_detached( diff --git a/tests/pluto_check.py b/tests/pluto_check.py index 5fc269de2e..ebb558b4be 100644 --- a/tests/pluto_check.py +++ b/tests/pluto_check.py @@ -18,7 +18,7 @@ def check_pluto_proxy( token = secrets.token_hex() container.run_detached( command=[ - "start-notebook.sh", + "start-notebook.py", f"--IdentityProvider.token={token}", ], ports={"8888/tcp": host_port},