broken for Julia repos?

I’ve been using GitHub - mitmath/binder-env: Binder environments for MIT math courses for several of my courses (as a backup for the small number of students who have trouble installing Julia on their own computers), and I recently tried to update it for Julia 1.10. The build crashed. Then I tried for Julia 1.9.4, and the build crashed again. (Even if I prune the package listing.) Then I reverted to the old Julia 1.8.1 environment, for which a cached image already exists, and hangs forever trying to spin it up.

Has anyone been successful with recently? Is there a good alternative?

1 Like

I’ve recently been setting up binder as a backup option for an upcoming class. Binder isn’t broken, but it often times out before the image is created, even with the stripped down image I’ve been working with (it pushes about 600Mb of 2GB limit) I’ve also come to the belief that it is better at certain times of the day (not the afternoon) but that might just be coincidence. When it loads, the notebooks are still very useful.

Is there a way to create an image locally and upload it?

I don’t know, but it would be really great if so.

Apparently there is some support for this although it sounds complicated; it seems like you then post on DockerHub. Other people recommend installing the repo2docker github action, so I’ll try that next. It seems like you should also be able to run repo2docker locally and upload to DockerHub.

If Docker Hub isn’t required and you can use any docker registry, you may consider using GitHub Container Registry and upload your container as a GitHub Package (this can be automated with the docker/build-push-action workflow, which works with any registry), if your code is already in GitHub anyway

Can those still be launched from or similar services? (The whole point of this is to ensure that students don’t need to install anything but a web browser.)

I stripped down the package listing and tried it on a Saturday morning, and the image finally built, but now I’m getting repeated “Launch attempt 1 failed, retrying…” messages.

It’s pretty frustrating — this repo and workflow has worked for four years, and suddenly broke with no explanation. (I’ve been pushing MIT to set up their own hub so that we wouldn’t be reliant on free services like mybinder, but I can’t get anyone to step up).

Success! After multiple attempts, it suddenly launched! (I just hope it doesn’t unexpectedly break again the night before the homework is due.)

1 Like

Option 1: The Data Science Dev Containers (requires VS Code)
:point_right: For local use, on a remote SSH host or with GitHub Codespaces.
:information_source: Codespaces requires a web browser only.

Option 2: The JupyterLab Julia docker stack (requires Docker)
:point_right: See for the differences to the Jupyter Docker Stacks.

Option 3: (requires web browser)
:point_right: Julia latest; Resources limited to 2 cores and 8 GB memory.


I would be willing to pay for a service that implements this on a reliable basis. by @schlichtanders comes the closest to doing this at 36.15€* per month
(or 0.0578€ per hour) for multiple users using Pluto.jl notebooks. I wonder if his service might be helpful.

1 Like

IMHO an institution like the MIT should provide this on-premise for its students.

It is not that hard… There is even a deployment template for use with docker. uses a slightly modified version of the template:

# Configuration file for jupyterhub.

import os
import sys

# JupyterHub(Application) configuration
## An Application for starting a Multi-User Jupyter Notebook server.

## Grant admin users permission to access single-user servers.
#  Users should be properly informed if this is enabled.
#  Default: False
c.JupyterHub.admin_access = True

## Allow named single-user servers per user
#  Default: False
# c.JupyterHub.allow_named_servers = False

