Deno 2.9 on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of Deno 2.9 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Deno is a modern, secure-by-default JavaScript/TypeScript runtime: built-in TypeScript support with no separate compile step, a single binary with no node_modules, and — its defining feature — a permissions sandbox: a Deno program has no ambient access to the filesystem, network, or environment unless you explicitly grant it.
The image installs the official pinned Deno v2.9.2 release binary from the denoland/deno GitHub release, verified against a sha256 checksum, to /usr/local/bin/deno (already on the default PATH for every user). Deno has no admin console or login — there is nothing to authenticate to — so this image ships a small demo HTTP API pre-deployed and running under systemd, proving the runtime is genuinely executing code on every request (a live server timestamp and a request counter), not just serving static files. The demo's systemd unit grants only --allow-net=127.0.0.1:8000 — no filesystem, environment, or subprocess access — demonstrating Deno's sandboxing model correctly, not as a shortcut.
What is included:
-
Deno v2.9.2 (official release binary, sha256-pinned) installed to
/usr/local/bin/deno, onPATHfor every user -
deno-demo.servicesystemd unit auto-starting on boot, running as the unprivilegeddeno:denosystem user with only--allow-net=127.0.0.1:8000granted -
deno-firstboot.servicesystemd oneshot that starts the demo app + nginx and confirms the demo answers before completing -
The demo API bound to loopback only (
127.0.0.1:8000) — nginx fronts it on:80 -
Unauthenticated
/healthzendpoint (nginx-native, HTTP 200) for load balancer / probe checks -
A pre-deployed demo
Deno.serve()app at/var/lib/deno-apps/demo/main.tsthat returns a live server timestamp and a request counter on every call -
/var/lib/deno-appson a dedicated 20 GiB Azure data disk — deployed app code and any app-level data/logs survive independently of the OS disk -
Ubuntu 24.04 LTS base with latest security patches applied at build time
-
Azure Linux Agent for seamless cloud integration and SSH key injection
-
24/7 cloudimg support with guaranteed 24 hour response SLA
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in target region
-
Subscription to the Deno 2.9 listing on Azure Marketplace
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development and light API workloads. CPU-bound or high-concurrency workloads should use Standard_D4s_v5 (4 vCPU, 16 GB RAM) or larger.
Step 1: Deploy from the Azure Portal
Search Deno in Marketplace, select the cloudimg publisher, click Create. NSG rules: TCP 22 (admin), TCP 80 (HTTP), TCP 443 (reserved for TLS) from your client networks.
Step 2: Deploy from the Azure CLI
RG="deno-prod"; LOCATION="eastus"; VM_NAME="deno-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/deno-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name deno-vnet --address-prefix 10.100.0.0/16 --subnet-name deno-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name deno-nsg
az network nsg rule create -g "$RG" --nsg-name deno-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name deno-nsg --name allow-http --priority 110 \
--destination-port-ranges 80 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name deno-nsg --name allow-https --priority 120 \
--destination-port-ranges 443 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name deno-vnet --subnet deno-subnet --nsg deno-nsg --public-ip-sku Standard
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
deno-demo.service, nginx.service and deno-firstboot.service all start automatically on first boot — there is no admin account to set up, because Deno has no login.
Step 4: Verify the Deno Demo and nginx Services
sudo systemctl status deno-demo.service --no-pager
sudo systemctl status nginx.service --no-pager
sudo test -f /var/lib/cloudimg/deno-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tln | grep -E ':(80|8000) '
Expected output:
● deno-demo.service - Deno demo HTTP API (cloudimg)
Active: active (running)
Main PID: 2107 (deno)
CGroup: /system.slice/deno-demo.service
└─2107 /usr/local/bin/deno run --allow-net=127.0.0.1:8000 /var/lib/deno-apps/demo/main.ts
...Listening on http://127.0.0.1:8000/
● nginx.service - A high performance web server and a reverse proxy server
Active: active (running)
FIRSTBOOT_DONE
LISTEN 0 511 127.0.0.1:8000 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 [::]:80 [::]:*
Notice the demo app is bound to 127.0.0.1:8000 only — Deno is never directly reachable from the network; nginx on :80 is the only public entry point.

Step 5: Check the Health Endpoint
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz
Expected output: 200. This unauthenticated endpoint is served directly by nginx (not proxied to Deno) so it always answers even under load — use it for a load balancer or uptime probe.
Step 6: Call the Pre-Deployed Demo API
Deno has no admin UI or login, so the value-proof for this image is a small demo Deno.serve() API pre-deployed at the server root. Request it twice in a row:
curl -s http://127.0.0.1/
curl -s http://127.0.0.1/
Expected output — serverTime and requestCount both change on every call:
{
"message": "Deno demo API running on cloudimg",
"denoVersion": "2.9.2",
"serverTime": "2026-07-09T18:12:32.026Z",
"requestCount": 1
}
{
"message": "Deno demo API running on cloudimg",
"denoVersion": "2.9.2",
"serverTime": "2026-07-09T18:12:32.034Z",
"requestCount": 2
}
This proves the runtime is genuinely executing code on every request — an in-process counter and a live server timestamp cannot come from a cached or static file. This is the actual value a runtime like Deno adds over serving a plain static site.

