Deployment guide

Polynote 0.7.2

Overview

This guide covers launching and using the Polynote 0.7.2 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud 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. Netflix wrote it 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.

What you get:

  • 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. 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/sac/data/polynote-credentials.txt (root only). nginx keeps only a SHA-512 crypt hash of the password. 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 license are in /usr/share/doc/sac-polynote/.
  • Defaults: the default user is ubuntu, the architecture is x86_64, and the recommended instance type is 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>). Avoid opening them to the whole internet.
  • A modern browser (Chrome, Edge, Firefox or Safari)
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 select your Region. Then choose Launch through EC2.

Choose m5.large, your key pair, your subnet and the security group above. The m5.large size has 8 GiB of memory. 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 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).

To launch with the AWS CLI instead, 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 the same across stops and starts, and so keeps its name and certificate the same too.

2

Connect to your instance

The SSH login user for this listing is:

  • Polynote 0.7.2 on Ubuntu 24.04: ubuntu

Connect with your key pair:

ssh -i <your-key.pem> ubuntu@<public-ip>

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

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/sac/data/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. The ss tool 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. List the listening ports:

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
4

Get your address and password

Run sudo cat /var/lib/sac/data/polynote-credentials.txt to see your notebook server's address, user name and password. Only root can read the file. It holds these settings, all generated for this instance:

sudo sed -n 's/^\(POLYNOTE_[A-Z0-9_]*\)=.*/\1/p' /var/lib/sac/data/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. To show only the settings that are not secret:

sudo grep -E '^POLYNOTE_(URL|USERNAME|TLS_SHA256_FINGERPRINT)=' /var/lib/sac/data/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 that only root and nginx can read. Check it:

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
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. Alternatively, 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). Alternatively, 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.

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 click 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), and run a cell with Shift+Enter. The Symbols panel on the right lists 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. Add Scala dependencies as Maven coordinates (for example org.typelevel::cats-core:2.10.0), fetched from Maven Central. Add 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. Set 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 then appears in the notebook list. The Download button at the top saves the open notebook to your computer.

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 shows 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 but still answers for ubuntu (your SSH account). 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, put them 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 ...).

Next, run the image's self-test in Step 8.

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
  • that a Scala cell and a Python cell (with numpy and pandas) compute through Polynote's own notebook connection

The self-test 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.

9

Change the password or use your own domain

To give the server a new random password, run the command below. The old password stops working at once. Then read the new POLYNOTE_PASSWORD with sudo cat /var/lib/sac/data/polynote-credentials.txt. Open browser tabs must sign in again, and 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.

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.

The command below sends six requests and shows the result of each:

  • no password: 401
  • the password, read from the credentials file without putting it on a command line: 200
  • a name that is not the server's: no answer (000)
  • plain HTTP: 301 to HTTPS
  • a cross-site form post: 403
  • a notebook connection from another site: 403

Both cross-site requests are refused before the password is even checked:

H="$(sudo sed -n 's/^POLYNOTE_HOST=//p' /var/lib/sac/data/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/sac/data/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. It puts the key in the editor page you open after signing in. The key changes every time Polynote restarts, and 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 it 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 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, because 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. Polynote writes some request addresses, key included, to its log, so the filter replaces anything shaped like Polynote's key with <uuid-withheld>.

What the editor fetches from the internet. Nothing. The instance serves Polynote's editor page and all of its scripts, fonts and styles. Your notebooks, code and data stay on the instance. Only the dependencies you add are downloaded, from Maven Central and PyPI.

11

Back up and restore

Everything Polynote keeps is in /var/lib/polynote: your notebooks, and the dependencies and Python environments they downloaded. 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, because they are standard Jupyter .ipynb files.

12

Security notes and the build record

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

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). The notebook account has no login shell and no sudo. Check both:

java -version 2>&1 | head -n 1 | tee /dev/stderr | grep -q '"11\.' \
  && sed -n '1,3p' /usr/share/doc/sac-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. It 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/sac-polynote/config.yml) is owned by root, so notebook code cannot change where Polynote listens.

Polynote 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/sac-polynote/venv are owned by root and read-only to notebooks. Dependencies that 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). jep needs this 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 license, the list of every bundled jar and the list of every Python package are in /usr/share/doc/sac-polynote/. Java and nginx come from Ubuntu's archive.

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 that your notebooks add update when you change their versions in each notebook's Configuration & dependencies panel. 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: sales@secureanycloud.com
  • Website: https://secureanycloud.com

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

Deploy on AWS

Launch Polynote 0.7.2 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS