Developer Tools AWS

Polynote 0.7.2 on AWS User Guide

| Product: Polynote 0.7.2 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Polynote 0.7.2 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Polynote is a notebook for Scala and Python together: a value defined in a Scala cell can be read by the Python cells below it, and the other way round. It was written at Netflix for data and machine-learning work on the JVM, and it edits like an IDE: completion and parameter hints as you type, errors marked in the code, and a symbol table that shows every value the notebook has defined. Notebooks are standard .ipynb files; you work in them from any browser while the computation runs on your EC2 instance.

The cloudimg image runs Polynote as a private notebook server:

  • HTTPS from the first boot. nginx serves Polynote on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS, and a request for any name other than your instance's own gets no answer.
  • No default password. The image contains no password, no certificate and no host name. First boot generates them and writes them to /var/lib/cloudimg/polynote-credentials.txt (root only). nginx keeps only a SHA-512 crypt hash of the password, and it asks for the password on every page and on the notebook connection.
  • Polynote's own key stays on. Polynote generates a new random WebSocket key every time it starts and gives it only to the editor page you signed in to; the notebook connection is refused without it. A connection from another website, or one that names no website at all, is refused before the password is even checked.
  • Scala and Python ready to run. Polynote 0.7.2 (the official release, checked against its published SHA-256 digest) on Java 11 from Ubuntu's security-patched packages, with Scala 2.12, and Python 3.12 with numpy, pandas and the jep bridge that lets Scala and Python share values. The Python environment is pinned: 55 packages, listed in the build record.
  • Notebooks that cannot reach the metadata service. Polynote and every notebook kernel run as their own account, polynote, with no login shell and no sudo. A notebook can reach the internet (to fetch Maven or PyPI dependencies, or data) but not the EC2 instance metadata service, so code in a notebook cannot read the instance's IAM role credentials.
  • Requests limited per address, by the address nginx accepted the connection from (a forged X-Forwarded-For header changes nothing).
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Polynote 0.7.2 (the official polynote-dist.tar.gz), OpenJDK 11 (Java runtime only), Python 3.12 with Polynote's Python packages in a read-only environment, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The build record, the list of every jar and Python package, and Polynote's licence are in /usr/share/doc/cloudimg-polynote/.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
  • A security group that allows TCP 443 (notebooks), TCP 80 (redirects to HTTPS) and TCP 22 (SSH). A notebook runs any Scala or Python code you type, so open 443 and 22 only to your own networks (for example <your-mgmt-cidr>), never to the whole internet if you can avoid it.
  • A modern browser (Chrome, Edge, Firefox or Safari).

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Polynote 0.7.2 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (8 GiB: the Polynote server is a Java process, and every open notebook starts its own Java process for its kernel; use a larger size if you keep several notebooks open or work with large data), your key pair, your subnet and the security group above.

Your notebooks, and the dependencies they download, live on the root volume, in /var/lib/polynote. The image's root volume is 16 GiB; give it more at launch if your notebooks will use large dependencies or data sets (for example 50 GiB).

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":50,"VolumeType":"gp3"}}]' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=polynote}]'

An Elastic IP keeps the instance's address, and so its name and certificate, the same across stops and starts.

Step 2: Connect to your instance

Version SSH user
Polynote 0.7.2 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

The login message shows your notebook server's address and where its password is.

Step 3: Check the services

First boot takes under a minute: it sets the server's name, generates the certificate and the password, puts a welcome notebook in place, and only then starts Polynote and nginx. Polynote itself takes a few more seconds to start. Check that everything is running:

systemctl is-active polynote-egress-guard polynote nginx \
  && sudo test -f /var/lib/cloudimg/polynote-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network. Polynote (a java process) listens on the loopback address 127.0.0.1:8192 behind nginx (ss shows it as [::ffff:127.0.0.1]); while a notebook is open, Polynote and that notebook's kernel, a separate java process, also talk over loopback ports of their own:

sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u | tee /dev/stderr \
  | awk '$1 !~ /^(127\.|\[::ffff:127\.|\[::1\])/ {sub(/.*:/, "", $1); print $1}' | sort -un | xargs | grep -qx '22 80 443' && echo "network listeners: 22 80 443"
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
[::ffff:127.0.0.1]:8192 users:(("java"))
network listeners: 22 80 443

