Deno 2.9.2 on AWS User Guide
Overview
This guide covers launching and using the Deno 2.9.2 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg app stacks and runtimes listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Deno is a modern runtime for JavaScript and TypeScript, created by the original author of Node.js. It runs TypeScript directly with no build step, ships as a single binary with a formatter, linter, test runner and bundler built in, supports npm packages and web standard APIs such as fetch and Request, and, its defining feature, runs every program in a permission sandbox: a Deno program cannot read files, open network connections, read environment variables or start other programs unless you grant exactly that.
The cloudimg image gives you a Deno server that is safe to put on a public address from the first minute:
- HTTPS from the first boot. nginx serves the app on port 443 with a certificate generated on your instance's first boot, and port 80 redirects to HTTPS. No certificate or key is baked into the image.
- A per instance API token. The demo API's
/api/statusendpoint needs a bearer token that is generated on first boot and kept in a file only root can read. No two instances share one, and you can replace it with one command. - Deno permissions scoped to the app. The demo app runs with
--allow-net=127.0.0.1:8000(its own loopback port) and--allow-read=/etc/deno-demo/api-token(its token) and nothing else: never-A, no file writes, no environment, no subprocesses, no remote or npm code at run time. - Loopback only, behind nginx. The app listens on
127.0.0.1:8000; nginx is the only service on the network. It answers only for your instance's own host names, takes the host from configuration and not from the request, and overwritesX-Forwarded-ForandX-Real-IPwith the real client address. - Sandboxed and fail closed. The app runs as the unprivileged
denouser under a locked down systemd unit, and neither the app nor nginx can reach the instance metadata service. Neither starts until first boot has generated the certificate and the token. - Your apps on their own disk.
/var/lib/deno-appsis a dedicated 20 GiB EBS volume you can resize independently of the operating system disk. - The official release, verified, on a fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Deno 2.9.2 (the official release binary at /usr/local/bin/deno, licence in /usr/share/doc/deno), the demo app in /var/lib/deno-apps/demo/main.ts, the deno-demo.service, deno-firstboot.service and nginx.service units, 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 a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 (HTTPS) and TCP 80 (redirects to HTTPS) from the clients your app serves. Nothing else needs to be open: the Deno app listens on loopback only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Deno 2.9.2 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger for CPU heavy apps), your key pair, and a security group as above. The image attaches its 20 GiB app volume automatically; you can make it bigger at launch.
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=deno}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Deno 2.9.2 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot generates the certificate and the API token, then the demo app and nginx start:
systemctl is-active deno-demo nginx
sudo test -f /var/lib/cloudimg/deno-firstboot.done && echo FIRSTBOOT_DONE
deno --version
active
active
FIRSTBOOT_DONE
deno 2.9.2 (stable, release, x86_64-unknown-linux-gnu)
v8 14.9.207.2-rusty
typescript 6.0.3
nginx listens on ports 80 and 443 on every address; the Deno app listens on loopback only:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/pid=[0-9]+,fd=[0-9]+/.../g' | sort -u
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:8000 users:(("deno",...))
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",...))
Step 4: Your instance's URL and API token
sudo cat /root/deno-info.txt
The file is readable by root only:
# Deno 2.9.2 - generated on first boot by deno-firstboot.service
# The API token is unique to this instance; none was baked into the image. Keep this file secret.
DENO_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/
DENO_API_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/api/status
DENO_API_TOKEN=<your-api-token>
DENO_TLS_CERT=/etc/nginx/tls/deno.crt
DENO_CERT_SHA256=<sha256-fingerprint>
DENO_APP_DIR=/var/lib/deno-apps/demo
#
# Call the demo API (token sent in a header, from this file):
# curl --cacert /etc/nginx/tls/deno.crt -H "Authorization: Bearer <DENO_API_TOKEN>" https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/api/status
# New API token: sudo deno-rotate-token
# Change the name it serves as: sudo deno-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/deno-2-9-2-on-ubuntu-24-04-aws/
The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve your own DNS name, see Step 9.
Step 5: Call the demo API
The landing document at / is public. /api/status answers only with the instance's token; it returns a live server time and an in process request counter, so you can see Deno running code on every request. These commands send the token from the root only file in a header, so it never appears on a command line:
H=$(sudo grep '^DENO_URL=' /root/deno-info.txt | cut -d= -f2 | sed 's#https://##; s#/##')
curl -s --cacert /etc/nginx/tls/deno.crt --resolve "$H:443:127.0.0.1" "https://$H/"
curl -s --cacert /etc/nginx/tls/deno.crt --resolve "$H:443:127.0.0.1" -w '%{http_code}\n' "https://$H/api/status"
for i in 1 2; do sudo sed -n 's/^DENO_API_TOKEN=\(.*\)/header = "Authorization: Bearer \1"/p' /root/deno-info.txt | curl -s --cacert /etc/nginx/tls/deno.crt --resolve "$H:443:127.0.0.1" -K - "https://$H/api/status"; done
{
"message": "Deno demo API running on cloudimg",
"denoVersion": "2.9.2",
"api": "/api/status (send Authorization: Bearer <DENO_API_TOKEN> from /root/deno-info.txt)"
}
{
"error": "missing or invalid API token"
}
401
{
"message": "authenticated request served by Deno",
"denoVersion": "2.9.2",
"v8Version": "14.9.207.2-rusty",
"typescriptVersion": "6.0.3",
"serverTime": "2026-10-19T22:16:15.170Z",
"requestCount": 1,
"clientIp": "127.0.0.1"
}
{
"message": "authenticated request served by Deno",
"denoVersion": "2.9.2",
"v8Version": "14.9.207.2-rusty",
"typescriptVersion": "6.0.3",
"serverTime": "2026-10-19T22:16:15.522Z",
"requestCount": 2,
"clientIp": "127.0.0.1"
}
From your own computer, open the DENO_URL in a browser (the certificate is self signed, so the browser asks you to confirm it the first time), or call the API with curl and the token from Step 4:
curl -k -H "Authorization: Bearer <your-api-token>" https://<public-ip>/api/status
Port 80 redirects to HTTPS on the configured name, and a host name the instance does not serve gets no answer at all (nginx closes the connection, shown by curl as 000):
H=$(sudo grep '^DENO_URL=' /root/deno-info.txt | cut -d= -f2 | sed 's#https://##; s#/##')
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/api/status"
curl -sk -o /dev/null -w '%{http_code}\n' --resolve "other.example:443:127.0.0.1" https://other.example/ || true
301 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/api/status
000
Step 6: Deno's permission sandbox
The demo app's unit grants exactly two permissions. You can read them from systemd:
systemctl cat deno-demo | grep -E '^(User|ExecStart|ExecStartPre|ProtectSystem|NoNewPrivileges|CapabilityBoundingSet)='
User=deno
ExecStartPre=+/usr/local/sbin/deno-demo-guard
ExecStart=/usr/local/bin/deno run --no-prompt --no-remote --no-npm --allow-net=127.0.0.1:8000 --allow-read=/etc/deno-demo/api-token /var/lib/deno-apps/demo/main.ts
NoNewPrivileges=true
ProtectSystem=strict
CapabilityBoundingSet=
Deno enforces these grants inside the program. To see it, run a small script that tries to read a file, read the environment and start a program, first with no grants and then with read access to one file only:
cat > /tmp/sandbox.ts <<'TS'
for (const [name, f] of [
["read /etc/hostname", () => Deno.readTextFileSync("/etc/hostname")],
["read the env", () => Deno.env.get("HOME")],
["run a program", () => new Deno.Command("id").outputSync()],
] as const) {
try { f(); console.log(`${name}: allowed`); } catch (e) { console.log(`${name}: ${(e as Error).name}`); }
}
TS
deno run --no-prompt --allow-read=/tmp/sandbox.ts /tmp/sandbox.ts
deno run --no-prompt --allow-read=/etc/hostname /tmp/sandbox.ts
read /etc/hostname: NotCapable
read the env: NotCapable
run a program: NotCapable
read /etc/hostname: allowed
read the env: NotCapable
run a program: NotCapable
NotCapable is Deno refusing the operation because it was not granted. Grant your own apps the narrowest flags that work (--allow-net=<host:port>, --allow-read=<path>, --allow-env=<NAME>); the Deno security documentation lists every flag.
Step 7: Deploy your own app
Put each app in its own directory on the app volume and give it its own systemd unit, with only the permissions it needs and the same metadata block as the demo. This example serves a TypeScript app on loopback port 8001:
sudo install -d -o root -g deno -m 0750 /var/lib/deno-apps/hello
sudo tee /var/lib/deno-apps/hello/main.ts >/dev/null <<'TS'
Deno.serve({ hostname: "127.0.0.1", port: 8001 }, () => new Response("Hello from my own Deno app\n"));
TS
sudo chown root:deno /var/lib/deno-apps/hello/main.ts && sudo chmod 0640 /var/lib/deno-apps/hello/main.ts
sudo tee /etc/systemd/system/deno-hello.service >/dev/null <<'UNIT'
[Unit]
Description=My Deno app
After=network-online.target
Wants=network-online.target
[Service]
User=deno
Group=deno
WorkingDirectory=/var/lib/deno-apps/hello
Environment=DENO_DIR=/var/cache/deno-hello DENO_NO_UPDATE_CHECK=1
CacheDirectory=deno-hello
ExecStart=/usr/local/bin/deno run --no-prompt --allow-net=127.0.0.1:8001 /var/lib/deno-apps/hello/main.ts
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload && sudo systemctl enable --now deno-hello.service
sleep 3; systemctl is-active deno-hello; curl -s http://127.0.0.1:8001/
active
Hello from my own Deno app
If your app imports remote modules or npm packages, cache them once with deno install as the deno user, or vendor them into the app directory, and keep --allow-net limited to the hosts the app really calls.
To publish the app on HTTPS, point nginx at it. Open the site file:
sudo nano /etc/nginx/sites-available/cloudimg-deno
In the HTTPS server block either change the upstream deno_backend at the top of the file to server 127.0.0.1:8001;, or add a path for the new app next to the existing location / block:
location /hello/ {
proxy_set_header Host $deno_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:8001/;
}
Then check the configuration and reload nginx:
sudo nginx -t && sudo systemctl reload nginx
Step 8: Replace the API token
deno-rotate-token writes a new random token, records it in /root/deno-info.txt and restarts the demo app; the old token stops working at once:
H=$(sudo grep '^DENO_URL=' /root/deno-info.txt | cut -d= -f2 | sed 's#https://##; s#/##')
OLD=$(sudo grep '^DENO_API_TOKEN=' /root/deno-info.txt | cut -d= -f2)
sudo deno-rotate-token
sleep 3
printf 'header = "Authorization: Bearer %s"\n' "$OLD" | curl -s -o /dev/null -w 'old token -> %{http_code}\n' --cacert /etc/nginx/tls/deno.crt --resolve "$H:443:127.0.0.1" -K - "https://$H/api/status"
sudo sed -n 's/^DENO_API_TOKEN=\(.*\)/header = "Authorization: Bearer \1"/p' /root/deno-info.txt | curl -s -o /dev/null -w 'new token -> %{http_code}\n' --cacert /etc/nginx/tls/deno.crt --resolve "$H:443:127.0.0.1" -K - "https://$H/api/status"
deno-rotate-token: new API token written (see /root/deno-info.txt); deno-demo restarted
old token -> 401
new token -> 200
Step 9: Your own DNS name and certificate
Point a DNS record at the instance, then set the name it serves. This mints a new self signed certificate for the name, updates nginx and /root/deno-info.txt, and keeps the name across reboots:
sudo deno-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running deno-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/tls/deno.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/tls/deno.key
sudo nginx -t && sudo systemctl reload nginx
Step 10: Check the security posture
The certificate was generated on this instance and names its own addresses; its private key is readable by root only:
H=$(sudo grep '^DENO_URL=' /root/deno-info.txt | cut -d= -f2 | sed 's#https://##; s#/##')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
sudo stat -c '%a %U:%G %n' /etc/nginx/tls/deno.key /etc/nginx/tls/deno.crt /root/deno-info.txt /etc/deno-demo/api-token
subject=CN = ec2-<public-ip-dashed>.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
600 root:root /etc/nginx/tls/deno.key
644 root:root /etc/nginx/tls/deno.crt
600 root:root /root/deno-info.txt
640 root:deno /etc/deno-demo/api-token
nginx takes the client address from the connection. A request that claims to come from somewhere else in X-Forwarded-For reaches the app, and the access log, with its real address:
H=$(sudo grep '^DENO_URL=' /root/deno-info.txt | cut -d= -f2 | sed 's#https://##; s#/##')
P=$(hostname -I | awk '{print $1}')
sudo sed -n 's/^DENO_API_TOKEN=\(.*\)/header = "Authorization: Bearer \1"/p' /root/deno-info.txt | curl -s --cacert /etc/nginx/tls/deno.crt --resolve "$H:443:$P" -H 'X-Forwarded-For: 203.0.113.9' -H 'X-Real-IP: 203.0.113.9' -K - "https://$H/api/status?spoof-test" | grep clientIp
sudo grep -h 'spoof-test' /var/log/nginx/access.log | tail -1 | cut -d' ' -f1-9
"clientIp": "<private-ip>",
<private-ip> - - [19/Oct/2026:22:16:49 +0000] "GET /api/status?spoof-test HTTP/1.1" 200
The demo app, nginx and the first boot unit cannot reach the instance metadata service (169.254.169.254), so code running in them cannot read the instance's role credentials:
systemctl show deno-demo nginx deno-firstboot -p IPAddressDeny
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
Step 11: Logs
The demo app writes to the journal; nginx logs every request, client address first, to /var/log/nginx/access.log:
sudo journalctl -u deno-demo --no-pager -o cat | grep -E 'listening|Started' | tail -2
sudo journalctl -u deno-firstboot --no-pager -o cat | tail -2
Started deno-demo.service - Deno 2.9.2 demo API (cloudimg; loopback only, behind nginx TLS).
deno-demo listening on 127.0.0.1:8000 (loopback only)
[2026-10-19T22:15:44+0000] deno-firstboot: First boot complete; the Deno app will be served at https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/ with a per-instance API token
Finished deno-firstboot.service - cloudimg Deno first boot (per-instance TLS certificate and API token).
Step 12: Server components
| Component | Path |
|---|---|
| Deno binary | /usr/local/bin/deno |
| Demo app | /var/lib/deno-apps/demo/main.ts |
| App volume (20 GiB EBS) | /var/lib/deno-apps |
| API token (app copy) | /etc/deno-demo/api-token (mode 0640, group deno) |
| nginx site and certificate | /etc/nginx/sites-available/cloudimg-deno, /etc/nginx/tls/ |
| Service units | deno-demo.service, deno-firstboot.service, nginx.service |
| Metadata block | /etc/systemd/system/<unit>.service.d/50-cloudimg-imds-deny.conf |
| Helpers | /usr/local/sbin/deno-rotate-token, /usr/local/sbin/deno-set-hostname |
| Licence | /usr/share/doc/deno/LICENSE.md |
| Instance details | /root/deno-info.txt (mode 0600) |
ls -l /usr/share/doc/deno | awk 'NR>1{print $1,$5,$9}'
sha256sum /usr/local/bin/deno
-rw-r--r-- 1074 LICENSE.md
-rw-r--r-- 119 VERSION
5bc8a7a4a628360b391ddeac2efac7dec9e670b33156d831bf1e899070655173 /usr/local/bin/deno
The binary comes from deno-x86_64-unknown-linux-gnu.zip of the v2.9.2 release, checked against the release's published SHA-256 checksum.
Step 13: Updating Deno
Ubuntu's security updates install automatically. Deno is a single binary, so you update it by replacing the file with a newer official release after checking its published checksum, then restarting your apps:
V=<version>
curl -fsSL -o /tmp/deno.zip "https://github.com/denoland/deno/releases/download/v$V/deno-x86_64-unknown-linux-gnu.zip"
curl -fsSL "https://github.com/denoland/deno/releases/download/v$V/deno-x86_64-unknown-linux-gnu.zip.sha256sum"
sha256sum /tmp/deno.zip
unzip -o /tmp/deno.zip -d /tmp && sudo install -m 0755 /tmp/deno /usr/local/bin/deno && sudo systemctl restart deno-demo
Support
cloudimg provides 24/7 technical support for this image by email and live chat: deployment, HTTPS and certificates, deploying your apps, upgrades and troubleshooting. Contact support@cloudimg.co.uk.
Deno is a trademark of Deno Land Inc. cloudimg is not affiliated with or endorsed by Deno Land Inc. or the Deno project.