Application Servers AWS

Spin WebAssembly Application Runtime on AWS User Guide

| Product: Spin 4.0.2 WebAssembly Application Runtime on AWS

Overview

This guide covers the deployment and configuration of Spin on AWS using cloudimg AWS Marketplace images. Spin is an open source runtime for building and running WebAssembly applications and microservices. It is a single self contained binary that loads a WebAssembly component and serves it over HTTP, so you run event driven microservices with the fast cold starts, strong sandboxing and tiny footprint of WebAssembly, without managing a language runtime or a container image for each service.

The image installs Spin 4.0.2 from the official prebuilt release binary, run under systemd as an unprivileged spin user. The Spin runtime is bound to the loopback interface on port 3000, and nginx terminates HTTPS on port 443 and reverse proxies requests to it, so the WebAssembly application itself is never directly exposed to the network. Plain HTTP on port 80 permanently redirects to HTTPS. A small prebuilt demo WebAssembly HTTP component ships in the application directory so a freshly launched instance immediately demonstrates end to end WebAssembly execution.

Secure by default. The Spin application is bound to loopback only, so the sole network reachable application surface is the nginx TLS reverse proxy. Components run inside the WebAssembly sandbox with no ambient access to the host. On the first boot of every instance a one shot service generates a fresh per instance self signed TLS certificate bound to that instance's address, so the server answers over HTTPS immediately. Spin has no login, so no shared or default credential of any kind ships in the image.

What is included:

  • Spin 4.0.2 from the official prebuilt release binary, run under systemd as the unprivileged spin user (spin.service), bound to 127.0.0.1:3000

  • nginx terminating HTTPS on port 443 and reverse proxying to the Spin runtime, with an HTTP to HTTPS permanent redirect on port 80 (nginx.service)

  • A demo WebAssembly HTTP component at /opt/spin-app/index.wasm that echoes your request path and reports a value it computes at request time, proving the Spin runtime executes the component rather than serving a static file (replace it with your own component)

  • The whole runtime in a single prebuilt binary with no separate language runtime to install and no container image to build for each microservice

  • A dedicated EBS data volume mounted at /opt/spin-app, so your components and their assets grow on a disk you can resize independently of the operating system disk

  • A per instance self signed TLS certificate generated on first boot, ready for you to front with your own domain and a trusted certificate

  • The Spin application manifest in one readable file, /opt/spin-app/spin.toml, that you keep in version control

Prerequisites

  • An active AWS account and a subscription to the Spin listing on AWS Marketplace

  • An EC2 key pair in your target region, and a VPC subnet with a route to the internet if your users reach the instance directly

  • A security group allowing inbound TCP 22 (administration) from your management network only, plus TCP 443 (HTTPS) and TCP 80 (the HTTP redirect) from the clients that will reach your application

Recommended instance type: m5.large (2 vCPU, 8 GB RAM). Size up for higher request concurrency or heavier components.

Connecting to your instance

Connect over SSH on port 22 using the EC2 key pair you selected at launch. The login user depends on the operating system variant you launched:

Operating system variant SSH login user Example
Ubuntu 24.04 ubuntu ssh -i my-key.pem ubuntu@<public-ip>

Spin has no login and no password, so there is no credential to retrieve. The first boot writes an informational note to /root/spin-info.txt recording this instance's endpoint details.

Step 1: Launch the instance

  1. Open the Spin listing on AWS Marketplace and choose Continue to Subscribe, then Continue to Configuration and Continue to Launch.
  2. Choose the region, the m5.large instance type (or larger), your VPC subnet and your EC2 key pair.
  3. Attach a security group that allows inbound TCP 22 from your management network and TCP 443 and TCP 80 from your users.
  4. Launch, then note the instance's public IPv4 address.

Step 2: First boot

On the first boot the spin-firstboot.service one shot unit resolves this instance's address from EC2 instance metadata, generates a fresh per instance self signed TLS certificate bound to it, writes the informational note to /root/spin-info.txt, then systemd starts spin.service and nginx.service. This takes a few seconds. SSH in as the login user for your variant:

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

Step 3: Confirm the services are running

Confirm spin.service and nginx.service are active, check the Spin version, and confirm the runtime is bound to loopback on port 3000 while nginx is listening on port 443 (HTTPS) and port 80 (the redirect):

sudo systemctl is-active spin nginx
/usr/local/bin/spin --version
sudo ss -tln | grep -E '127.0.0.1:3000|:443 |:80 '

Real output from a running instance:

active
active
spin 4.0.2 (bfc7543 2026-06-23)
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*
LISTEN 0      1024       127.0.0.1:3000      0.0.0.0:*
LISTEN 0      511             [::]:80           [::]:*
LISTEN 0      511             [::]:443          [::]:*

Both services report active, the binary reports spin 4.0.2, the Spin runtime listens on 127.0.0.1:3000 only, and nginx listens on ports 443 and 80 on all interfaces.

