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!
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: JuliaPluto#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
.
PlutoCon 2020 presentation about how PlutoSliderServer works
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 is open to the web
The easiest way to get this is to rent a server from digitalocean.com, AWS, Google Cloud, etc. This setup was tested with digitalocean.com, which has the easiest 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.
# You can edit me: The Julia version (1.8.0) split into three parts:
JULIA_MAJOR_VERSION=1
JULIA_MINOR_VERSION=8
JULIA_PATCH_VERSION=0
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
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
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.
# 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
TODO
When you change the notebooks in the git repository, your server will automatically update (it keeps calling git pull
)! Awesome!
If 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)
The default settings will serve Pluto on the IP address of your server, on http
(not https
), on port 8080 (not 80 or 443).
Normally, websites are available on a domain name, on https, on the default port (80 for http, 443 for https) (e.g. https://plutojl.org/
). Here's how you get there!
If you use a server managed by your university/company, ask your system administrator how to achieve these steps.
You need to buy a domain name, and get access 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!
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, use their DNS, 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.