Step 4: Get your address and password

Run sudo cat /var/lib/cloudimg/polynote-credentials.txt to see your notebook server's address, user name and password. The file is readable by root only and holds these settings, all generated for this instance:

sudo sed -n 's/^\(POLYNOTE_[A-Z0-9_]*\)=.*/\1/p' /var/lib/cloudimg/polynote-credentials.txt | tee /dev/stderr \
  | grep -qx POLYNOTE_PASSWORD && echo "credentials present"
POLYNOTE_URL
POLYNOTE_HOST
POLYNOTE_USERNAME
POLYNOTE_PASSWORD
POLYNOTE_TLS_CERT
POLYNOTE_TLS_SHA256_FINGERPRINT
credentials present

POLYNOTE_URL is https://<your instance's public DNS name>/, POLYNOTE_USERNAME is admin and POLYNOTE_PASSWORD is the password. The settings that are not secret can be shown on their own:

sudo grep -E '^POLYNOTE_(URL|USERNAME|TLS_SHA256_FINGERPRINT)=' /var/lib/cloudimg/polynote-credentials.txt \
  | tee /dev/stderr | grep -q '^POLYNOTE_URL=https://'
POLYNOTE_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/
POLYNOTE_USERNAME=admin
POLYNOTE_TLS_SHA256_FINGERPRINT=6C:1A:...:9E

nginx stores the password only as a SHA-512 crypt hash, in a file readable only by root and nginx:

sudo grep -c '^admin:\$6\$' /etc/nginx/polynote.htpasswd | grep -qx 1 \
  && sudo stat -c '%U:%G %a' /etc/nginx/polynote.htpasswd | tee /dev/stderr | grep -qx 'root:www-data 640' && echo "hash only"
root:www-data 640
hash only

Step 5: Sign in from your browser

Open POLYNOTE_URL in your browser. The certificate is self-signed and was generated on your instance, so the browser warns about it the first time. Before you accept it, check that it is your instance's certificate: the browser's certificate viewer shows a SHA-256 fingerprint that must match POLYNOTE_TLS_SHA256_FINGERPRINT. Or copy the certificate to your machine and compare it there:

scp -i <your-key.pem> ubuntu@<public-ip>:/etc/ssl/polynote/polynote.crt ./polynote.crt
openssl x509 -in polynote.crt -noout -fingerprint -sha256

To stop the warning altogether, add polynote.crt to your machine's trusted certificates (Keychain Access on macOS, certmgr on Windows), or use your own domain and a public certificate (Step 9).

The browser then asks for a user name and password: enter admin and POLYNOTE_PASSWORD. Polynote opens with its notebook list on the left:

The Polynote home page, served over HTTPS by the instance

The browser remembers the password until you close it. To sign out, close every window of the browser (or use a private window for the session).

Step 6: Work in a notebook

The image puts a short welcome notebook, Welcome.ipynb, in the notebook folder, /var/lib/polynote/notebooks. Click it in the notebook list, then the Run all button (the double arrow at the top left). The first run takes a little longer while the notebook's kernel starts. The Scala cell adds up 1 to 10, and the Python cell below it reads that total straight from Scala:

The welcome notebook: a Scala cell's value read by a Python cell

The + next to Notebooks creates a new notebook. Pick each cell's language in its drop-down (Scala, Python or Text, among others), run a cell with Shift+Enter, and watch the Symbols panel on the right list every value the notebook has defined. Notebooks use Scala 2.12 unless you choose another version in their settings.

Configuration & dependencies, at the top of each notebook, holds that notebook's own settings:

  • Dependencies. Scala dependencies as Maven coordinates (for example org.typelevel::cats-core:2.10.0), fetched from Maven Central; Python dependencies as pip package names, installed into that notebook's own virtual environment. They are downloaded the first time the notebook's kernel starts and kept under /var/lib/polynote.
  • JVM arguments. Options for the notebook's kernel, for example -Xmx4g for a 4 GiB heap. Polynote 0.7.2 takes kernel options only from each notebook's own settings, so set them here; without them Java sizes the heap from the instance's memory.
  • Environment variables for the kernel.

numpy and pandas are installed for every notebook: import numpy, pandas works in a Python cell without adding them as dependencies.

To bring a notebook in from elsewhere, copy the .ipynb file into the notebook folder over SSH (sudo install -o polynote -g polynote -m 0640 my-notebook.ipynb /var/lib/polynote/notebooks/); it appears in the notebook list. The Download button at the top saves the open notebook to your computer.

Step 7: What a notebook can reach

Every notebook kernel runs as the polynote account, inside the Polynote service. It can reach the internet (Maven Central, PyPI, data downloads, APIs) but not the EC2 instance metadata service. The screenshot below is that check, run in a notebook's Scala cell: the metadata service is refused, Maven Central answers, and the code runs as polynote. The Python cell below it gives the same three answers:

A Scala cell: the instance metadata service is refused, Maven Central answers

You can run the same check over SSH. The metadata service is refused for the polynote account and still answers for ubuntu (your SSH account), and Maven Central answers for polynote:

{ sudo -u polynote curl -s -m 5 -o /dev/null -w 'polynote -> metadata: %{http_code}\n' -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60'
  sudo -u polynote curl -sL -m 20 -o /dev/null -w 'polynote -> repo1.maven.org: %{http_code}\n' https://repo1.maven.org/maven2/
  curl -s -m 5 -o /dev/null -w 'ubuntu -> metadata: %{http_code}\n' -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60'
} | tee /dev/stderr | xargs | grep -qx 'polynote -> metadata: 000 polynote -> repo1.maven.org: 200 ubuntu -> metadata: 200' && echo "metadata blocked for notebooks"
polynote -> metadata: 000
polynote -> repo1.maven.org: 200
ubuntu -> metadata: 200
metadata blocked for notebooks

Two things enforce this: the Polynote service's own network filter (systemd IPAddressDeny) and an nftables rule that rejects the metadata addresses for the polynote account anywhere on the instance. If a notebook needs AWS credentials, give it its own, with only the permissions it needs (for example in a file under /var/lib/polynote that only polynote can read).

A notebook can reach your VPC and other private addresses, like any program on the instance; Polynote itself makes no requests on anyone else's behalf, so the image does not fence private networks.

The polynote account has no sudo and cannot write outside /var/lib/polynote. To install system libraries, use your SSH session as ubuntu (sudo apt-get install ...).

Step 8: Run the self-test

The image ships a self-test that checks the services and listening ports, that a request with no password or a wrong one is refused on every path and on the notebook connection (and that nginx refuses a // path), that the password in the credentials file opens Polynote over HTTPS with this instance's certificate, that a notebook connection needs Polynote's key and is refused from another site, and that a Scala cell and a Python cell (with numpy and pandas) compute through Polynote's own notebook connection. It starts a notebook kernel, so it takes a minute or two:

sudo polynote-selftest | tail -n 1 | tee /dev/stderr | grep -q '^POLYNOTE_SELFTEST_OK '
POLYNOTE_SELFTEST_OK no password or a wrong one = 401 everywhere, '//' = 400; the note's password opens https://ec2-203-0-113-10.compute-1.amazonaws.com/; same-origin WebSocket with Polynote's key 101, cross-origin or Origin null 403, without the key 403; a Scala cell and a Python cell (numpy + pandas) computed SELFTEST-SCALA-2 and SELFTEST-PYTHON-2 through the WebSocket API

The self-test creates, runs and deletes its own small notebook; it does not touch yours.

Step 9: Change the password or use your own domain

To give the server a new random password (the old one stops working at once), run the command below, then read the new POLYNOTE_PASSWORD with sudo cat /var/lib/cloudimg/polynote-credentials.txt. Open browser tabs must sign in again; running notebooks keep running:

sudo polynote-rotate-password

The new password goes to the hasher on standard input; only its SHA-512 crypt hash goes into /etc/nginx/polynote.htpasswd, and the password never appears on a command line or in a log.

To use your own domain, point a DNS name at the instance (an Elastic IP keeps it stable), then make it the server's name. This generates a new certificate for that name and pins nginx to it:

sudo polynote-set-site notebooks.example.com

To use a certificate from a public CA instead, for example one issued by certbot with a DNS challenge, run polynote-set-site first, then replace /etc/ssl/polynote/polynote.crt (full chain) and /etc/ssl/polynote/polynote.key (mode 0600) and run sudo systemctl reload nginx. Browsers then show no warning.

Step 10: What the front door refuses

nginx answers only for the server's own names, asks for the password on every path, and passes requests to Polynote on loopback. Below: no password (401), the password (200, read from the credentials file without putting it on a command line), a name that is not the server's (no answer, 000), plain HTTP (301 to HTTPS), a cross-site form post (403) and a notebook connection from another site (403), both refused before the password is even checked:

H="$(sudo sed -n 's/^POLYNOTE_HOST=//p' /var/lib/cloudimg/polynote-credentials.txt)"
C=(--cacert /etc/ssl/polynote/polynote.crt --resolve "$H:443:127.0.0.1")
{ curl -s -o /dev/null -w '%{http_code}\n' "${C[@]}" "https://$H/"
  curl -s -o /dev/null -w '%{http_code}\n' "${C[@]}" -K <(sudo sed -n 's/^POLYNOTE_PASSWORD=\(.*\)$/user = "admin:\1"/p' /var/lib/cloudimg/polynote-credentials.txt) "https://$H/"
  curl -s -o /dev/null -w '%{http_code}\n' -k -H 'Host: someone-else.example' https://127.0.0.1/
  curl -s -o /dev/null -w '%{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
  curl -s -o /dev/null -w '%{http_code}\n' "${C[@]}" -X POST -H 'Origin: https://evil.example' "https://$H/"
  curl -s -o /dev/null -w '%{http_code}\n' --http1.1 "${C[@]}" -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \
    -H "Sec-WebSocket-Key: $(openssl rand -base64 16)" -H 'Origin: https://evil.example' "https://$H/ws"
} | xargs | tee /dev/stderr | grep -qx '401 200 000 301 403 403' && echo "front door OK"
401 200 000 301 403 403
front door OK
  • Polynote's key. On top of the password, Polynote accepts a notebook connection only with the WebSocket key it generated when it started, which it puts in the editor page you open after signing in. The key changes every time Polynote restarts (open tabs reconnect with the new page).
  • Unusual paths. A request whose path contains //, or that climbs above the root with encoded dots, gets 400 from nginx before the password is checked.
  • Cross-site requests. A request that changes something (anything but GET, HEAD or OPTIONS) must come from Polynote's own page, and so must every notebook connection: with another site's Origin header, or none at all, nginx answers 403 before Polynote sees the request.
  • Request limits. Each client address may make 20 requests a second (bursts of 200, since opening Polynote loads many files at once) and hold 128 connections; over the limit nginx answers 429. nginx replaces any X-Forwarded-For header with the address it accepted the connection from and blanks X-Real-IP, Forwarded and similar headers, so a client cannot change the address the limits use. If you put a load balancer in front of the instance, every client then shares the load balancer's address for the limits.
  • Sizes and methods. Polynote takes no large HTTP uploads (notebooks and cell runs travel over the notebook connection), so request bodies are limited to 1 MiB; TRACE and CONNECT get 405.
  • Headers. Responses carry Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, a Content-Security-Policy of frame-ancestors 'self', Referrer-Policy: same-origin and a Permissions-Policy that turns off the payment and USB APIs, and nginx does not advertise its version.

What is logged: nginx's access log (/var/log/nginx/access.log) records the client address, request path and status, without query strings (Polynote's key travels in one). nginx's error log records only start-up problems, not refused requests, so a password in a rejected request never reaches the disk. Polynote logs to the journal (journalctl -u polynote) through a small filter that replaces anything shaped like Polynote's key with <uuid-withheld>, because Polynote writes some request addresses, key included, to its log.

What the editor fetches from the internet: nothing. Polynote's editor page and its scripts, fonts and styles are all served by the instance. Your notebooks, code and data stay on the instance; only the dependencies you add are downloaded, from Maven Central and PyPI.

Step 11: Back up and restore

Everything Polynote keeps (your notebooks, and the dependencies and Python environments they downloaded) is in /var/lib/polynote. Take an EBS snapshot of the root volume, or copy the notebooks off the instance (dependencies are downloaded again from each notebook's own settings when its kernel starts):

sudo tar -C /var/lib/polynote -czf polynote-notebooks-$(date +%F).tar.gz notebooks

To restore into a new instance of this image, unpack the archive as polynote: sudo -u polynote tar -C /var/lib/polynote -xzf - < polynote-notebooks-<date>.tar.gz. Keep your notebooks in git as well: they are standard Jupyter .ipynb files.

Step 12: Security notes and the build record

The certificate generated at first boot is a server certificate only: it cannot sign other certificates.

openssl x509 -in /etc/ssl/polynote/polynote.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsage | tee /dev/stderr | grep -q 'CA:FALSE'
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Extended Key Usage: 
    TLS Web Server Authentication

Polynote is the 0.7.2 release on Java 11 (Polynote 0.7.2 runs on Java 8 or 11), and the notebook account has no login shell and no sudo:

java -version 2>&1 | head -n 1 | tee /dev/stderr | grep -q '"11\.' \
  && sed -n '1,3p' /usr/share/doc/cloudimg-polynote/BUILD-INFO.txt >&2 \
  && getent passwd polynote | cut -d: -f1,6,7 | tee /dev/stderr | grep -qx 'polynote:/var/lib/polynote:/usr/sbin/nologin' \
  && sudo -l -U polynote 2>&1 | grep -q 'not allowed to run sudo' && echo "Polynote 0.7.2 on Java 11; polynote: no login shell, no sudo"
openjdk version "11.0.32.1" 2026-08-18
polynote          Polynote 0.7.2 (github.com/polynote/polynote, 2026-01-27), Apache-2.0; /opt/polynote
                  polynote-dist.tar.gz sha256 a43afd3a19343b93ec2d323731f18c296ed0af33b7a56420af2ade40dd14c17b (= the GitHub release asset digest)
                  Scala 2.12 (default; deps/2.12: 9 jars) - jar list in jars-2.12.txt
polynote:/var/lib/polynote:/usr/sbin/nologin
Polynote 0.7.2 on Java 11; polynote: no login shell, no sudo

Polynote's About panel shows the build commit, c32a0ec, which is the 0.7.2 release tag; the version label beside it reads 0.7.0-SNAPSHOT in the official 0.7.2 release build.

Polynote runs as polynote with no capabilities and NoNewPrivileges, sees the rest of the system read-only (ProtectSystem=strict, writable only under /var/lib/polynote), has a private /tmp and cannot see other users' processes. Its configuration (/opt/cloudimg-polynote/config.yml) is owned by root, so notebook code cannot change where Polynote listens. It cannot read the credentials file, the password hash or the TLS key, and nginx never passes the password on to it. The Polynote release in /opt/polynote and the Python environment in /opt/cloudimg-polynote/venv are owned by root and read-only to notebooks; dependencies a notebook adds go to /var/lib/polynote. Some sandbox settings are deliberately relaxed because Java compiles code as it runs: JIT compilation (no MemoryDenyWriteExecute), any system call (no SystemCallFilter), a full /proc for its own processes, and netlink sockets. The service preloads Python's shared library (LD_PRELOAD), which jep needs so that Python modules with compiled code, such as numpy and pandas, load inside a kernel.

Do not run Polynote yourself as ubuntu or root (for example python3 /opt/polynote/polynote.py): that starts a second server without the password, the key protection or the metadata block. Manage the service with systemctl and the polynote-* helpers instead.

Polynote is Apache-2.0 licensed; its licence, the list of every bundled jar and the list of every Python package are in /usr/share/doc/cloudimg-polynote/. Java and nginx come from Ubuntu's archive.

Step 13: Keep it up to date

Ubuntu security updates, including Java and nginx, install automatically. To apply everything now:

sudo apt-get update && sudo apt-get upgrade -y

Dependencies your notebooks add update when you change their versions in each notebook's Configuration & dependencies. New Polynote releases arrive as new versions of the AWS Marketplace listing: launch the new version, copy /var/lib/polynote/notebooks into it (Step 11) and run sudo polynote-set-site with your domain if you use one. Polynote and its Python environment are installed from the release, not from an apt repository, so apt-get upgrade does not change them.

Support

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk

The other versions on this listing are on the developer and API tools guide.