not just sliders!
Web server to run just the @bind parts of a Pluto.jl notebook.
See it in action at computationalthinking.mit.edu! Sliders, buttons and camera inputs work instantly, without having to wait for a Julia process. Plutoplutopluto
using PlutoSliderServer
path_to_notebook = download("https://raw.githubusercontent.com/fonsp/Pluto.jl/v0.17.2/sample/Interactivity.jl") # fill in your own notebook path here!
PlutoSliderServer.run_notebook(path_to_notebook)
Now open a browser, and go to the address printed in your terminal!
Later when you are using PlutoSliderServer on a public website, PlutoSliderServer.run_notebook
is a good way to test your notebook locally before putting it online.
PlutoSliderServer can run a notebook and generate the export HTML file. This will give you the same file as the export button inside Pluto (top right), but automatically, without opening a browser.
One use case is to automatically create a GitHub Pages site from a repository with notebooks. For this, take a look at our template repository that used GitHub Actions and PlutoSliderServer to generate a website on every commit.
PlutoSliderServer.export_notebook("path/to/notebook.jl")
# will create a file `path/to/notebook.html`
The main functionality of PlutoSliderServer is to run a slider server. This is a web server that runs a notebook using Pluto, and allows visitors to change the values of @bind
-ed variables.
The important differences between running a slider server and running Pluto with public access are:
- A slider server can only set
@bind
values, it is not possible to change the notebook's code. - A slider server is stateless: it does not keep track of user sessions. Every request to a slider server is an isolated HTTP
GET
request, while Pluto maintains a WebSocket connection. - Pluto synchronizes everything between all connected clients in realtime. The slider server does the opposite: all 'clients' are disconnected, they don't see the
@bind
values or state of others.
To learn more, watch the PlutoCon 2020 presentation about how PlutoSliderServer works.
PlutoSliderServer.run_notebook("path/to/notebook.jl")
# will create a file `path/to/notebook.html`
Many input elements only have a finite number of possible values, for example, PlutoUI.Slider(5:15)
can only have 11 values. For finite inputs like the slider, PlutoSliderServer can run the slider server in advance, and precompute the results to all possible inputs (in other words: precompute the response to all possible requests).
This will generate a directory of subdirectories and files, each corresponding to a possible request. You can host this directory along with the generated HTML file (e.g. on GitHub pages), and Pluto will be able to use these pregenerated files as if they are a slider server! You can get the interactivity of a slider server, without running a Julia server!
We use the bond connections graph to understand which bound variables are co-dependent, and which are disconnected. For all groups of co-dependent variables, we precompute all possible combinations of their values. This allows us to tame the 'combinatorial explosion' that you would get when considering all possible combinations of all bound variables! If two variables are 'disconnected', then we don't need to consider possible combinations between them.
This part is still work-in-progress: #29
All of the functionality above can also be used on all notebooks in a directory. PlutoSliderServer will scan a directory recursively for notebook files.
See PlutoSliderServer.export_directory
and PlutoSliderServer.run_directory
.
After scanning a directory for notebook files, you can ask Pluto to continue watching the directory for changes. When notebook files are added/removed, they are also added/removed from the server. When a notebook file changes, the notebook session is restarted.
This works especially well when this directory is a git-tracked directory. When running in a git directory, PlutoSliderServer can keep git pull
ing the directory, updating from the repository automatically.
See the SliderServer_watch_dir
option and PlutoSliderServer.run_git_directory
.
The result is a Continuous Deployment setup: you can set up your PlutoSliderServer on a dedicated server running online, synced with your repository on github. You can then update the repository, and the PlutoSliderServer will update automatically.
The alternative is to redeploy the entire server every time a notebook changes. We found that this setup works fairly well, but causes long downtimes whenever a notebook changes, because all notebooks need to re-run. This can be a problem if your project consists of many notebooks, and they change frequently.
See PlutoSliderServer.run_git_directory
.
PlutoSliderServer is not just Pluto with editing disabled – it works quite differently. This section explains why it has to be different, how it works, and what the advantages and disadvantages are.
PlutoCon 2020 presentation about how PlutoSliderServer works
When I created this package for computationalthinking.mit.edu, the goal was to serve notebook HTMLs publicly online, with working interactivity, for a large number of visitors.
The simplest approach is to just run Pluto on a public server and disable editing. (Try it: open a notebook in Pluto, copy the URL, and open it in multiple windows next to each other. Add &disable_ui=true
to the URL to simulate disabled editing.) The problem with this is that all sessions are synchronised: moving a slider in one window moves it everywhere. And updates to other cells (values and plots) show in all windows. You could simply disable this synchronisation between windows, but then you would still "feel" effects from other users. You might not see other sliders moving, but they are still changing values in your shared session, influencing results.
A crucial idea in the PlutoSliderServer is the bond connections graph. This is a bit of a mathematical adventure, I tried my best to explain it in the PlutoCon 2020 presentation about how PlutoSliderServer works. Here is another explanation in text:
Let's take a look at this simple notebook:
@bind x Slider(1:10)
@bind y Slider(1:5)
x + y
@bind z Slider(1:100)
"Hello $(z)!"
We have three bound variables: x
, y
and z
. When analyzed by Pluto, we find the dependecies between cells: 1 -> 3
, 2 -> 3
, 4 -> 5
. This means that, as a graph, the last two cells are completely disconnected from the rest of the graph. Our bond connections graph will capture this idea.
For each bound variable, we use Pluto's reactivity graph to know:
- Which cells depend on the bound variable?
- Which bound variables are (indirect) dependencies of any cell from (1)? These are called the co-dependencies of the bound variable.
In our example, x
influences the result of x + y
, which depends on y
. So x
and y
are the co-dependencies of x
. Variable z
influences "Hello $(z)!"
, which is does not have x
or y
as dependencies. So z
is not codependent with x
or with y
.
This forms a dictionary, which looks like:
Dict(
:x => [:x, :y],
:y => [:x, :y],
:z => [:z],
)
For more examples, take a look at this notebook, which has this bond connection graph.
Now, whenever you send the value of a bound variable x
to the slider server, you also have to send the values of the co-dependencies of x
, which are x
and y
in our example. By sending both, you are sending all the information that is needed to fully determine the dependent cells.
Like the regular slider server, we use the bond connections graph, which tells us which bound variables are co-dependent. This allows us to tame the 'combinatorical explosion' that you would get when considering all possible combinations of all bound variables! If two variables are 'disconnected', then we don't need to consider possible combinations between them.
In our example notebook, there are 10 (x) * 5 (y) + 100 (z) = 150
combinations to precompute. Without considering the connections graph, there would be 10 (x) * 5 (y) * 100 (z) = 5000
possible combinations.
As PlutoSliderServer embeds so much functionality, it may be confusing to figure out how to approach your setting. Here is an overview of our most important functions:
export_directory
will find all notebooks in a directory, run them, and generate HTML files. (export_notebook
for a single file.) One example use case is https://github.com/JuliaPluto/static-export-templaterun_directory
does the same asexport_directory
, but it keeps the notebooks running and runs the slider server! It will also watch the given directory for changes to notebook files, and automatically update the slider server. (run_notebook
for a single file.)run_git_directory
does the same asrun_directory
, but it will keep runninggit pull
in the given directory. Any changes will get picked up by our directory watching!
PlutoSliderServer is very configurable, and we use Configurations.jl to configure the server. We try our best to be smart about the default settings, and we hope that most users do not need to configure anything.
There are two ways to change configurations: using keywords arguments, and using a PlutoDeployment.toml
file.
Our functions can take keyword arguments, for example:
run_directory("my_notebooks";
SliderServer_port=8080,
SliderServer_host="0.0.0.0",
Export_baked_notebookfile=false,
)
🌟 For the full list of options, see the documentation for the function you are using. For example, in the Julia REPL, run
?run_directory
.
If you are using a package environment for your slider server (if you are deploying it on a server, you probably should), then you can also use a TOML file to configure PlutoSliderServer.
In the same folder where you have your Project.toml
and Manifest.toml
files, create a third file, called PlutoDeployment.toml
. Its contents should look something like:
[Export]
baked_notebookfile = true
[SliderServer]
port = 8080
host = "0.0.0.0"
# You can also set Pluto's configuration here:
[Pluto]
[Pluto.compiler]
threads = 2
# See documentation for `Pluto.Configuration` for the full list of options. You need specify the categories within `Pluto.Configuration.Options` (`compiler`, `evaluation`, etc).
🌟 For the full list of options, run
PlutoSliderServer.show_sample_config_toml_file()
.
Our functions will look for the existance of a file called PlutoDeployment.toml
in the active package environment, and use it automatically.
You can also combine the two configuration methods: keyword options and toml options will be merged, the former taking precedence.
Sample setup: Given a repository, start a PlutoSliderServer to serve static exports with live preview
These instructions set up a slider server on a dedicated server, which automatically synchronises with a git repository, containing the notebook files. Make sure to create one before we start.
Disclaimer: This is work in progress, there might be holes!
Create a folder called pluto-slider-server-environment
with the Project.toml
and Manifest.toml
for the PlutoSliderServer
: (Not the notebooks - the notebooks should contain their own package environment.)
$ cd <your-repository-with-notebooks>
$ mkdir pluto-slider-server-environment
$ julia --project=pluto-slider-server-environment
julia> ]
pkg> add Pluto PlutoSliderServer
Create a configuration file in the same folder as Project.toml
, see the section about PlutoDeployment.toml
above.
TEMPFILE=$(mktemp)
cat > $TEMPFILE << __EOF__
[SliderServer]
port = 8080
host = "0.0.0.0"
# more configuration can go here!
__EOF__
sudo mv $TEMPFILE pluto-slider-server-environment/PlutoDeployment.toml
This configuration sets the port to 8080
(not 80
, this requires sudo), and the host to "0.0.0.0"
(which allows traffic from outside the computer, unlike the default "127.0.0.1"
).
Let's try running it locally before setting up our server:
julia --project="pluto-slider-server-environment" -e "import PlutoSliderServer; PlutoSliderServer.run_git_directory(\".\")"
run_git_directory
will periodically call git pull
, which requires the start_dir
to be a repository in which you can git pull
without password (which means it's either public, or you have the required keys in ~/.ssh/
and your git's provider security page!)
Note Julia by default uses
libgit2
for git operations, which can be problematic. It is also known to cause issues in cloud environments like AWS's CodeCommit where re-authentication is required at regular intervals.A simple workaround is to set the
JULIA_PKG_USE_CLI_GIT
environment variable totrue
, which will fallback to the system git (the one on the shell). Make sure that this is installed! (sudo apt-get install git
does the trick in Ubuntu).
Also note that git pull
may fail on the server if you force push the branch from your laptop, so handle history-rewriting commands, like git push -f
, git rebase
etc with care!
For this step, we'll assume a very specific but also common setup:
- Ubuntu-based server with
apt-get
,git
,vim
and internet - access through SSH
- root access
- port 80 and 8080 are open to the web
The easiest way to get this is to rent a server from digitalocean, AWS, Google Cloud, hetzner, etc. This setup was tested with digitalocean.com, which has an easy interface for beginners.
When renting a server, you need to decide which "droplet size" you want. The bottleneck is memory – CPU power and disk space will always be sufficient. As minimum, you need
500MB + 300MB * length(notebooks)
. But if you use large packages, like Plots or DifferentialEquations, a notebook might need 1000MB memory.There is no minimum requirement on CPU power, but it does have a big impact on launch time and responsiveness. We found that DigitalOcean "dedicated CPU" is noticably faster (more than 2x) in both areas than "shared CPU".
It is really important to make sure that you will be able to resize your server later, adding/removing memory as needed, to minimize your costs. For DigitalOcean, we have a specific tip: always start with the smallest possible droplet (512MB or 1000MB), and then resize memory/CPU to fit your needs, without resizing the disk. When resizing, DigitalOcean does not allow shrinking the disk size.
sudo apt-get update
sudo apt-get upgrade
You should run systemd --version
to verify that we have version 230 or higher.
This script assumes that you have a 64-bit x86 computer. If not, edit the script where necessary, or install Julia in another way.
# You can edit me: The Julia version (1.10.5) split into three parts:
JULIA_MAJOR_VERSION=1
JULIA_MINOR_VERSION=10
JULIA_PATCH_VERSION=5
JULIA_VERSION="$(echo $JULIA_MAJOR_VERSION).$(echo $JULIA_MINOR_VERSION).$(echo $JULIA_PATCH_VERSION)"
wget https://julialang-s3.julialang.org/bin/linux/x64/$(echo $JULIA_MAJOR_VERSION).$(echo $JULIA_MINOR_VERSION)/julia-$(echo $JULIA_VERSION)-linux-x86_64.tar.gz
tar -xvzf julia-$JULIA_VERSION-linux-x86_64.tar.gz
rm julia-$JULIA_VERSION-linux-x86_64.tar.gz
sudo ln -s `pwd`/julia-$JULIA_VERSION/bin/julia /usr/local/bin/julia
Now, the julia
command should be available. Log out and log in, and type julia --version
in the terminal, and you should see something!
cd ~
git clone https://github.com/<user>/<repo-with-notebooks>
cd <repo-with-notebooks>
git pull
TEMPFILE=$(mktemp)
cat > $TEMPFILE << __EOF__
[Unit]
After=network.service
StartLimitIntervalSec=500
StartLimitBurst=5
[Service]
ExecStart=/usr/local/bin/pluto-slider-server.sh
Restart=always
RestartSec=5
User=$(whoami)
Group=$(id -gn)
[Install]
WantedBy=default.target
__EOF__
sudo mv $TEMPFILE /etc/systemd/system/pluto-server.service
This script uses whoami
and id -gn
to automatically insert your username an group name into the configuration file. We want to run the PlutoSliderServer as your user, not as root.
TEMPFILE=$(mktemp)
cat > $TEMPFILE << __EOF__
#!/bin/bash
# this env var allows us to side step various issues with the Julia-bundled git
export JULIA_PKG_USE_CLI_GIT=true
cd /home/<your-username>/<your-repo> # Make sure to change to the absolute path to your repository. Don't use ~.
julia --project="pluto-slider-server-environment" -e "import Pkg; Pkg.instantiate(); import PlutoSliderServer; PlutoSliderServer.run_git_directory(\".\")"
__EOF__
sudo mv $TEMPFILE /usr/local/bin/pluto-slider-server.sh
Tip
If you want to use GLMakie, it might be necessary to set a display. In the script above, replace julia
with
DISPLAY=:0 xvfb-run -a -n 100 -s '-screen 0 1024x768x24' julia
sudo chmod 744 /usr/local/bin/pluto-slider-server.sh
sudo chmod 664 /etc/systemd/system/pluto-server.service
sudo systemctl daemon-reload
sudo systemctl start pluto-server
sudo systemctl enable pluto-server
Tip
If you need to change the service file or the startup script later, re-run this step to update the daemon.
Your server should be running now!! The next steps will explain how to monitor your server, and where to see the notebooks in action.
# To see quick status (running/failed and memory):
systemctl -l status pluto-server
# To browse past logs:
sudo journalctl --pager-end -u pluto-server
# To see logs coming in live:
sudo journalctl --follow -u pluto-server
Important
These three commands are important! Write them down somewhere.
TODO
When you change the notebooks in the git repository, your server will automatically update (PlutoSliderServer keeps calling git pull
every couple of seconds)!
When a notebook file changes, PlutoSliderServer will shut down the old version of that notebook, and start the new one. When notebook files are added or removed, they are added or removed from the server. If you have a lot of notebooks, this is a big speedup compared to always having to restart the entire server when a notebook file changes.
When the configuration file (PlutoDeployment.toml
) changes, PlutoSliderServer will detect a change in configuration and shut down. Because we set up our service using systemctl
, the server will automatically restart. (With the new settings)
Yay! If everything went well, we now set up a web server with PlutoSliderServer. To see the result, you open a browser and go to the URL of your server. This looks like:
http://12.34.56.78:8080/
where 12.34.56.78
is the IP address of your server. You should see an index of your notebooks, and clicking on a notebook should give an interactive page!
The default settings will serve Pluto on http://12.34.56.78:8080/
: with http
(not https
), at the IP address (not a domain like example.com
), on port 8080 (not 80 or 443).
This Part 3 explains how to get https, domain and port 80. But you don't always need this! If http://12.34.56.78:8080/
works for your application, then you are done, and the proud owner of a new PlutoSliderServer! Following Part 3 might add unnecessary complexity.
The main reason why these steps can be useful is to get https. This is necessary when:
- Using camera, microphone or GPS inputs in your notebook (e.g.
PlutoUI.WebcamInput
). - You have an advanced setup: a static PlutoSliderServer that deploys to Netlify/GHPages, combined with a dynamic PlutoSliderServer that only does the bonds, and they are linked together. Your static site is only allowed to make requests to the dynamic server if both are on
https
(this is CORS).
The easiest way to get https
is with cloudflare, and you can only get cloudflare if you have a domain, and a server on port 80.
Note
If you use a server managed by your university/company, ask your system administrator how to achieve these steps. You can probably get a domain like https://myproject.mit.edu/
.
You need to buy a domain name (like mycoolwebsite.org
). The easiest place to buy a domain name is njal.la, but most registrars will work (namecheap.com works).
On the website of your registrar (where you bought the domain), go to the DNS settings. Set an "A record" that points to your IP address.
You can now access your PlutoSliderServer at http://mydomain.org:8080/
. Nice!
Tip
In step 3 we set up https with cloudflare. If this is what you want, then you could set this up directly. That means that you don't set the A record on your registrar site, but you tell your registrar to use Cloudflare DNS, and you set the A record in cloudflare.
We don't want everyone to add :8080
to the URL! The default port for http is 80, so we want our website to be available at port 80.
The tricky thing is: we don't want to run PlutoSliderServer directly on port 80, because this requires sudo
privileges for running julia
. We want to avoid this because we don't want julia
to read/write files as root
(this would mess up your git directory).
The solution is to run PlutoSliderServer on port 8080, and use a separate server (running as root) to redirect traffic from port 80 to port 8080. We use nginx
for that!
sudo apt install nginx
nginx is now installed and it is configured to run at startup.
Let's configure nginx as a redirect from port 80 to port 8080.
TEMPFILE=$(mktemp)
cat > $TEMPFILE << __EOF__
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
proxy_pass http://localhost:8080;
}
}
__EOF__
sudo mv $TEMPFILE /etc/nginx/sites-available/default
After changing configuration, restart nginx:
sudo systemctl restart nginx
The easiest way to get https is to use cloudflare. Register an account, set up your domain to use their DNS (check the cloudflare docs), make sure the A record is there, and enable the "Always HTTPS" service. (Cloudflare is also very useful for caching! This will make your PlutoSliderServer faster.)
Alternatively, you can set up HTTPS yourself with nginx
and Let's Encrypt, but this is beyond the scope of this tutorial. 💛
Now, your service should be available at https://yourdomain.org/
. Nice!
There are many packages that evaluate literate Julia documents to generate HTML or PDF output!
The most similar project is PlutoStaticHTML.jl. This package generates static HTML files from Pluto notebooks, meaning that they do not require JavaScript to load: cell inputs and outputs are stored directly as HTML. (PlutoSliderServer.jl uses the same technique as the "Export to HTML" button inside Pluto: an HTML file is generated with no contents, but with an embedded data stream containing the editor state. This HTML file loads Pluto's JS assets and displays this state just like the editor would.)
This means that the output of PlutoSliderServer.jl will look exactly the same as what you see while writing the notebook. Output from PlutoStaticHTML.jl is more minimal, which means that it loads faster, it can be styled with CSS, and it can more easily be embedded within other web pages (like Documenter.jl sections).
Other Julia packages which export to HTML/PDF, but not necessarily with Pluto notebook files as input, include:
- Documenter.jl
- Franklin.jl
- Books.jl
- Weave.jl
PlutoSliderServer is the only package that lets you run a slider server for Pluto notebooks (an interactive site to interact with a Pluto notebook through @bind
).
There are alternatives for running a Julia-backed interactive site if your code is not a Pluto notebook, including JSServe.jl, Stipple.jl and Dash.jl, each with their own philosophy and ideal use case. (Feel free to suggest others!)
PlutoStaticHTML.jl should also have this feature in the future, after it is added to PlutoSliderServer (it is still being worked on).
If you code is not a Pluto notebook, then JSServe.jl also has precomputing abilities, with a different approach and philosophy.
Since this server is a new and experimental concept, we highly recommend that you run it inside an isolated environment. While visitors are not able to change the notebook code, it is possible to manipulate the API to set bound values to arbitrary objects. For example, when your notebook uses @bind x Slider(1:10)
, the API could be used to set the x
to 9000
, [10,20,30]
or "👻"
.
In the future, we are planning to implement a hook that allows widgets (such as Slider
) to validate a value before it is run: AbstractPlutoDingetjes.Bonds.validate_value
.
Of course, we are not security experts, and this software does not come with any kind of security guarantee. To be completely safe, assume that someone who can visit the server can execute arbitrary code in the notebook, despite our measures to prevent it. Run PlutoSliderServer in a containerized environment.