Polynote 0.7.2 on AWS User Guide
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
jepbridge 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 nosudo. 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-Forheader 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 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 + 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
-Xmx4gfor 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:

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, gets400from nginx before the password is checked. - Cross-site requests. A request that changes something (anything but
GET,HEADorOPTIONS) must come from Polynote's own page, and so must every notebook connection: with another site'sOriginheader, or none at all, nginx answers403before 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 anyX-Forwarded-Forheader with the address it accepted the connection from and blanksX-Real-IP,Forwardedand 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;
TRACEandCONNECTget405. - Headers. Responses carry
Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN, aContent-Security-Policyofframe-ancestors 'self',Referrer-Policy: same-originand aPermissions-Policythat 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.