SwarmSpawner enables JupyterHub to spawn single user notebook servers in Docker Services.
This is a MODIFIED version of the original cassinyio/SwarmSpawner.
Modifications include:
- Removal of the hashing of usernames. Each spawned service will now be named after the user who owns it. This improves compatibility with images used by the DockerSpawner/SystemUserSpawner from jupyterhub/dockerspawner.
- Added
{username}
support to mount point targets. Previously, only a mount's source could include{username}
. - Added
{username}
support to notebook_dir config parameter. - Added workdir (optional) to container_spec config parameter. The workdir also
supports the
{username}
magic. This currently defaults to the notebook_dir if notebook_dir is specified. Previously, a system user container spawned by SwarmSpawner would open to the location specified byJUPYTER_SERVER_ROOT
.JUPYTER_SERVER_ROOT
(whether intended or not) gets set to the lastWORKDIR
declared in a Dockerfile. The value of workdir overrides notebook_dir for the directory your notebook opens. I wasn't sure if there were other reasons one might want to set a separate notebook_dir. Perhaps the appropriate behavior is to always set the workdir to the value of notebook_dir. - Added
USER_ID
,USER
,GROUP_ID
andGROUP
to getenv for compatibility with my setup. May be more appropriate to addJPY_USER_ID
,JPY_GROUP_ID
,JPY_GROUP
and other values (see: set_user_setuid). There may already be self values for these(?).
More info about Docker Services here.
Python version 3.3 and above is required.
Clone the repo:
git clone https://github.com/vroomanj/SwarmSpawner
cd SwarmSpawner
Install dependencies:
pip install -r requirements.txt
Install SwarmSpawner to the system:
python setup.py install
You can find an example jupyter_config.py inside examples
Docker Engine in Swarm Mode and the related services work in a different way compared to Docker containers.
Tell JupyterHub to use SwarmSpawner by adding the following lines to
your jupyterhub_config.py
:
c.JupyterHub.spawner_class = 'cassinyspawner.SwarmSpawner'
# This should be the name of the jupyterhub service
c.JupyterHub.hub_ip = '0.0.0.0'
c.SwarmSpawner.jupyterhub_service_name = 'NameOfTheService'
What is jupyterhub_service_name
?
Inside a Docker engine in Swarm Mode the services use a name
instead of a ip
to communicate with each other.
'jupyterhub_service_name' is the name of ther service for the JupyterHub.
It's important to put the JupyterHub service (also the proxy) and the services that are running Jupyter Notebook inside the same network, otherwise they couldn't reach each other. SwarmSpawner use the service's name instead of the service's ip, as a consequence JupyterHub and servers should share the same overlay network (network across nodes).
c.SwarmSpawner.networks = ["mynetwork"] #list of networks
You can define container_spec, resource_spec and networks within jupyterhub_config.py.
'command' and 'args' depends on the image that you are using.
If you use one of the images from the Jupyter docker stack you need to specify args as: /usr/local/bin/start-singleuser.sh
If you are using a specific image, well it's up to you to specify the right command.
c.SwarmSpawner.container_spec = {
# The command to run inside the service
# 'args' : ['/usr/local/bin/start-singleuser.sh'], # (list)
'Image' : 'YourImage',
'mounts' : mounts
}
Note: in a container spec, args
sets the equivalent of CMD in the Dockerfile.
command
sets the equivalent of ENTRYPOINT.
The notebook server itself should not be the ENTRYPOINT,
so generally use args
, not command
, to specify how to launch the notebook server.
See this issue for more info.
With 'type':'bind'
you mount a local directory of the host inside the container.
Remember that source should exist in the node where you are creating the service.
notebook_dir = os.environ.get('NOTEBOOK_DIR') or '/home/jovyan/work'
c.SwarmSpawner.notebook_dir = notebook_dir
mounts = [{'type' : 'bind',
'source' : 'MountPointOnTheHost',
'target' : 'MountPointInsideTheContainer',}]
With 'type':'volume'
you mount a Docker Volume inside the container.
If the volume doesn't exist it will be created.
mounts = [{'type' : 'volume',
'source' : 'NameOfTheVolume',
'target' : 'MountPointInsideTheContainer',}]
For both types, volume and bind, you can specify a {username}
inside the source:
mounts = [{'type' : 'volume',
'source' : 'jupyterhub-user-{username}',
'target' : 'MountPointInsideTheContainer',}]
username will be the hashed version of the username.
This kind of volume will be removed with the service
mounts = [{'type' : 'volume',
'target' : 'MountPointInsideTheContainer',}]
You can also specify some resource for each service
c.SwarmSpawner.resource_spec = {
'cpu_limit' : 1000, # (int) – CPU limit in units of 10^9 CPU shares.
'mem_limit' : int(512 * 1e6), # (int) – Memory limit in Bytes.
'cpu_reservation' : 1000, # (int) – CPU reservation in units of 10^9 CPU shares.
'mem_reservation' : int(512 * 1e6), # (int) – Memory reservation in Bytes
}
There is the possibility to set parameters using user_options
# To use user_options in service creation
c.SwarmSpawner.use_user_options = False
To control the creation of the services you have 2 ways, using jupyterhub_config.py or user_options.
Remember that at the end you are just using the Docker Engine API.
user_options, if used, will overwrite jupyter_config.py for services.
If you set 'c.SwarmSpawner.use_user_option = True' the spawner will use the dict passed through the form or as json body when using the Hub Api.
The spawner expect a dict with these keys:
user_options = {
'container_spec' : {
'args' : ['/usr/local/bin/start-singleuser.sh'], #(string or list) command to run in the image.
'Image' : '', # name of the image
'mounts' : mounts, # Same as jupyterhub_config
'resource_spec' : {
'cpu_limit' : 1000, # (int) – CPU limit in units of 10^9 CPU shares.
'mem_limit' : int(512 * 1e6),# (int) – Memory limit in Bytes.
'cpu_reservation' : 1000, # (int) – CPU reservation in units of 10^9 CPU shares.
'mem_reservation' : int(512 * 1e6), # (int) – Memory reservation in Bytes
},
'placement' : [], #list of constrains
'network' : [], #list of networks
'name' : '' # Name of service
We JupyterHub spawns a new Jupyter Notebook server the name of the service will be {service_prefix}-{service_owner}-{service_suffix}
You can change the service_prefix in this way:
Prefix of the service in Docker
c.SwarmSpawner.service_prefix = "jupyterhub"
service_owner
is the hexdigest() of the hashed user.name
.
In case of named servers (more than one server for user) service_suffix
is the name of the server, otherwise is always 1.
Docker Engine in Swarm Mode downloads images automatically from the repository. Either the image is available on the remote repository or locally, if not you will get an error.
Because before starting the service you have to complete the download of the image is better to have a longer timeout (default is 30 secs)
c.SwarmSpawner.start_timeout = 60 * 5
You can use all the docker images built for JupyterHub.
If you would like to contribute to the project, please read contributor documentation.
For a development install, clone the repository and then install from source:
git clone https://github.com/cassiny/SwarmSpawner
cd SwarmSpawner
pip3 install -r dev-requirements.txt -e .
All code is licensed under the terms of the revised BSD license.