Analytics AWS

Voila 0.5.13 on AWS User Guide

| Product: Voila 0.5.13 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Voila 0.5.13 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg BI and web analytics listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Voila turns a Jupyter notebook into a standalone web application. It runs the notebook in a Python kernel, shows the outputs and the interactive widgets (sliders, drop downs, buttons, text boxes) and hides the code. When someone moves a slider, the widget's new value travels to the kernel, the notebook's own callback recomputes, and the page updates. Your audience gets a dashboard; they never see or edit the notebook.

A fresh upstream Voila listens without a login, serves plain HTTP, and its kernel accepts any message a browser sends it. The cloudimg image changes that:

  • HTTPS from the first boot. nginx serves Voila on port 443 with a certificate generated on your instance's first boot for its public address, public DNS name and private address. Port 80 redirects to HTTPS. The certificate's SHA-256 fingerprint is written to the credentials file so you can check it the first time you connect.
  • A login for everything. Every page, every script and stylesheet, every widget asset and the kernel's websocket need the admin login (HTTP basic authentication in nginx). Without it nginx answers 401 and Voila never sees the request.
  • A password unique to your instance. First boot generates a random 24 character password and stores only its SHA-512-crypt hash for nginx. The password is written to /root/voila-credentials.txt (root only). The image ships no password, no hash, no TLS key and no kernel state.
  • Only the dashboard's widgets reach the kernel. Voila 0.5.13 passes a browser's messages to the kernel only when they are widget messages (comm_open, comm_msg, comm_close, comm_info_request, kernel_info_request). A browser that sends execute_request, inspect_request, complete_request, history_request, debug_request or an interrupt gets no reply and nothing runs. The image proves this on every build with a websocket client that sends each of them, and you can run the same proof yourself (Step 7).
  • No Jupyter server pages. Voila runs in single notebook mode: the file browser (/tree), the editor, terminals, the contents API, the kernel list and sessions are not served (nginx answers 404 before the login is even checked), and the notebook source cannot be downloaded.
  • Hardened. Voila listens on 127.0.0.1:8866 only, and only root and nginx may connect to it. Voila and its kernels run as the unprivileged voila account, which can write only to its own home directory, with no new privileges, no writable and executable memory and no core dumps. Voila, its kernels and nginx are blocked from the instance metadata service and from private addresses in your VPC. nginx drops requests for host names the instance does not serve, refuses cross site requests, sets a strict Content Security Policy and rate limits each client address. The AMI requires IMDSv2.
  • Verified packages. Voila, ipywidgets and ipykernel and every package they need (88 in all) are installed from a lock file that pins each package to a SHA-256 hash, as binary wheels only. The image is fully patched Ubuntu 24.04 LTS with unattended security upgrades on.

What is on the image: Voila 0.5.13 with ipywidgets 8.1.9 and ipykernel 7.4.0 in /opt/voila/venv (Python 3.12), a demo dashboard at /opt/voila/notebooks/dashboard.ipynb, nginx 1.24 from the Ubuntu archive, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The environment is deliberately small: it has no numpy, pandas, matplotlib or Pillow. Step 9 shows how to add the packages your own notebook needs.

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 with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (SSH), TCP 443 (Voila) and TCP 80 (redirects to HTTPS). Open 22 from your own networks only, and 443 to the people who use the dashboard. Port 8866 is Voila's internal port and must never be opened.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Voila 0.5.13 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.

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 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=voila}]'

Step 2: Connect to your instance

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

The login message shows the Voila address, the user name and where to find the password:

  Voila 0.5.13 (cloudimg): https://<public-ip>/   user admin, password: sudo cat /root/voila-credentials.txt
  Serve your notebook: sudo voila-set-notebook FILE.ipynb    Self-test: sudo voila-selftest
  Guide: https://www.cloudimg.co.uk/guides/voila-0-5-13-on-ubuntu-24-04-aws/

Step 3: Check the services

First boot creates the password, its hash and the certificate, then Voila and nginx start. voila-firstboot runs once and then shows as inactive; cloudimg-voila-egress holds the network rules for Voila and nginx:

systemctl is-active nginx voila voila-firstboot cloudimg-voila-egress
sudo test -f /var/lib/cloudimg/voila-firstboot.done && echo FIRSTBOOT_DONE
active
active
inactive
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network. Voila listens on 127.0.0.1:8866:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/pid=[0-9]+,fd=[0-9]+/.../g' | sort
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",...))
127.0.0.1:8866 users:(("voila",...))
127.0.0.53%lo:53 users:(("systemd-resolve",...))
127.0.0.54:53 users:(("systemd-resolve",...))
[::]:22 users:(("sshd",...),("systemd",...))
[::]:443 users:(("nginx",...),("nginx",...),("nginx",...))
[::]:80 users:(("nginx",...),("nginx",...),("nginx",...))

While someone has the dashboard open you also see one 127.0.0.1:<port> line for each running kernel (python). That is ipykernel's internal pipe for output from processes the notebook starts; it accepts only messages carrying a random per kernel token and carries output, never code. The kernel talks to Voila over UNIX sockets in a private directory.

Step 4: Get your URL and password

The credentials file is readable by root only. This prints everything in it except the password line:

sudo grep -v '^VOILA_PASSWORD=' /root/voila-credentials.txt
# Voila 0.5.13 (cloudimg) on this instance. Open VOILA_URL in a browser and sign in as VOILA_USERNAME / VOILA_PASSWORD.
# The password is unique to this instance. Change it: sudo voila-set-password (or --stdin to choose one).
# Serve your own notebook: sudo voila-set-notebook /path/to/your.ipynb (see the guide).
# The certificate is self-signed for this instance (/etc/voila/voila-instance.crt): compare its SHA-256 fingerprint with VOILA_TLS_SHA256.
VOILA_URL=https://<public-ip>/
VOILA_HOST=<public-ip>
VOILA_USERNAME=admin
VOILA_TLS_SHA256=<VOILA_TLS_SHA256>
VOILA_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/voila-0-5-13-on-ubuntu-24-04-aws/

Print the password itself only when you are ready to sign in: sudo sed -n 's/^VOILA_PASSWORD=//p' /root/voila-credentials.txt

VOILA_URL uses the instance's public IPv4 address, read from instance metadata on first boot (an instance without one uses its private address). To serve Voila under your own DNS name, see Step 11.

Step 5: Check the certificate and sign in

