Deployment guide

Deno 2.9.2

Overview

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 and ships as a single binary with a formatter, linter, test runner and bundler built in. It supports npm packages and web-standard APIs such as fetch and Request. Its defining feature is that it 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.

This guide covers the Deno 2.9.2 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud app stacks and runtimes listing on AWS Marketplace. You pick it as the software version when you subscribe and launch. The image gives you a Deno server that is safe to put on a public address from the first minute.

What you get:

  • 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/status endpoint 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, and no remote or npm code at run time.
  • Loopback only, behind nginx. The app listens on 127.0.0.1:8000, and nginx is the only service on the network. nginx answers only for your instance's own host names and takes the host from configuration, not from the request. It overwrites X-Forwarded-For and X-Real-IP with the real client address.
  • Sandboxed and fail closed. The app runs as the unprivileged deno user 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-apps is a dedicated 20 GiB EBS volume that 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, license 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

You need:

  • 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, because the Deno app listens on loopback only.
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 select your Region, then choose 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, and you can make it bigger at launch.

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

Connect to your instance

Connect over SSH. The SSH user for Deno 2.9.2 on Ubuntu 24.04 is ubuntu:

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

Check the services

First boot generates the certificate and the API token, and then the demo app and nginx start. Confirm that both services are active and that first boot finished:

systemctl is-active deno-demo nginx
sudo test -f /var/lib/sac/data/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, while 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",...))
4

Your instance's URL and API token

Print your instance's URL and API token. The file is readable by root only:

sudo cat /root/deno-info.txt

The file looks like this:

# 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://secureanycloud.com/stacks/deno-2-9-2/

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.

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 sac", "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. Alternatively, 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. A host name the instance does not serve gets no answer at all: nginx closes the connection, which curl shows 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
6

Deno's permission sandbox

The demo app's unit grants exactly two permissions. 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 this, run a small script that tries to read a file, read the environment and start a program. Run it 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 means Deno refused 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.

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. 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/sac-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
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
9

Your own DNS name and certificate

Point a DNS record at the instance, then set the name it serves. The command 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
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 = sac 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 still 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
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 (sac; 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 - sac Deno first boot (per-instance TLS certificate and API token).
12

Server components

These are the key components and their paths:

  • 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/sac-deno, /etc/nginx/tls/
  • Service units: deno-demo.service, deno-firstboot.service, nginx.service
  • Metadata block: /etc/systemd/system/<unit>.service.d/50-sac-imds-deny.conf
  • Helpers: /usr/local/sbin/deno-rotate-token, /usr/local/sbin/deno-set-hostname
  • License: /usr/share/doc/deno/LICENSE.md
  • Instance details: /root/deno-info.txt (mode 0600)

Check the license files and the binary checksum:

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.

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. Check the release's published checksum first, then restart 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

SecureAnyCloud provides 24/7 technical support for this image by email and live chat. Support covers deployment, HTTPS and certificates, deploying your apps, upgrades and troubleshooting. Contact sales@secureanycloud.com.

Deno is a trademark of Deno Land Inc. SecureAnyCloud is not affiliated with or endorsed by Deno Land Inc. or the Deno project.

Deploy on AWS

Launch Deno 2.9.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