Step 4: Verify WebAssembly execution over HTTPS

The demo component at /opt/spin-app/index.wasm is executed by the Spin runtime for every request: it echoes the request path back to you and reports a value it computes at request time (6 x 7), so you can prove the Spin runtime is executing the WebAssembly component rather than serving a static file. Fetch it over HTTPS (the -k flag accepts the per instance self signed certificate):

curl -ks https://127.0.0.1/hello-from-cloudimg \
  | sed -e 's|<style>.*</style>||' -e 's|</title>|\n|g' -e 's|</h1>|\n|g' \
        -e 's|</p>|\n|g' -e 's|</th>|: |g' -e 's|</tr>|\n|g' -e 's/<[^>]*>//g' \
  | grep -iE 'produced by a WebAssembly|^Runtime|^Request path|^WASM execution'
curl -ksI https://127.0.0.1/ | grep -i x-cloudimg-spin

Real output from a running instance:

This response was produced by a WebAssembly component executed by the Spin runtime.
Runtime: Fermyon Spin (WebAssembly component)
Request path (echoed live): /hello-from-cloudimg
WASM execution check (6 x 7): wasm-exec-check: 42
x-cloudimg-spin: executed-42

The path you requested is echoed back, which a static file server cannot do, the 6 x 7 check evaluates to 42 on the server, and the response carries an x-cloudimg-spin: executed-42 header.

Now open https://<public-ip>/ in a browser. Your browser warns about the self signed certificate until you front the instance with a trusted certificate (Step 8); accept the warning to continue. The demo landing page confirms the runtime and echoes your request path:

A web browser showing the demo Spin landing page served over HTTPS, headed Spin on cloudimg, confirming the response was produced by a WebAssembly component executed by the Spin runtime, with a table listing the runtime as Fermyon Spin, the request path echoed live as a single forward slash, the full request URL, and the WASM execution check of 6 times 7 equal to 42, followed by quickstart instructions for deploying your own WebAssembly component

Request a different path and the component echoes that path instead, live, on every request:

The same demo Spin page in a browser after requesting the path slash hello-from-cloudimg, with the request path row now echoing slash hello-from-cloudimg and the full request URL row showing the same path, demonstrating that the WebAssembly component is executed per request rather than serving a static file

The shipped manifest maps the wildcard route /... to the demo component, so any path is routed to it, including deep API style paths:

The demo Spin page in a browser after requesting a deep path of slash api slash v1 slash orders slash 42, with the request path row echoing that full path, showing that the wildcard route in the Spin manifest sends every path to the component

Step 5: HTTP to HTTPS redirect

Plain HTTP on port 80 permanently redirects (301) to HTTPS, so a user who types the bare address still lands on the encrypted endpoint:

curl -ksI http://127.0.0.1/ | grep -iE 'HTTP/|location'

Real output from a running instance:

HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/

Step 6: Confirm the runtime is not reachable from the network

The Spin application listens on 127.0.0.1:3000 only, so the plaintext runtime port is never exposed. Only the nginx TLS proxy is reachable. Confirm the binding, then prove the port genuinely refuses a connection on the instance's own network address:

sudo ss -tln | grep ':3000 '
HOSTIP=$(hostname -I | awk '{print $1}')
if curl -s --max-time 5 -o /dev/null "http://${HOSTIP}:3000/"; then
    echo "UNEXPECTED: the Spin application port answered off loopback"; exit 1
else
    echo "refused (exit $?) - correct: the Spin runtime is bound to loopback only"
fi

Real output from a running instance:

LISTEN 0      1024       127.0.0.1:3000      0.0.0.0:*
refused (exit 7) - correct: the Spin runtime is bound to loopback only

Keep it that way: terminate TLS at nginx rather than exposing the runtime port directly.

Step 7: Review the application directory and manifest

The application directory /opt/spin-app is a dedicated EBS volume, separate from the operating system disk, so you can resize it as your components and assets grow. The Spin manifest is one readable file:

findmnt -no SOURCE,TARGET,FSTYPE,SIZE /opt/spin-app
sudo cat /opt/spin-app/spin.toml

Real output from a running instance:

/dev/nvme1n1 /opt/spin-app ext4   19.5G

spin_manifest_version = 2

[application]
name = "cloudimg-spin-demo"
version = "0.1.0"
authors = ["cloudimg"]
description = "cloudimg Spin demo WebAssembly HTTP component (proves WASM execution)"

[[trigger.http]]
route = "/..."
component = "cloudimg-spin-demo"

[component.cloudimg-spin-demo]
source = "index.wasm"
allowed_outbound_hosts = []

The [[trigger.http]] section maps an HTTP route to a component and [component.*] points at the compiled WebAssembly file, so you can host several components on one instance and route between them by path.

Step 8: Deploy your own WebAssembly component

Build a WebAssembly component (Spin supports Rust, JavaScript, TypeScript, Python, Go and more), copy the compiled .wasm onto the application volume, point the manifest at it, then restart the service:

# copy your compiled component onto the dedicated application volume
sudo cp ./my-component.wasm /opt/spin-app/index.wasm

# (optional) edit /opt/spin-app/spin.toml to change the route or add components

# apply the change
sudo systemctl restart spin

The image ships without a language toolchain, so build your component on your own workstation or CI runner and copy the compiled artifact across.

Step 9: Review the first boot note

The first boot records this instance's endpoint details. It holds no secret, because Spin has no login:

sudo grep -vE '^[[:space:]]*$' /root/spin-info.txt \
  | sed '/127\.0\.0\.1/!s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/INSTANCE-IP/g'

Real output from a running instance. The sed above masks this instance's own public address as INSTANCE-IP; on your instance those lines carry its real address:

# Spin (cloudimg AWS Marketplace image)
# Spin is a WebAssembly application runtime with NO login and NO password.
# This note is informational and holds no secret - it is unique to THIS instance.
spin.version=4.0.2
spin.binary=/usr/local/bin/spin
spin.app_manifest=/opt/spin-app/spin.toml
spin.app_component=/opt/spin-app/index.wasm
spin.listen=127.0.0.1:3000  (loopback only; nginx terminates TLS in front)
nginx.tls_cert=/etc/nginx/cloudimg/certs/site.crt  (per-instance self-signed; SAN=INSTANCE-IP)
site_ip=INSTANCE-IP
HTTPS_URL=https://INSTANCE-IP/
HTTP_URL=http://INSTANCE-IP/   (permanently redirects to the HTTPS URL)
#
# Secure by default: the Spin application server listens on loopback only; the only
# network-reachable application surface is the nginx TLS reverse proxy on :443.
#
# Deploy your own WebAssembly component: build a WASM component, copy it into
#   /opt/spin-app, point /opt/spin-app/spin.toml at it, then: sudo systemctl restart spin
# User guide: https://www.cloudimg.co.uk/guides/spin-aws/

Step 10: Confirm the certificate is unique to this instance

Every instance generates its own certificate at first boot, so no certificate is baked into the image and no two instances share one. Confirm the subject and validity window:

sudo openssl x509 -in /etc/nginx/cloudimg/certs/site.crt -noout -subject -dates \
  | sed 's/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/INSTANCE-IP/g'

Real output from a running instance, with the address masked by the sed above:

subject=O = cloudimg, CN = INSTANCE-IP
notBefore=Jul 26 02:17:48 2026 GMT
notAfter=Jul 23 02:17:48 2036 GMT

The common name is this instance's own address, generated at its own first boot.

Step 11: Front your instance with a trusted certificate

The image ships with a per instance self signed certificate so it answers over HTTPS immediately. For production, point a domain's DNS record at this instance and replace the certificate that nginx serves with a trusted one (for example from your own certificate authority, an ACME client such as Certbot, or by terminating TLS at an AWS Application Load Balancer in front of the instance). The nginx site is at /etc/nginx/sites-available/cloudimg-spin; update the ssl_certificate and ssl_certificate_key paths and reload nginx:

sudo nano /etc/nginx/sites-available/cloudimg-spin
sudo nginx -t && sudo systemctl reload nginx

Step 12: Security recommendations

  • Restrict the management port. Allow inbound TCP 22 only from your management network, never 0.0.0.0/0.
  • Front your users with HTTPS only. Expose TCP 443 to your users and TCP 80 only so the redirect works. Switch to a trusted certificate for your own domain (Step 11) so browsers do not warn.
  • Keep the runtime on loopback. This image binds the Spin application to 127.0.0.1:3000 so only the nginx TLS proxy is network reachable. Leave it that way.
  • Keep the OS patched. The image ships fully patched with unattended-upgrades enabled, so security updates continue automatically. Reboot after kernel updates.
  • Run your components as least privilege. spin.service already runs as the unprivileged spin user with systemd sandboxing, and WebAssembly components run inside the Spin sandbox with no ambient host access. Declare only the outbound hosts a component genuinely needs in allowed_outbound_hosts.
  • Snapshot the application volume. /opt/spin-app is a dedicated EBS volume; include it in your EBS snapshot policy and enable encryption at rest if your policy requires it.

Step 13: Support and Licensing

Spin is open source software distributed under the Apache License 2.0. This cloudimg image bundles Spin 4.0.2 with nginx TLS termination, fully patched and hardened. cloudimg provides the packaging, configuration, security hardening and support; it does not modify the upstream Spin licence.

Deploy on AWS

Launch Spin from the cloudimg AWS Marketplace listing and follow this guide to deploy your WebAssembly component in minutes.

Need Help?

cloudimg provides 24/7 technical support for this Spin image. Contact support@cloudimg.co.uk for help with building WebAssembly components in Rust, JavaScript, TypeScript, Python and Go, the Spin manifest and HTTP routing, TLS and certificate options, data volume sizing, and Spin version upgrades.

All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.