The certificate is self signed, so your browser asks you to confirm it the first time. Before you accept it, compare its SHA-256 fingerprint (in the browser's certificate viewer) with VOILA_TLS_SHA256 from the credentials file. From your workstation you can read it with OpenSSL:

echo | openssl s_client -connect <public-ip>:443 2>/dev/null | openssl x509 -noout -fingerprint -sha256

On the instance itself the same check reads the fingerprint nginx serves:

H=$(sudo sed -n 's/^VOILA_HOST=//p' /root/voila-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -fingerprint -sha256
sudo grep '^VOILA_TLS_SHA256=' /root/voila-credentials.txt

The two fingerprints are the same:

sha256 Fingerprint=<VOILA_TLS_SHA256>
VOILA_TLS_SHA256=<VOILA_TLS_SHA256>

Open VOILA_URL in your browser. It asks for a user name and password. Without them, or with a wrong password, nginx answers with its own page and Voila is never reached:

The nginx 401 Authorization Required page a browser shows when it opens the Voila address without the login

Enter admin and the password from the credentials file. The demo dashboard opens: a compound interest calculator with sliders for the principal, the rate and the years, a drop down for the compounding, the final balance and a bar chart of the balance year by year:

The Voila demo dashboard Compound interest, showing Principal, Rate, Years and Compounding controls, a final balance of 16,470.09 over 10 years and a bar chart of the yearly balance

Step 6: Use the dashboard

Move a slider or change the compounding. The new value goes to the kernel behind your page, the notebook's callback recomputes the balance, and the summary and the chart update:

The same Voila dashboard after the controls changed to 20,000 principal, 6.50 percent, 20 years and quarterly compounding, showing a final balance of 72,623.08 and a longer bar chart

Each browser tab gets its own kernel, so several people can use the dashboard at the same time without seeing each other's choices. A kernel that has had no activity and no open page for 10 minutes is shut down, so closing a tab frees its memory a few minutes later. The kernel runs the notebook as the voila account, which can write only to /var/lib/voila.

The page's template refers to two Font Awesome stylesheets on cdn.jsdelivr.net. The Content Security Policy only allows stylesheets from the instance itself, so the browser does not fetch them (its console reports two blocked stylesheets) and nothing on the page loads from a third party. Dashboards built with ipywidgets look the same, except that widgets which draw Font Awesome icons (a Button or ToggleButtons with an icon, the Play widget's controls) show without those icons.

Step 7: Check the security posture from the command line

Port 80 redirects to HTTPS, every published path refuses requests without a valid login, the Jupyter server's own pages are not served, and a request for a host name the instance does not serve is dropped without an answer. /healthz is answered by nginx itself for load balancer health checks and reveals nothing about Voila:

H=$(sudo sed -n 's/^VOILA_HOST=//p' /root/voila-credentials.txt); C=(--cacert /etc/voila/voila-instance.crt --connect-to ::127.0.0.1:)
curl -s -o /dev/null -w 'http://             -> %{http_code}\n' "${C[@]}" "http://$H/"
curl -s -o /dev/null -w 'dashboard, no login -> %{http_code}\n' "${C[@]}" "https://$H/"
curl -s -o /dev/null -w 'script, no login    -> %{http_code}\n' "${C[@]}" "https://$H/voila/static/voila.js"
curl -s -o /dev/null -w 'bad password        -> %{http_code}\n' "${C[@]}" -K <(printf 'user = "admin:%s"\n' "$(openssl rand -hex 12)") "https://$H/"
curl -s -o /dev/null -w 'file browser /tree  -> %{http_code}\n' "${C[@]}" "https://$H/tree"
curl -s -o /dev/null -w 'kernel list         -> %{http_code}\n' "${C[@]}" "https://$H/api/kernels"
curl -s -o /dev/null -w 'contents API        -> %{http_code}\n' "${C[@]}" "https://$H/api/contents"
curl -s -o /dev/null -w 'terminals           -> %{http_code}\n' "${C[@]}" "https://$H/terminals/1"
curl -s -o /dev/null -w 'nginx /healthz      -> %{http_code}\n' "${C[@]}" "https://$H/healthz"
curl -sk -o /dev/null -w 'unknown host        -> %{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
http://             -> 301
dashboard, no login -> 401
script, no login    -> 401
bad password        -> 401
file browser /tree  -> 404
kernel list         -> 404
contents API        -> 404
terminals           -> 404
nginx /healthz      -> 200
unknown host        -> 000

000 means nginx closed the connection without answering, which is how it treats any host name it does not serve.

voila-selftest runs the full check over HTTPS with the instance's own password (never on a command line): the refusals above, the dashboard with the password, and the kernel websocket proof. It opens the dashboard's kernel, moves a widget, then sends execute_request on every channel, the other code and introspection requests, and an execute_request over the newer websocket protocol, and checks that none of them is answered and that the file the code would have written does not exist:

sudo voila-selftest 2>&1 | tail -1
VOILA_SELFTEST_OK

Step 8: Serve your own notebook

Voila serves one notebook, /opt/voila/notebooks/dashboard.ipynb. It is owned by root and read only for Voila and its kernels, so nothing a browser does can change it. Copy your notebook to the instance and make it the dashboard with voila-set-notebook, which checks that the file is a notebook, installs it with the right owner and mode and restarts Voila:

scp -i <your-key.pem> my-dashboard.ipynb ubuntu@<public-ip>:
ssh -i <your-key.pem> ubuntu@<public-ip> sudo voila-set-notebook my-dashboard.ipynb

Voila runs every cell each time a page opens and shows the outputs and widgets, never the code. Build the interactive parts with ipywidgets (interact, or widgets with observe callbacks), as the demo does. Treat the notebook as code that runs on your server: only serve notebooks you have reviewed. To go back to the demo:

sudo voila-set-notebook --default
voila-set-notebook: now serving dashboard.ipynb.default (Voila restarted)

Step 9: Add Python packages for your notebook

The notebook can import the packages in /opt/voila/venv; /usr/share/doc/voila/requirements.lock lists all 88. The environment has no pip of its own, so use Ubuntu's pip and point it at the environment's Python. For example, to add pandas:

# Ubuntu's pip, then pip install into the Voila environment (needs internet access)
sudo apt-get install -y python3-pip
sudo python3 -m pip --python /opt/voila/venv/bin/python install --only-binary=:all: pandas
sudo systemctl restart voila

A notebook that runs import pandas then works as usual (on this image, pandas 3.1.1 with numpy 2.5.4 at the time of writing). Packages you add are your own choice and are not covered by the image's lock file. Install only what your notebook needs, from sources you trust, and keep them up to date.

Step 10: Change the password

To change the password, either generate a new random one (written to /root/voila-credentials.txt) or set your own of 16 to 128 characters (not stored). It takes effect at once:

# a new random password
sudo voila-set-password
# or your own
printf '%s\n' '<new-password>' | sudo voila-set-password --stdin

The login is shared by everyone who uses the dashboard. Change it when someone should no longer have access.

Step 11: Your own DNS name and certificate

Point a DNS record at the instance (an Elastic IP keeps the address stable across stop and start), then run voila-set-domain with the name. It sets nginx's server names and issues a new self signed certificate for that name, plus any extra names or IPv4 addresses you list, localhost and 127.0.0.1:

sudo voila-set-domain <your-domain>
sudo grep -E '^VOILA_(URL|HOST|TLS_SHA256)=' /root/voila-credentials.txt

Without your own name, run sudo voila-set-domain with the new address after a stop and start changes the instance's public IP.

To serve Voila with a certificate your browsers already trust, put an Application Load Balancer with an AWS Certificate Manager certificate in front of the instance: forward HTTPS to the instance on port 443, use /healthz as the health check path and enable websocket support (the Application Load Balancer passes websockets by default). The load balancer passes your name on to the instance, and its health checks use the instance's private address, so serve both: sudo voila-set-domain <your-domain> <private-ip>. nginx drops any other host name, health checks included.

Step 12: Reach a private network from your dashboard

Your notebook's code can reach the public internet (to read a public API or dataset, for example), but Voila, its kernels and nginx are blocked from private addresses, carrier grade NAT addresses, your VPC's own address ranges and the instance metadata service. If your dashboard must read from a host inside your VPC, such as a database, add its network to the allow list and reload the rule:

echo '<private-ip>/32' | sudo tee -a /etc/cloudimg/voila-egress-allow
sudo systemctl reload cloudimg-voila-egress

The metadata service and link local addresses can never be allowed, so a notebook can never read the instance role's credentials.

Step 13: Versions, files and logs

The versions on the image:

/opt/voila/venv/bin/python -c "import voila, ipywidgets, ipykernel; print('voila', voila.__version__); print('ipywidgets', ipywidgets.__version__); print('ipykernel', ipykernel.__version__)"
/opt/voila/venv/bin/python -V
nginx -v 2>&1
voila 0.5.13
ipywidgets 8.1.9
ipykernel 7.4.0
Python 3.12.3
nginx version: nginx/1.24.0 (Ubuntu)
Component Path
Voila, ipywidgets, ipykernel and their Python packages (root owned) /opt/voila/venv
The served notebook (root owned, read only for Voila) /opt/voila/notebooks/dashboard.ipynb
Voila's home, its only writable path (IPython profile, notebook signing key) /var/lib/voila (voila:voila 0700)
Login hash for nginx /etc/voila/htpasswd (root:www-data 0640)
Instance certificate (public copy) /etc/voila/voila-instance.crt
Egress allow list /etc/cloudimg/voila-egress-allow
nginx site /etc/nginx/sites-available/voila
Licences, third party notices, source note, lock file /usr/share/doc/voila/
Helpers /usr/local/sbin/voila-set-notebook, voila-set-password, voila-set-domain, voila-selftest
Services voila, nginx, voila-firstboot (first boot only), cloudimg-voila-egress
Credentials /root/voila-credentials.txt (mode 0600)

Logs. Voila and its kernels log to the journal (sudo journalctl -u voila), which only root and the adm group can read; Voila's request lines there include the full request address. nginx's access log records request paths without query strings, and no password is written to any log.

Step 14: Security recommendations

  • Allow port 22 in the security group from your own networks only, and port 443 only to the people who use the dashboard. Never open port 8866.
  • Check the certificate fingerprint the first time you connect (Step 5), then use your own DNS name (Step 11) or a load balancer with a trusted certificate.
  • Serve only notebooks you have reviewed (Step 8). The notebook's code runs on the instance with access to the public internet.
  • Change the password when someone should no longer have access (Step 10).
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.

Support and licensing

Voila is open source under the BSD 3-Clause licence, as are ipywidgets and ipykernel; the other Python packages are under permissive open source licences listed in /usr/share/doc/voila/dependency-licences.txt. The third party notices, including the attributions for code adapted from Stack Overflow (CC BY-SA), a note on where the source comes from and the lock file are in /usr/share/doc/voila/. There is no per user or per CPU licence fee. cloudimg is not affiliated with or endorsed by the Voila project or Project Jupyter. The AMI price covers cloudimg's hardening, first boot setup and support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA