Datasette 0.65.5 on AWS User Guide
Overview
This guide covers launching and using the Datasette 0.65.5 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.
Datasette is a tool for exploring and publishing data held in SQLite databases. Every table gets a browsable page with sorting, filtering and facets (counts of the values in a column, which you click to narrow the rows), every database gets a SQL editor, and every page has a JSON and CSV version, so the same data serves people in a browser and scripts through an API.

Datasette has no login of its own: anyone who can reach it can read every database it serves. The cloudimg image makes it safe to run on EC2:
- A password on every page, unique to your instance. nginx asks for a user name and password (HTTP basic authentication) on every page, every JSON and CSV URL and every download. The password is generated on your instance's first boot into a root only file; no password, certificate or key is baked into the image. Change it at any time with
datasette-set-password. - Rate limited per client. Every request carries the password, so nginx limits requests per client address (10 a second, bursts of 60) and answers
429beyond that, which turns password guessing into a crawl. ForgedX-Forwarded-Forheaders do not move the limit. - HTTPS from the first boot. nginx serves Datasette on port 443 with a certificate generated on your instance's first boot for its public address; port 80 redirects to HTTPS. Requests for a host name the instance does not serve get no answer.
- Read-only databases. Every database is opened immutable: the SQL editor runs
SELECTstatements only, and Datasette cannot change, lock or create a file. Datasette reads only the databases in/var/lib/datasette/databases, and SQLite extensions cannot be loaded, so a query cannot reach any other file. - No plugins. Datasette 0.65.5 is installed from a lock file in which each of the 28 Python packages is pinned by version and SHA-256 hash, into a root owned virtual environment. No plugin is installed and none can be installed at run time.
- Datasette listens on localhost only (
127.0.0.1:8001), runs as its own unprivilegeddatasetteuser with no Linux capabilities and cannot write anywhere on disk. Other local accounts (includingubuntu) cannot connect to it directly. - Other web sites cannot use your session. nginx refuses any request other than reading (
GET) that comes from another site's page, and sends a content security policy that allows scripts and styles from the instance only. - No outbound reach inside AWS. Datasette and nginx are refused, at the kernel level and on every boot, any connection to the EC2 instance metadata service, link-local, private (RFC 1918), carrier grade NAT and reserved addresses and your VPC's own address ranges.
- Fully patched Ubuntu 24.04 LTS at build time.
The image ships with a small fictional demo database so you can try Datasette straight away. Step 7 shows how to serve your own.
What is on the image: Datasette 0.65.5 (/opt/datasette/venv), the database directory /var/lib/datasette/databases with the demo database demo.db, the sqlite3 command line shell, nginx, the tools datasette-add-database, datasette-set-password, datasette-set-domain and datasette-selftest, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
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 internet access.
- A security group that allows TCP 22 (SSH) and TCP 443 (Datasette) from your own network, and TCP 80 (redirects to HTTPS) if you want plain
http://links to work. Open 443 only to the people who should read your data.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Datasette 0.65.5 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a smaller instance such as t3.small serves small databases to a few people well), your key pair, a subnet and a security group as above. An Elastic IP keeps the address, and so the certificate's name, stable across a stop and start.
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=datasette}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Datasette 0.65.5 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot reads the instance's public address from EC2 instance metadata, generates the password and the certificate and writes the credentials file; the egress backstop loads; then Datasette and nginx start:
systemctl is-active cloudimg-datasette-egress datasette nginx
sudo test -f /var/lib/cloudimg/datasette-firstboot.done && echo FIRSTBOOT_DONE
All three units are active and the first boot marker is present:
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Datasette listens on localhost only:
sudo ss -lntpH | awk '{print $4, $6}' | sed -E 's/users:\(\("([^"]+)".*/\1/' | grep -v '127.0.0.5' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.1:8001 datasette
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
Step 4: Get your URL and password
sudo cat /root/datasette-credentials.txt
The file is readable by root only and holds the URL, the user name, the password and the certificate's fingerprint:
# Datasette 0.65.5 (cloudimg) on this instance. Open DATASETTE_URL in a browser and sign in as DATASETTE_USERNAME / DATASETTE_PASSWORD.
# The password is unique to this instance. Change it: sudo datasette-set-password (or --stdin to choose one).
# The certificate is self-signed for this instance: compare its SHA-256 fingerprint with DATASETTE_TLS_SHA256.
# The demo database is fictional sample data; serve your own SQLite file (read-only): sudo datasette-add-database FILE
DATASETTE_URL=https://<public-ip>/
DATASETTE_HOST=<public-ip>
DATASETTE_USERNAME=admin
DATASETTE_PASSWORD=<DATASETTE_PASSWORD>
DATASETTE_TLS_SHA256=<64 hex digits, colon separated>
DATASETTE_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/datasette-0-65-5-on-ubuntu-24-04-aws/
DATASETTE_ADDRESS_SOURCE says where the address came from: the public IPv4 address from instance metadata, or the private address when the instance has no public one.
The image includes a self test. Through nginx and pinned to this instance's certificate it checks that seven paths, including the JSON API, the SQL editor and the database download, refuse a request with no password and one with a wrong password, that the password opens a table and its JSON, and that a write is refused:
sudo datasette-selftest
datasette-selftest: 7 paths x (no credential, wrong password) -> 401; the password opens /demo/countries and its JSON API; a write is refused
DATASETTE_SELFTEST_OK
Step 5: Sign in and explore
Open DATASETTE_URL in your browser. The certificate is self signed for your instance, so the browser warns once. To check you are talking to your instance, open the certificate details and compare its SHA-256 fingerprint with DATASETTE_TLS_SHA256, then continue. The browser then asks for a user name and password: enter admin and DATASETTE_PASSWORD. The browser remembers them until you close it.
The home page lists every database Datasette serves and its tables. The demo database holds three small fictional tables: countries, cities and sensor_readings:

Open a table to browse its rows. Click a column heading to sort, use the filter row to narrow the rows (for example population > 100000000), and pick Suggested facets (or add ?_facet=<column> to the URL) to count the values in a column, as in the picture at the top of this guide. The links at the top of each table give the same rows as JSON or CSV, with your sort and filters applied.
Step 6: Run SQL and use the API
Every database page has a SQL editor. Datasette runs SELECT statements (and WITH queries that read), shows the result as a table and links it as JSON and CSV. For example, on the demo database:
SELECT continent, count(*) AS countries, sum(population) AS population
FROM countries GROUP BY continent ORDER BY population DESC

The sensor_readings table holds CPU and memory readings taken every 10 minutes on three fictional hosts; facet it by host and metric, or filter on recorded_at:

Every page has a JSON version: add .json to the path (add ?_shape=array for a plain list of rows). Scripts sign in with the same user name and password. On the instance (the password is read from the credentials file, so it does not appear on the command line):
H=$(sudo sed -n 's/^DATASETTE_HOST=//p' /root/datasette-credentials.txt)
sudo sed -n 's/^DATASETTE_PASSWORD=/user = "admin:/p' /root/datasette-credentials.txt | sed 's/$/"/' | \
curl -s -K - --cacert <(sudo cat /etc/ssl/datasette/datasette.crt) --resolve "$H:443:127.0.0.1" \
"https://$H/demo/countries.json?_shape=array&_size=3&_sort_desc=population&_col=name&_col=population"
echo
[{"id": 5, "name": "India", "population": 1428600000}, {"id": 8, "name": "United States", "population": 339996000}, {"id": 7, "name": "Nigeria", "population": 223800000}]
From your computer, use DATASETTE_URL with curl -u admin (curl asks for the password), Python's requests with auth=("admin", password), or any HTTP client that sends basic authentication. CSV works the same way with .csv, and the database page links a download of the whole SQLite file.
Writes are refused. The SQL editor answers any statement that is not a read with an error, and the database itself is opened immutable, so even a query that tries to write through a WITH clause fails:
INSERT INTO countries (name, continent, population, area_km2) VALUES ('x', 'x', 1, 1)
-> Statement must be a SELECT
Step 7: Serve your own database
Datasette serves every SQLite file named <name>.db in /var/lib/datasette/databases. It reads them as the datasette user but cannot write to them or to the directory:
sudo stat -c '%a %U:%G %n' /var/lib/datasette/databases /var/lib/datasette/databases/demo.db
750 root:datasette /var/lib/datasette/databases
640 root:datasette /var/lib/datasette/databases/demo.db
- From your computer, copy your SQLite database to the instance:
scp -i <your-key.pem> <your-database>.db ubuntu@<public-ip>:/tmp/
- On the instance, add it.
datasette-add-databasechecks that the file is an SQLite 3 database, copies it into the database directory read-only for Datasette, restarts Datasette and waits until it serves the new database:
sudo datasette-add-database /tmp/<your-database>.db
rm /tmp/<your-database>.db
datasette-add-database: serving <your-database> (read-only) at /<your-database>
The name in the URL is the file name without .db (give a second argument to choose another name: letters, digits, _ and -). The database appears on the home page beside demo.
WAL databases. Datasette opens every file immutable, which is only safe for a database in rollback journal mode, so datasette-add-database refuses a database in WAL mode. Switch it on your computer first (this also folds any -wal file into the database):
sqlite3 <your-database>.db 'PRAGMA journal_mode=DELETE;'
Updating a database. Because Datasette serves a copy, update your database where you edit it, then replace the copy:
sudo rm /var/lib/datasette/databases/<your-database>.db
sudo datasette-add-database /tmp/<your-database>.db
Removing the demo database. When you no longer need it:
sudo rm /var/lib/datasette/databases/demo.db && sudo systemctl restart datasette
Queries across databases. Datasette is started with cross database queries enabled, so the SQL editor on the _memory database can join tables from every database you serve, for example SELECT * FROM demo.countries.
Title and description. The title on the home page comes from /etc/datasette/metadata.json:
cat /etc/datasette/metadata.json
{
"title": "Datasette (cloudimg)",
"description_html": "Explore the bundled <strong>demo</strong> database (fictional sample data), or add your own SQLite files with <code>sudo datasette-add-database FILE</code>. Every database is served read-only."
}
Edit it with sudo nano /etc/datasette/metadata.json (it must stay owned by root and not writable by other users) and run sudo systemctl restart datasette. Datasette's per database, per table and per column descriptions, licences and sources go in the same file; see the Datasette metadata documentation. Plugin and permission settings have no effect, because no plugin is installed and every reader signs in with the same password.
Step 8: Change the password
To generate a new random password (written to the credentials file as DATASETTE_PASSWORD):
sudo datasette-set-password
To choose your own (16 characters or more, read from standard input and not stored on the instance):
sudo datasette-set-password --stdin
The new password takes effect at once; your browser asks for it again. The password is stored for nginx as a bcrypt hash in /etc/nginx/cloudimg/datasette.htpasswd.
Step 9: Use your own domain, or a new address
The certificate and the host names nginx answers are set for the address the instance had on first boot. If you stop and start an instance that has no Elastic IP, its public address changes; run datasette-set-domain with the new address. To use a domain, point an A record (for example data.example.com) at the instance's public IP address, then:
sudo datasette-set-domain <your-domain>
This sets the name nginx accepts, generates a new self signed certificate for it and updates DATASETTE_URL, DATASETTE_HOST and DATASETTE_TLS_SHA256 in the credentials file. Add more names or addresses after the first to serve them too (sudo datasette-set-domain <your-domain> <public-ip>).
For a certificate your browser trusts, put the certificate chain for <your-domain> in /etc/ssl/datasette/datasette.crt and its private key in /etc/ssl/datasette/datasette.key (root only, mode 600) and run sudo systemctl reload nginx, or put the instance behind an Application Load Balancer with an AWS Certificate Manager certificate that forwards to port 443 on the instance.
Step 10: Outbound connections
Datasette, nginx and the first boot service are refused any connection to the instance metadata service, and the datasette and www-data accounts are refused link-local, private, carrier grade NAT, reserved and multicast addresses and your VPC's own address ranges (read from instance metadata on every boot), and all IPv6 except localhost. Datasette needs no outbound connection: it reads only the database files on the instance.
The allow list is empty by default:
sed -n '1,5p' /etc/cloudimg/datasette-egress-allow
# cloudimg Datasette egress allow-list (empty by default).
# Datasette and nginx never need an internal address (Datasette only reads local SQLite files). If something you run as
# the datasette account must reach a host inside your VPC or a private network, list its network here, one CIDR per line - it must lie inside 10.0.0.0/8,
# 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 or fc00::/7 (metadata and link-local addresses can never be allowed) -
# then run: sudo systemctl reload cloudimg-datasette-egress
The instance metadata service can never be allowed.
Step 11: Security checks
The password guards every page, the API and the download; a write from another site's page is refused; the health check needs no password; a host name the instance does not serve gets no answer; plain HTTP redirects; and other local accounts cannot reach Datasette directly:
H=$(sudo sed -n 's/^DATASETTE_HOST=//p' /root/datasette-credentials.txt); WRONG="admin:not-the-password"
curl -sk -o /dev/null -w 'no password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/demo/countries"
curl -sk -o /dev/null -w 'JSON without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/demo/countries.json"
curl -sk -o /dev/null -w 'download without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/demo.db"
curl -sk -o /dev/null -w 'wrong password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -u "$WRONG" "https://$H/"
curl -sk -o /dev/null -w 'POST from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H 'Origin: https://evil.example' "https://$H/demo"
curl -sk -o /dev/null -w 'health check: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'unknown host name: %{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
curl -s -o /dev/null -w 'http: %{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -s -o /dev/null -w 'Datasette directly, as ubuntu: %{http_code}\n' http://127.0.0.1:8001/ || true
no password: 401
JSON without a password: 401
download without a password: 401
wrong password: 401
POST from another site: 403
health check: 200
unknown host name: 000
http: 301
Datasette directly, as ubuntu: 000
Datasette and nginx are blocked from the instance metadata service and reserved addresses:
for u in datasette nginx; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
sudo -u datasette curl -s -m 5 -o /dev/null -w 'metadata from the datasette account: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
datasette 168.63.129.16/32 169.254.169.254/32 192.0.0.0/24 198.18.0.0/15 2001::/32 2002::/16 224.0.0.0/4 240.0.0.0/4 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
nginx 168.63.129.16/32 169.254.169.254/32 192.0.0.0/24 198.18.0.0/15 2001::/32 2002::/16 224.0.0.0/4 240.0.0.0/4 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
metadata from the datasette account: 000
The private ranges and your VPC are refused by the egress backstop, which reads the VPC's ranges on every boot:
sudo iptables -S CLOUDIMG-DATASETTE-EGRESS | sed -n '1,4p'
cat /run/cloudimg/datasette-vpc-cidrs
-N CLOUDIMG-DATASETTE-EGRESS
-A CLOUDIMG-DATASETTE-EGRESS -d 127.0.0.0/8 -o lo -j RETURN
-A CLOUDIMG-DATASETTE-EGRESS -d 169.254.0.0/16 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-DATASETTE-EGRESS -d 169.254.0.0/16 -j REJECT --reject-with icmp-admin-prohibited
<your VPC CIDR, for example 172.31.0.0/16>
Datasette runs as datasette, on localhost, with no Linux capabilities, cannot gain any, and opens each database immutable (-i):
P=$(systemctl show -p MainPID --value datasette)
ps -o user:9=,args= -p "$P"
sudo grep -E '^(CapEff|CapBnd|CapAmb|NoNewPrivs)' /proc/"$P"/status
datasette /opt/datasette/venv/bin/python /opt/datasette/venv/bin/datasette serve --host 127.0.0.1 --port 8001 --metadata /etc/datasette/metadata.json --setting force_https_urls on --setting trace_debug off --setting sql_time_limit_ms 3000 --crossdb -i /var/lib/datasette/databases/demo.db
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1
No plugin is installed:
/opt/datasette/venv/bin/datasette --version
/opt/datasette/venv/bin/datasette plugins
datasette, version 0.65.5
[]
The credentials, the password hash and the TLS key are readable by root and nginx (the hash) only:
sudo stat -c '%a %U:%G %n' /root/datasette-credentials.txt /etc/nginx/cloudimg/datasette.htpasswd /etc/ssl/datasette/datasette.key /etc/datasette/metadata.json
600 root:root /root/datasette-credentials.txt
640 root:www-data /etc/nginx/cloudimg/datasette.htpasswd
600 root:root /etc/ssl/datasette/datasette.key
644 root:root /etc/datasette/metadata.json
Other notes:
- Query time limit. A query that runs longer than 3 seconds is stopped. Datasette's settings are passed by
/usr/local/lib/cloudimg/datasette/datasette-serve. - Rate limits. The burst of 60 covers normal browsing. Scripts that call the API in a tight loop from one address get
429; slow them to 10 requests a second. - Client addresses. nginx replaces any
X-Forwarded-FororX-Real-IPa client sends with the address it connected from, and keeps no query strings (so no SQL) in its access log. - Response headers. nginx sends HSTS, a content security policy,
X-Content-Type-Options: nosniffandX-Frame-Options: DENY, and no version number. - NVMe host identity. No NVMe-oF host identity ships in the image. If you install
nvme-cli, its host NQN and host ID are your instance's own. - Who can reach it. Keep ports 22 and 443 open only to your own networks.
- Logs. nginx logs are in
/var/log/nginx; Datasette logs to the journal (journalctl -u datasette).
Step 12: Back up
Datasette changes nothing on disk: your databases are the copies you added, and the originals stay wherever you edit them. To copy the served databases to your computer:
ssh -i <your-key.pem> ubuntu@<public-ip> 'sudo tar -C /var/lib/datasette -czf - databases' > datasette-databases.tar.gz
An EBS snapshot of the instance's volume is also a complete backup.
Step 13: Keep it up to date
To apply Ubuntu updates now:
sudo apt-get update && sudo apt-get upgrade -y
New Datasette releases arrive as new versions of this listing, built and checked the same way. To move to one, launch the new version and add your databases to it (Step 7). Read the Datasette changelog before you move.
Support and licensing
Datasette is open source under the Apache License 2.0. It is installed unmodified; its licence text, the lock file it was installed from and the licence of every other Python package (all permissive) are in /usr/share/doc/datasette:
ls /usr/share/doc/datasette
LICENSE.datasette
README.cloudimg
dependency-licences.txt
requirements.lock
The source code is at github.com/simonw/datasette. Datasette is not endorsed by or affiliated with cloudimg. There is no per user licence fee. The AMI price covers cloudimg's build, 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
The other versions on this listing are on the BI and web analytics guide.