From your workstation (NSG must allow TCP 80):
open http://<vm-ip>/
Step 7: Confirm the Deno Version and Least-Privilege Permissions
deno --version
find /var/lib/deno-apps -maxdepth 2 -not -path '*/.cache*'
cat /etc/systemd/system/deno-demo.service
Expected output:
deno 2.9.2 (stable, release, x86_64-unknown-linux-gnu)
v8 14.9.207.2-rusty
typescript 6.0.3
---
/var/lib/deno-apps
/var/lib/deno-apps/demo
/var/lib/deno-apps/demo/main.ts
/var/lib/deno-apps/lost+found
---
[Unit]
Description=Deno demo HTTP API (cloudimg)
After=network.target
[Service]
Type=simple
User=deno
Group=deno
WorkingDirectory=/var/lib/deno-apps/demo
ExecStart=/usr/local/bin/deno run --allow-net=127.0.0.1:8000 /var/lib/deno-apps/demo/main.ts
Restart=on-failure
RestartSec=10
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
Notice the ExecStart line grants only --allow-net=127.0.0.1:8000 — scoped to the exact loopback address and port the demo binds. There is no --allow-all, no unscoped --allow-net, and no --allow-read/--allow-write/--allow-env/--allow-run. This is Deno's permissions sandbox working as designed: the runtime refuses filesystem, environment, or subprocess access to this program because it was never granted.

Step 8: Deploy Your Own TypeScript/JavaScript App
Drop your own app into a new directory under /var/lib/deno-apps/, then add a systemd unit granting only the permissions it actually needs:
# Copy your app to the VM
scp -r ./myapp azureuser@<vm-ip>:/tmp/myapp
ssh azureuser@<vm-ip> 'sudo mv /tmp/myapp /var/lib/deno-apps/myapp && sudo chown -R deno:deno /var/lib/deno-apps/myapp'
Then create /etc/systemd/system/myapp.service on the VM, following the same least-privilege pattern as the demo unit — grant only what your app needs (e.g. add --allow-read=/var/lib/deno-apps/myapp/data if it reads local files, or --allow-env=API_KEY for a single environment variable — never reach for --allow-all):
[Unit]
Description=My Deno app
After=network.target
[Service]
Type=simple
User=deno
Group=deno
WorkingDirectory=/var/lib/deno-apps/myapp
ExecStart=/usr/local/bin/deno run --allow-net=127.0.0.1:8001 /var/lib/deno-apps/myapp/main.ts
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
(Run on your own VM against your own unit file — not executed as part of this guide's verification, since myapp.service doesn't exist on a fresh image.)
Then add a second nginx location block (or a new port) to route to it — the demo app on :8000 can stay running alongside your own app on a different loopback port.
Step 9: Read the Endpoint Notes
At first boot, deno-firstboot.service writes a non-secret notes file documenting the deployed URLs (Deno has no credentials to rotate):
sudo cat /var/lib/cloudimg/deno-endpoints.notes
Expected output:
# Deno — Endpoint Notes
# Generated on first boot by deno-firstboot.service
#
# Deno itself has no admin UI or login — there is nothing to authenticate to.
#
DENO_DEMO_URL=http://<vm-ip>/
DENO_HEALTHZ_URL=http://<vm-ip>/healthz
DENO_BINARY=/usr/local/bin/deno
DENO_APPS_DIR=/var/lib/deno-apps
DEPLOY_YOUR_OWN_APP=Drop a .ts/.js app into /var/lib/deno-apps/<name>/, add a systemd unit granting only the permissions it needs (e.g. --allow-net), then enable + start it.

Step 10: Server Components
| Component | Path |
|---|---|
| Deno binary | /usr/local/bin/deno |
| App data disk | /var/lib/deno-apps |
| Demo app | /var/lib/deno-apps/demo/main.ts |
| Logs | systemd journal (journalctl -u deno-demo.service) |
| Systemd unit | /etc/systemd/system/deno-demo.service |
| Firstboot script | /usr/local/sbin/deno-firstboot.sh |
| Firstboot service | /etc/systemd/system/deno-firstboot.service |
| Endpoint notes | /var/lib/cloudimg/deno-endpoints.notes (mode 0644) |
| Firstboot sentinel | /var/lib/cloudimg/deno-firstboot.done |
| nginx vhost | /etc/nginx/sites-available/cloudimg-deno |
Step 11: Managing the Deno Demo Service
sudo systemctl status deno-demo.service --no-pager
sudo systemctl restart deno-demo.service
sudo journalctl -u deno-demo.service -n 50 --no-pager
Deno's stdout/stderr go to the systemd journal (journalctl -u deno-demo.service) — use sudo journalctl -u deno-demo.service -f to follow the log live.
Step 12: Security Recommendations
-
Never expose port 8000 directly — it is bound to loopback only by design; always go through nginx (
:80) or add a TLS-terminating reverse proxy on:443 -
Add TLS by fronting nginx with Let's Encrypt/Certbot
-
Restrict NSG so
:80/:443are reachable only from your client networks if this is an internal service -
Always grant the minimum permissions your app needs — start from
--allow-net=<host>:<port>and add only what's required (--allow-read=<path>,--allow-env=<VAR>), never--allow-all -
Remove or replace the demo app (
/var/lib/deno-apps/demo/) once you've deployed your own application -
Patch the OS monthly with
apt-get update && apt-get upgrade && reboot -
Run Deno apps as a non-privileged user (already done —
deno:denosystem user)
Step 13: Support and Licensing
Deno is licensed under the MIT License. There is no per-CPU or per-deployment fee.
cloudimg provides commercial support for this image separately from the upstream project.
- Email: support@cloudimg.co.uk
- Website: www.cloudimg.co.uk
- Support hours: 24/7 with guaranteed 24 hour response SLA
Deploy on Azure
Launch Deno 2.9 on Ubuntu 24.04 with 24/7 support from cloudimg.
View on Marketplace
Need Help?
Our support team is available 24/7.
support@cloudimg.co.uk