## Class for authenticating users.
#          This should be a subclass of :class:`jupyterhub.auth.Authenticator`
#          with an :meth:`authenticate` method that:
#          - is a coroutine (asyncio or tornado)
#          - returns username on success, None on failure
#          - takes two arguments: (handler, data),
#            where `handler` is the calling web.RequestHandler,
#            and `data` is the POST form data from the login page.
#          .. versionchanged:: 1.0
#              authenticators may be registered via entry points,
#              e.g. `c.JupyterHub.authenticator_class = 'pam'`
#  Currently installed: 
#    - auth0: oauthenticator.auth0.Auth0OAuthenticator
#    - azuread: oauthenticator.azuread.AzureAdOAuthenticator
#    - bitbucket: oauthenticator.bitbucket.BitbucketOAuthenticator
#    - cilogon: oauthenticator.cilogon.CILogonOAuthenticator
#    - generic-oauth: oauthenticator.generic.GenericOAuthenticator
#    - github: oauthenticator.github.GitHubOAuthenticator
#    - gitlab: oauthenticator.gitlab.GitLabOAuthenticator
#    - globus: oauthenticator.globus.GlobusOAuthenticator
#    - google:
#    - local-auth0: oauthenticator.auth0.LocalAuth0OAuthenticator
#    - local-azuread: oauthenticator.azuread.LocalAzureAdOAuthenticator
#    - local-bitbucket: oauthenticator.bitbucket.LocalBitbucketOAuthenticator
#    - local-cilogon: oauthenticator.cilogon.LocalCILogonOAuthenticator
#    - local-generic-oauth: oauthenticator.generic.LocalGenericOAuthenticator
#    - local-github: oauthenticator.github.LocalGitHubOAuthenticator
#    - local-gitlab: oauthenticator.gitlab.LocalGitLabOAuthenticator
#    - local-globus: oauthenticator.globus.LocalGlobusOAuthenticator
#    - local-google:
#    - local-okpy: oauthenticator.okpy.LocalOkpyOAuthenticator
#    - local-openshift: oauthenticator.openshift.LocalOpenShiftOAuthenticator
#    - mediawiki: oauthenticator.mediawiki.MWOAuthenticator
#    - okpy: oauthenticator.okpy.OkpyOAuthenticator
#    - openshift: oauthenticator.openshift.OpenShiftOAuthenticator
#    - default: jupyterhub.auth.PAMAuthenticator
#    - dummy: jupyterhub.auth.DummyAuthenticator
#    - pam: jupyterhub.auth.PAMAuthenticator
#  Default: 'jupyterhub.auth.PAMAuthenticator'
from oauthenticator.github import GitHubOAuthenticator
c.JupyterHub.authenticator_class = GitHubOAuthenticator
c.GitHubOAuthenticator.oauth_callback_url = 'https://{subdomain}.{domain}/hub/oauth_callback'.format(
    subdomain = os.environ['JUPYTERHUB_SUBDOMAIN'],
    domain = os.environ['JUPYTERHUB_DOMAIN']

## Whether to shutdown single-user servers when the Hub shuts down.
#          Disable if you want to be able to teardown the Hub while leaving the
#  single-user servers running.
#          If both this and cleanup_proxy are False, sending SIGINT to the Hub will
#          only shutdown the Hub, leaving everything else running.
#          The Hub should be able to resume from database state.
#  Default: True
c.JupyterHub.cleanup_servers = False

## url for the database. e.g. `sqlite:///jupyterhub.sqlite`
#  Default: 'sqlite:///jupyterhub.sqlite'
c.JupyterHub.db_url = 'postgresql://postgres:{password}@{host}:5432/{db}'.format(
    host = os.environ['POSTGRES_HOST'],
    password = os.environ['POSTGRES_PASSWORD'],
    db = os.environ['POSTGRES_DB']

## The ip address for the Hub process to *bind* to.
#  By default, the hub listens on localhost only. This address must be accessible
#  from the proxy and user servers. You may need to set this to a public ip or ''
#  for all interfaces if the proxy or user servers are in containers or on a
#  different host.
#  See `hub_connect_ip` for cases where the bind and connect address should
#  differ, or `hub_bind_url` for setting the full bind URL.
#  Default: ''
c.JupyterHub.hub_ip = ''

## Maximum number of concurrent named servers that can be created by a user at a
#  time.
#  Setting this can limit the total resources a user can consume.
#  If set to 0, no limit is enforced.
#  Default: 0
# c.JupyterHub.named_server_limit_per_user = 0

## List of service specification dictionaries.
#  A service
#  For instance::
#      services = [
#          {
#              'name': 'cull_idle',
#              'command': ['/path/to/'],
#          },
#          {
#              'name': 'formgrader',
#              'url': '',
#              'api_token': 'super-secret',
#              'environment':
#          }
#      ]
#  Default: [] = [
        'name': 'idle-culler',
        'admin': True,
        'command': [
            '-m', 'jupyterhub_idle_culler',

## Shuts down all user servers on logout
#  Default: False
# c.JupyterHub.shutdown_on_logout = False

## The class to use for spawning single-user servers.
#          Should be a subclass of :class:`jupyterhub.spawner.Spawner`.
#          .. versionchanged:: 1.0
#              spawners may be registered via entry points,
#              e.g. `c.JupyterHub.spawner_class = 'localprocess'`
#  Currently installed: 
#    - docker: dockerspawner.DockerSpawner
#    - docker-swarm: dockerspawner.SwarmSpawner
#    - docker-system-user: dockerspawner.SystemUserSpawner
#    - default: jupyterhub.spawner.LocalProcessSpawner
#    - localprocess: jupyterhub.spawner.LocalProcessSpawner
#    - simple: jupyterhub.spawner.SimpleLocalProcessSpawner
#  Default: 'jupyterhub.spawner.LocalProcessSpawner'
c.JupyterHub.spawner_class = 'dockerspawner.SwarmSpawner'

# Spawner(LoggingConfigurable) configuration

## Override escaping with any callable of the form escape(str)->str
#  This is used to ensure docker-safe container names, etc.
#  The default escaping should ensure safety and validity,
#  but can produce cumbersome strings in cases.
#  Set c.DockerSpawner.escape = 'legacy' to preserve the earlier, unsafe behavior
#  if it worked for you.
#  .. versionadded:: 12.0
#  .. versionchanged:: 12.0
#      Escaping has changed in 12.0 to ensure safety,
#      but existing deployments will get different container and volume names.

## Maximum number of cpu-cores a single-user notebook server is allowed to use.
#  If this value is set to 0.5, allows use of 50% of one CPU. If this value is
#  set to 2, allows use of up to 2 CPUs.
#  The single-user notebook server will never be scheduled by the kernel to use
#  more cpu-cores than this. There is no guarantee that it can access this many
#  cpu-cores.
#  **This is a configuration setting. Your spawner must implement support for the
#  limit to work.** The default spawner, `LocalProcessSpawner`, does **not**
#  implement this support. A custom spawner **must** add support for this setting
#  for it to be enforced.
#  Default: None
c.Spawner.cpu_limit = 2

## The URL the single-user server should start in.
#  `{username}` will be expanded to the user's username
#  Example uses:
#  - You can set `notebook_dir` to `/` and `default_url` to
#    `/tree/home/{username}` to allow people to navigate the whole filesystem
#    from their notebook server, but still start in their home directory.
#  - Start with `/notebooks` instead of `/tree` if `default_url` points to a
#    notebook instead of a directory.
#  - You can set this to `/lab` to have JupyterLab start by default, rather
#    than Jupyter Notebook.
#  Default: ''
c.Spawner.default_url = '/lab'

## Extra environment variables to set for the single-user server's process.
#  Environment variables that end up in the single-user server's process come from 3 sources:
#    - This `environment` configurable
#    - The JupyterHub process' environment variables that are listed in `env_keep`
#    - Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN)
#  The `environment` configurable should be set by JupyterHub administrators to
#  add installation specific environment variables. It is a dict where the key is
#  the name of the environment variable, and the value can be a string or a
#  callable. If it is a callable, it will be called with one parameter (the
#  spawner instance), and should return a string fairly quickly (no blocking
#  operations please!).
#  Note that the spawner class' interface is not guaranteed to be exactly same
#  across upgrades, so if you are using the callable take care to verify it
#  continues to work after upgrades!
#  .. versionchanged:: 1.2
#      environment from this configuration has highest priority,
#      allowing override of 'default' env variables,
#      such as JUPYTERHUB_API_URL.
#  Default: {}
c.Spawner.environment = {
    'LANGS': 'de_CH.UTF-8 fr_CH.UTF-8 it_CH.UTF-8',
    'SWAP_ENABLE': '1',
    'SWAP_FACTOR': '0.5',
    'TZ': 'Europe/Zurich'

## Timeout (in seconds) before giving up on a spawned HTTP server
#  Once a server has successfully been spawned, this is the amount of time we
#  wait before assuming that the server is unable to accept connections.
#  Default: 30
# c.Spawner.http_timeout = 30

## Maximum number of bytes a single-user notebook server is allowed to use.
#  Allows the following suffixes:
#    - K -> Kilobytes
#    - M -> Megabytes
#    - G -> Gigabytes
#    - T -> Terabytes
#  If the single user server tries to allocate more memory than this, it will
#  fail. There is no guarantee that the single-user notebook server will be able
#  to allocate this much memory - only that it can not allocate more than this.
#  **This is a configuration setting. Your spawner must implement support for the
#  limit to work.** The default spawner, `LocalProcessSpawner`, does **not**
#  implement this support. A custom spawner **must** add support for this setting
#  for it to be enforced.
#  Default: None
c.Spawner.mem_limit = '8G'

## Path to the notebook directory for the single-user server.
#  The user sees a file listing of this directory when the notebook interface is
#  started. The current interface does not easily allow browsing beyond the
#  subdirectories in this directory's tree.
#  `~` will be expanded to the home directory of the user, and {username} will be
#  replaced with the name of the user.
#  Note that this does *not* prevent users from accessing files outside of this
#  path! They can do so with many other means.
#  Default: ''
notebook_dir = os.environ.get('DOCKER_NOTEBOOK_DIR') or '/home/{raw_username}'
c.Spawner.notebook_dir = notebook_dir

## An optional hook function that you can implement to do some bootstrapping work
#  before the spawner starts. For example, create a directory for your user or
#  load initial content.
#  This can be set independent of any concrete spawner implementation.
#  This maybe a coroutine.
#  Example::
#      from subprocess import check_call
#      def my_hook(spawner):
#          username =
#          check_call(['./examples/bootstrap-script/', username])
#      c.Spawner.pre_spawn_hook = my_hook
#  Default: None
def set_username(spawner):
    username =
    spawner.environment['NB_USER'] = username

c.Spawner.pre_spawn_hook = set_username

## Timeout (in seconds) before giving up on starting of single-user server.
#  This is the timeout for start to return, not the timeout for the server to
#  respond. Callers of spawner.start will assume that startup has failed if it
#  takes longer than this. start should return when the server process is started
#  and its location is known.
#  Default: 60
# c.Spawner.start_timeout = 60

# DockerSpawner(LoggingConfigurable) configuration
# See

## The image to use for single-user servers.
#  This image should have the same version of jupyterhub as the Hub itself
#  installed.
#  If the default command of the image does not launch jupyterhub-singleuser,
#  set `c.Spawner.cmd` to launch jupyterhub-singleuser, e.g.
#  Any of the jupyter docker-stacks should work without additional config, as
#  long as the version of jupyterhub in the image is compatible.
#  Default: 'jupyterhub/singleuser:1.3'
c.Spawner.allowed_images = {
    'R (jupyterlab/r/verse:latest)': '',
    'R (jupyterlab/r/geospatial:latest)': '',
    'R (jupyterlab/r/qgisprocess:latest)': '',
    'Python (jupyterlab/python/scipy:latest)': '',
    'Julia (jupyterlab/julia/pubtools:latest)': ''

## Run the containers on this docker network.
#  If it is an internal docker network, the Hub should be on the same network,
#  as internal docker IP addresses will be used. For bridge networking,
#  external ports will be bound.
#  Default: 'bridge'
c.Spawner.network_name = os.environ['DOCKER_NETWORK_NAME']

## Prefix for container names.
#  See name_template for full container name for a particular user’s server.
#  Default: 'jupyter'
c.Spawner.prefix = 'jupyterlab-demo'

## If True, delete containers when servers are stopped.
#  This will destroy any data in the container not stored in mounted volumes.
#  Default: False
c.Spawner.remove = True

## Map from host file/directory to container (guest) file/directory mount point
## and (optionally) a mode.
#  When specifying the guest mount point (bind) for the volume, you may use a
#  dict or str. If a str, then the volume will default to a read-write
#  (mode="rw"). With a dict, the bind is identified by "bind" and the "mode"
#  may be one of "rw" (default), "ro" (read-only), "z" (public/shared SELinux
#  volume label), and "Z" (private/unshared SELinux volume label).
#  If format_volume_name is not set, default_format_volume_name is used for
#  naming volumes. In this case, if you use {username} in either the host or
#  guest file/directory path, it will be replaced with the current user’s name.
#  Default: {}
c.Spawner.volumes = { 'jupyterhub-user-{username}': notebook_dir }

# Authenticator(LoggingConfigurable) configuration
## Base class for implementing an authentication provider for JupyterHub

## Set of users that will have admin rights on this JupyterHub.
#  Admin users have extra privileges:
#   - Use the admin panel to see list of users logged in
#   - Add / remove users in some authenticators
#   - Restart / halt the hub
#   - Start / stop users' single-user servers
#   - Can access each individual users' single-user server (if configured)
#  Admin access should be treated the same way root access is.
#  Defaults to an empty set, in which case no user has admin access.
#  Default: set()
c.Authenticator.admin_users = { 'benz0li' }


version: '3.9'

      - DOCKER_NETWORK_NAME=jupyter-demo
      #- GITLAB_URL=https://gitlab.${GL_DOMAIN}
      - GITHUB_CLIENT_ID=[redacted]
      - GITHUB_CLIENT_SECRET=[redacted]
      - POSTGRES_HOST=jupyterhub-postgres

with and JH_SUBDOMAIN=demo.jupyter set in .env.

1 Like

What makes the JupyterLab Julia docker stack different:

  1. Multi-arch: linux/amd64, linux/arm64/v8
    :information_source: Runs on Apple M series using Docker Desktop.
  2. Base image: Debian instead of Ubuntu
    :information_source: CUDA-enabled images are Ubuntu-based.
  3. IDE: code-server next to JupyterLab
    :information_source: code-server = VS Code in the browser.
  4. Just Python – no Conda / Mamba

:point_right: See the CUDA-enabled JupyterLab Julia docker stack for GPU accelerated docker images.

Thank you very much @mkitti for the ping.

I think the binder setup is indeed the best to keep for your usecase.
(It mainly targets academic usages).

( is more for industry and companies who have teams which work together on the same code.)

@stevengj if you need some freelancer to help you setup your own binder on your custom infrastructure, I can help you. Just drop me a private message.

I agree. I’ve been bugging various committees at MIT about this for years, but so far no one wants to be the one to pay to set it up. (It’s not something an individual faculty member, or arguably even an individual department, can or should fund, but the higher up you go the harder it is to get anyone to make a decision.)

1 Like

Setting up a BinderHub on Kubernetes:

This above guide assumes experience with setting up a JupyterHub on Kubernetes:

@stevengj , I was having a conversation with Blaec from @cocalc . I’m wondering if this might fit your needs?

I was talking to them about a JuliaCon sponsorship, so I would appreciate if you could take a look.

For a large course like linear algebra this costs thousands of dollars per month according to their site (to accommodate the worst case of all students using it simultaneously, though in practice most students seem able to install Julia locally). I don’t get a budget like this for a course at MIT (short of going through an onerous grant-application process). I guess if I were more optimistic about the number of students needing it simultaneously I could reduce it to a few hundred per month, though even then I would need to jump through hoops to squeeze the funding out of MIT.

I will look into it further. It looks like there is some free capability that might be good enough for a demonstration.

I thought this was only for a small number of students who ran into trouble on their own computers or for short demos?

The other thing I noticed is there is also a capability to have students pay for their resources.

After creating a free account on, I was able to create a Pluto.jl notebook. You can also boot up Jupyter Classic, JupyterLab, or VS code.

Here is the output of versioninfo():

Julia Version 1.10.0
Commit 3120989f39b (2023-12-25 18:01 UTC)
Build Info:
  Official release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × Intel(R) Xeon(R) CPU @ 2.80GHz
  LIBM: libopenlibm
  LLVM: libLLVM-15.0.7 (ORCJIT, cascadelake)
  Threads: 2 on 4 virtual cores
  JULIA_DEPOT_PATH = /home/user/.julia:/ext/julia/depot/
  JULIA_LOAD_PATH = /tmp/jl_OnyX4I:@:@v#.#:@stdlib

The main disability of the free account is the lack of Internet access from cloud machine. Thus, I needed to switch Pluto to use the default environment as follows.

	using Pkg
	Pkg.activate("v1.10", shared=true)

The output is Pkg.status() is

  Activating project at `/ext/julia/depot/environments/v1.10`
Status `/ext/julia/depot/environments/v1.10/Project.toml`
⌃ [a134a8b2] BlackBoxOptim v0.5.0
⌃ [764a87c0] BoundaryValueDiffEq v2.8.0
⌅ [336ed68f] CSV v0.7.10
  [159f3aea] Cairo v1.0.5
  [49dc2e85] Calculus v0.5.1
⌅ [324d7699] CategoricalArrays v0.8.3
⌅ [aaaa29a8] Clustering v0.14.4
  [35d6a980] ColorSchemes v3.24.0
  [5ae59095] Colors v0.12.10
  [861a8166] Combinatorics v1.0.2
⌅ [34da2185] Compat v3.47.0
  [a81c6b42] Compose v0.9.5
  [f65535da] Convex v0.15.4
  [150eb455] CoordinateTransformations v0.6.3
⌃ [3b531cbf] DataConvenience v0.2.0
⌅ [a93c6f00] DataFrames v0.21.8
⌃ [1313f7d8] DataFramesMeta v0.7.1
  [7806a523] DecisionTree v0.12.4
⌅ [31c24e10] Distributions v0.23.12
⌅ [7c1d4256] DynamicPolynomials v0.4.6
  [4f61f5a4] FFTViews v0.3.2
  [7a1cc6ca] FFTW v1.7.2
  [e6aeac8e] FastGroupBy v0.2.6
  [becb17da] Feather v0.5.10
  [5789e2e9] FileIO v1.16.1
  [186bb1d3] Fontconfig v0.4.1
  [f6369f11] ForwardDiff v0.10.36
  [da1fdf0e] FreqTables v0.4.6
  [38e38edf] GLM v1.9.0
⌃ [c91e804a] Gadfly v1.3.3
  [a2cc645c] GraphPlot v0.5.2
  [42e2da0e] Grisu v1.0.2
⌃ [f213a82b] HomotopyContinuation v2.6.4
  [7073ff75] IJulia v1.24.2
⌃ [2803e5a7] ImageAxes v0.6.9
  [4381153b] ImageDraw v0.2.6
  [92ff4b2b] ImageFeatures v0.5.2
⌅ [6a3955dd] ImageFiltering v0.6.21
  [6218d12a] ImageMagick v1.3.0
⌃ [bc367c6b] ImageMetadata v0.9.5
⌅ [787d08f9] ImageMorphology v0.2.11
⌃ [80713f31] ImageSegmentation v1.4.8
⌅ [916415d5] Images v0.23.3
  [c601a237] Interact v0.10.5
  [b6b21f68] Ipopt v1.5.1
⌃ [babc3d20] JDF v0.3.0
⌃ [13d6d4a1] JLBoost v0.1.12
  [682c06a0] JSON v0.21.4
  [0f8b85d8] JSON3 v1.14.0
  [4076af6c] JuMP v1.17.0
  [a5e1c1ea] LatinHypercubeSampling v1.9.0
  [50d2b5c4] Lazy v0.15.1
  [093fc24a] LightGraphs v1.3.5
⌅ [e1d29d7a] Missings v0.4.5
  [76087f3c] NLopt v1.0.0
  [2774e3e8] NLsolve v4.5.1
  [429524aa] Optim v1.7.8
  [46a55296] ParquetFiles v0.2.0
⌃ [91a5bcdd] Plots v1.35.0
  [c3e4b0f8] Pluto v0.19.36
  [2fc8631c] PlutoSliderServer v0.3.28
⌃ [7f904dfe] PlutoUI v0.7.1
  [27ebfcd6] Primes v0.5.5
  [438e738f] PyCall v1.96.4
  [d330b81b] PyPlot v2.11.2
  [ce6b1742] RDatasets v0.7.7
  [37e2e3b7] ReverseDiff v1.15.1
  [f2b01f46] Roots v2.0.22
⌅ [0bca4576] SciMLBase v1.81.0
  [562c1548] SortingLab v0.2.8
  [928aab9d] SpecialMatrices v3.0.0
⌅ [2913bbd2] StatsBase v0.33.21
⌃ [2cb19f9e] StatsKit v0.3.0
⌃ [f3b207a7] StatsPlots v0.14.32
  [24249f21] SymPy v2.0.1
  [40c74d1a] TableView v0.7.2
  [bd369af6] Tables v1.11.1
  [5e47fb64] TestImages v1.8.0
⌃ [b8865327] UnicodePlots v2.12.4
⌃ [0f1e0344] WebIO v0.8.17
  [ddb6d928] YAML v0.4.9
  [a5390f91] ZipFile v0.10.1
  [37e2e46d] LinearAlgebra
  [9a3f8284] Random
  [10745b16] Statistics v1.10.0
Info Packages marked with ⌃ and ⌅ have new versions available. Those with ⌃ may be upgradable, but those with ⌅ are restricted by compatibility constraints from upgrading. To see why use `status --outdated`

This was sufficient to run the following code and generate a plot as follows.

Finally, I could “publish” the Pluto.jl notebook at the following URL, enabling others to easily run it:

Besides the lack of internet access with a free account, another major limitation is only having 1 GB of RAM. For Julia, this can be critical since precompilation of a package like Makie.jl can easily exceed that RAM requirement.