AWS Lightsail is the one corner of Amazon Web Services where a flat $5 a month buys a whole virtual server. CloudMart’s October 20, 2026 check against the AWS Price List API lists that plan as 512 MB of RAM, 2 vCPUs, 20 GB of SSD and 1 TB of monthly transfer. This AWS Lightsail setup tutorial turns that plan into a locked-down Linux web server in 13 steps, and it takes about 60 minutes if you paste the commands as written.
You will finish with an Ubuntu instance on a Lightsail static IP, a firewall that only admits you on SSH, an app behind Nginx with HTTPS, daily automatic snapshots, CPU alarms, and a second instance behind a Lightsail load balancer. Every step runs from the AWS CLI, so you can save the commands as a script and rebuild the whole stack later. If you are still choosing a provider, our DigitalOcean vs Vultr vs Lightsail comparison covers the IOPS and price differences before you commit.
One warning up front. Lightsail prices changed shape during 2026, and AWS’s own pages disagree on the entry plan (more on that below). Treat every price here as a snapshot from October 25, 2026, and confirm it in the console before you click Create.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What You Will Build With AWS Lightsail
The finished project has six moving parts, and each one maps to a step you can test on its own. A Linux instance named web-01 runs Ubuntu, Nginx, and a 40-line Python service. A static IP keeps the address stable across reboots and rebuilds. The built-in Lightsail firewall opens ports 80 and 443 to the world and port 22 only to your current address.
On top of that sits a DNS zone with an A record, a Let’s Encrypt certificate on the instance, and an automatic snapshot schedule. The last stage adds web-02 and a Lightsail load balancer with a free managed certificate, so a failed instance no longer takes the site down.
Why bother with Lightsail instead of EC2? The 2025 Stack Overflow Developer Survey found that 43.3% of respondents use AWS, which means most teams already hold an AWS account. Lightsail lets them run a small site, a staging box, a hobby API, or a game server without learning VPCs, security groups, and Elastic IP billing rules first. Our Palworld cloud server walkthrough uses the same pattern for a private game host.
Output blocks in this tutorial show representative results, so IDs, timestamps, and IP addresses will differ on your account.
The trade-off is control. Lightsail hides the VPC, caps you at fixed plan sizes, and exposes fewer knobs than EC2. For one to five servers that is a feature. For fifty it becomes a ceiling, and a later section covers when to move on.
| Step | What you do | Time |
|---|---|---|
| — | — | — |
| 1 | Install the AWS CLI and create a scoped profile | 8 min |
| 2 | Pick a region, plan and blueprint | 4 min |
| 3 | Generate an Ed25519 SSH key | 2 min |
| 4 | Create the instance with a launch script | 6 min |
| 5 | Attach a static IP | 2 min |
| 6 | Lock the Lightsail firewall | 4 min |
| 7 | Harden SSH and add swap | 6 min |
| 8 | Deploy the app behind Nginx | 7 min |
| 9 | Create the DNS zone and A record | 5 min |
| 10 | Issue the HTTPS certificate | 4 min |
| 11 | Enable automatic snapshots and test a restore | 6 min |
| 12 | Add CPU alarms with email alerts | 4 min |
| 13 | Put two instances behind a load balancer | 12 min |
What AWS Lightsail Costs in October 2026
Lightsail bills each instance by the hour, capped at the plan’s monthly price, and the plan bundles RAM, vCPUs, SSD storage, and a transfer allowance. That cap is the whole appeal. A forgotten instance can never cost more than its monthly number, unlike a forgotten EC2 box with a fat EBS volume and a public IPv4 charge.
AWS’s pricing page, as fetched this week, lists general-purpose Linux plans at $5, $7, $12, $24, and $44 a month, with larger plans running up to $1,764. CloudMart’s ladder adds the 1 GB, 2 GB, 4 GB, and 8 GB specs for those middle tiers and notes that the $7 plan now ships 2 vCPUs and 40 GB of SSD. Older comparison posts still list the $7 plan with 1 vCPU and 20 GB, so check the date on anything you read.
Here is the catch with the entry price. AWS’s Lightsail FAQ still advertises a plan from $0.0047 an hour, capped at $3.50 a month, and Surfercloud’s October 2026 benchmark repeats $3.50 for Lightsail’s Linux entry tier. The pricing page shows $5 for the smallest public-IPv4 general-purpose plan. The sources I checked do not explain the gap, so budget $5 and treat anything lower as a bonus.
The table below uses only line items AWS publishes on its own pricing pages. The totals are plain addition.
| Line item | Price | Notes |
|---|---|---|
| — | — | — |
| Entry general-purpose instance (public IPv4) | $5 per month | Hourly billing, capped at the monthly price |
| Second identical instance | $5 per month | Needed for the load-balancer stage |
| Lightsail load balancer | $18 per month | Free managed TLS certificates, AWS handles renewal |
| Static IP, attached to an instance | $0 | One account can hold five |
| Static IP, unattached | $0.005 per hour | Billed only after the address has been unattached for more than one hour |
| Automatic snapshots | Free to enable | You pay for the snapshot storage |
| Block storage | $0.10 per GB per month | Starts at 8 GB |
| Outbound transfer past the plan allowance | From $0.09 per GB | Varies by region |
| Container service (for comparison) | $7 per month | Includes a 500 GB monthly transfer quota |
For the finished project, two instances plus the load balancer come to $28 a month before snapshot storage. AWS’s own worked example for two instances and 100 GB of overage lands at $19 a month, which shows how fast $0.09 a gigabyte adds up when traffic spikes. Snapshots, by AWS’s published rate, cost $0.05 per GB-month, so a 20 GB snapshot adds roughly $1 a month.
Prerequisites and Tools (Steps 1 and 2)
You need an AWS account with billing enabled, a Linux or macOS shell (Windows users can use WSL2), and a domain name you control. The domain is optional until step 9. Everything before that works with the raw static IP.
| Tool | Version | Why |
|---|---|---|
| — | — | — |
| AWS CLI | 2.x (current v2 build) | Runs every Lightsail command |
| OpenSSH client | 8.x or newer | Ed25519 keys and modern defaults |
| jq | Not listed | Parsing CLI output |
| dig (dnsutils) | any | Checking DNS propagation |
| Ubuntu LTS blueprint | newest LTS Lightsail offers | Step 2 shows how to list it |
| Python | 3.x from the Ubuntu image | Runs the demo app, standard library only |
The same cloud tooling shows up across our cloud computing coverage, so skim it if the AWS CLI is new to you.
Step 1: Install the AWS CLI and Create a Scoped Profile
Download and install AWS CLI v2. The installer is a single zip file and needs no Python on your machine.
curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
aws --version
The last command should print something like this:
aws-cli/2.x.x Python/3.x.x Linux/6.8.0 exe/x86_64.ubuntu.24
Next, create an IAM user or an IAM Identity Center permission set that can only touch Lightsail. Do not run this tutorial with root credentials. A minimal policy looks like this:
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "lightsail:*", "Resource": "*" }
]
}
Then store the credentials under a named profile and export the variables the rest of the tutorial uses:
aws configure --profile lightsail-admin
export AWS_PROFILE=lightsail-admin
export AWS_REGION=us-east-1
export AZ=${AWS_REGION}a
export NAME=web-01
aws sts get-caller-identity
If get-caller-identity prints your account ID, the profile works. The most common failure here is a typo in the secret key, which shows up as InvalidClientTokenId.
Step 2: Pick a Region, Plan, and Blueprint
Pick the region closest to your users. Lightsail added instances, managed databases, containers, and load balancers in Asia Pacific (Hong Kong), South America (Sao Paulo), and Europe (Spain) on June 12, 2026, according to AWS release notes, so the map is wider than it was a year ago. List what your account can use:
aws lightsail get-regions --query 'regions[].name' --output text
aws lightsail get-blueprints \
--query "blueprints[?starts_with(blueprintId,'ubuntu')].[blueprintId,name,version]" \
--output table
aws lightsail get-bundles \
--query "bundles[?supportedPlatforms[0]=='LINUX_UNIX'].[bundleId,price,ramSizeInGb,cpuCount,diskSizeInGb,transferPerMonthInGb]" \
--output table
Sample output, trimmed. Your IDs and the newest Ubuntu release may differ:
-------------------------------------------
| GetBlueprints |
+--------------+------------+-------------+
| ubuntu_24_04| Ubuntu | 24.04 LTS |
+--------------+------------+-------------+
-------------------------------------------------------
| GetBundles |
+------------+--------+------+------+-------+----------+
| nano_3_0 | 5.0 | 0.5 | 2 | 20 | 1024 |
| micro_3_0 | 7.0 | 1.0 | 2 | 40 | 2048 |
| small_3_0 | 12.0 | 2.0 | 2 | 60 | 3072 |
+------------+--------+------+------+-------+----------+
Copy the newest Ubuntu LTS ID and the bundle ID that matches the $5 plan into variables. Skip any bundle whose ID contains ipv6, because a Lightsail static IP cannot attach to an IPv6-only instance, per AWS’s FAQ.
export BLUEPRINT=ubuntu_24_04
export BUNDLE=nano_3_0
AWS also launched compute-optimized bundles in April 2026, with up to 72 vCPUs, so CPU-bound jobs no longer have to jump to EC2. A web server on the $5 plan does not need them.
Create the Instance, Static IP, and Firewall (Steps 3 to 6)
These four steps produce a reachable server. Order matters here, because the launch script in step 4 embeds the SSH key from step 3, and the firewall in step 6 should close SSH to the public internet before you do any real work on the box.
Step 3: Generate an Ed25519 SSH Key
Create a dedicated key for this project. A passphrase is worth the five seconds it costs.
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/lightsail_ed25519 -C "lightsail-web"
chmod 600 ~/.ssh/lightsail_ed25519
cat ~/.ssh/lightsail_ed25519.pub
The public key is one line starting with ssh-ed25519. The tutorial injects it through the launch script rather than importing it into Lightsail, which keeps the key pair out of your account-level key list.
Step 4: Create the Instance With a Launch Script
A Lightsail launch script is a shell script that runs once as root on first boot. This one adds your key, patches the system, and installs Nginx, fail2ban, and unattended upgrades before you ever log in.
PUBKEY=$(cat ~/.ssh/lightsail_ed25519.pub)
cat > launch.sh <<EOF
#!/bin/bash
set -eux
echo "$PUBKEY" >> /home/ubuntu/.ssh/authorized_keys
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get upgrade -y
apt-get install -y nginx fail2ban unattended-upgrades python3
systemctl enable --now nginx fail2ban
EOF
aws lightsail create-instances \
--instance-names "$NAME" \
--availability-zone "$AZ" \
--blueprint-id "$BLUEPRINT" \
--bundle-id "$BUNDLE" \
--user-data file://launch.sh \
--tags key=project,value=ls-demo
The API answers with an operation record whose status reads Started. Poll the instance until it reports running:
aws lightsail get-instance-state --instance-name "$NAME"
{
"state": {
"code": 16,
"name": "running"
}
}
Code 16 means running, but the launch script keeps working for another minute or two. Do not SSH in until the script finishes, or you will race apt for its lock. A quick way to wait is to retry ssh until nginx answers on port 80 in step 6.
Step 5: Attach a Lightsail Static IP
An instance’s default public IP changes whenever you stop and start it. A Lightsail static IP does not. Allocate one and attach it:
aws lightsail allocate-static-ip --static-ip-name web-01-ip
aws lightsail attach-static-ip --static-ip-name web-01-ip --instance-name "$NAME"
export IP=$(aws lightsail get-static-ip --static-ip-name web-01-ip \
--query 'staticIp.ipAddress' --output text)
echo "$IP"
203.0.113.25
(That address is from the documentation range, yours will differ.) An attached static IP carries no charge, and one account can hold five. The trap is the unattached case: AWS bills $0.005 per hour for a static IP that sits unattached for more than an hour, so release addresses you stop using with release-static-ip.
Step 6: Lock the Lightsail Firewall
Every Lightsail instance has a built-in firewall that filters by source IP, port, and protocol. New instances start with SSH and HTTP open to the whole internet. Replace that with rules that fit this project. SSH accepts only your current address plus the browser-based SSH client in the Lightsail console, and web ports stay open.
MYIP=$(curl -s https://checkip.amazonaws.com)
cat > ports.json <<EOF
[
{"fromPort":22,"toPort":22,"protocol":"tcp",
"cidrs":["${MYIP}/32"],"cidrListAliases":["lightsail-connect"]},
{"fromPort":80,"toPort":80,"protocol":"tcp",
"cidrs":["0.0.0.0/0"],"ipv6Cidrs":["::/0"]},
{"fromPort":443,"toPort":443,"protocol":"tcp",
"cidrs":["0.0.0.0/0"],"ipv6Cidrs":["::/0"]}
]
EOF
aws lightsail put-instance-public-ports \
--instance-name "$NAME" --port-infos file://ports.json
aws lightsail get-instance-port-states --instance-name "$NAME" \
--query 'portStates[].[fromPort,protocol,cidrs[0],state]' --output table
Read that last output carefully. put-instance-public-ports replaces the whole rule set, so any port you leave out of the JSON closes. That is the behavior you want here, and it is also the number one cause of “I locked myself out” tickets.
Test from your own machine. Port 80 should answer with the default Nginx page, and the same request from a phone on mobile data should still work:
curl -sI "http://$IP" | head -n 1
HTTP/1.1 200 OK
A Lightsail firewall only filters traffic to this one instance. If you need rules across many hosts or a shared appliance, the open-source approach in our pfSense firewall setup guide is a different tool for a different job.
Harden the Server and Deploy the App (Steps 7 and 8)
The instance is reachable and filtered, but the operating system still ships with defaults. These two steps tighten SSH, add swap so the 512 MB plan survives a package install, and put a real application behind Nginx.
Step 7: Harden SSH and Add Swap
Log in with the key from step 3. The default user on Ubuntu blueprints is ubuntu.
ssh -i ~/.ssh/lightsail_ed25519 ubuntu@"$IP"
Once you are in, write an SSH drop-in file. Ubuntu reads everything in /etc/ssh/sshd_config.d/ before the main file, so this survives package upgrades.
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 20
AllowUsers ubuntu
EOF
sudo sshd -t && sudo systemctl reload ssh
Always run sshd -t first. A syntax error plus a reload equals a dead SSH daemon, and your only way back is the browser-based console or a snapshot restore. Keep your current session open and test a second login from another terminal before you close it.
Next, turn on unattended security upgrades and give the small plan some swap. A 512 MB instance will kill apt or pip with an out-of-memory error without it.
sudo tee /etc/apt/apt.conf.d/20auto-upgrades >/dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h
total used free shared buff/cache available
Mem: 457Mi 168Mi 62Mi 1.0Mi 226Mi 289Mi
Swap: 1.0Gi 0B 1.0Gi
The fail2ban package from the launch script already protects SSH with its default jail. For a deeper look at host defenses, our ClamAV on-access scanning guide shows how to add malware scanning to a Linux server that accepts uploads.
Step 8: Deploy the App Behind Nginx
The demo app is a 25-line Python service that returns JSON with the hostname and uptime. It uses only the standard library, so there is nothing to install and nothing to patch. Save it on your workstation as deploy-app.sh, which you will reuse for the second instance in step 13.
#!/bin/bash
# deploy-app.sh: run on the server as root via: ssh host 'sudo bash -s' < deploy-app.sh
set -eux
mkdir -p /opt/ls-demo
cat > /opt/ls-demo/app.py <<'PYEOF'
import json, socket, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
START = time.time()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/healthz":
body, ctype = b"ok", "text/plain"
else:
body = json.dumps({
"host": socket.gethostname(),
"uptime_s": int(time.time() - START),
"path": self.path,
}).encode()
ctype = "application/json"
self.send_response(200)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
ThreadingHTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
PYEOF
cat > /etc/systemd/system/ls-demo.service <<'UNITEOF'
[Unit]
Description=Lightsail demo app
After=network.target
[Service]
DynamicUser=yes
ExecStart=/usr/bin/python3 /opt/ls-demo/app.py
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
UNITEOF
cat > /etc/nginx/sites-available/ls-demo <<'NGXEOF'
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
NGXEOF
ln -sf /etc/nginx/sites-available/ls-demo /etc/nginx/sites-enabled/ls-demo
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl daemon-reload
systemctl enable --now ls-demo
systemctl reload nginx
From your workstation, push it to the server and test:
ssh -i ~/.ssh/lightsail_ed25519 ubuntu@"$IP" 'sudo bash -s' < deploy-app.sh
curl -s "http://$IP/"
curl -s "http://$IP/healthz"
{"host": "ip-172-26-9-14", "uptime_s": 41, "path": "/"}
ok
If you see 502 Bad Gateway instead, the Python service is not running. Run journalctl -u ls-demo -n 30 on the server and read the last lines. The /healthz route matters later, because the load balancer in step 13 uses it to decide whether an instance is alive.
Add DNS and HTTPS (Steps 9 and 10)
Browsers distrust plain HTTP and certificate authorities will not issue a certificate for a bare IP. So the project needs a hostname. These two steps create the DNS records in Lightsail and then request a free Let’s Encrypt certificate.
Step 9: Create the Lightsail DNS Zone
Lightsail can host your DNS zone for free. AWS’s FAQ reportedly allows up to six zones per account. One catch trips people up: the domain commands only work in us-east-1, even when your instance lives elsewhere. The examples below use example.com, so swap in your own domain.
aws lightsail create-domain --region us-east-1 --domain-name example.com
aws lightsail create-domain-entry --region us-east-1 \
--domain-name example.com \
--domain-entry "name=app.example.com,type=A,target=$IP"
aws lightsail get-domain --region us-east-1 --domain-name example.com \
--query "domain.domainEntries[?type=='NS'].target" --output text
The last command prints four name servers. Copy them into your registrar’s name-server settings, replacing the registrar’s defaults. If your DNS lives elsewhere, skip this step and create the A record there instead. Then check propagation:
dig +short app.example.com
203.0.113.25
Propagation can take minutes or hours. Do not move to step 10 until dig returns your static IP, because Let’s Encrypt validates over the public internet and a stale record fails the challenge.
Step 10: Issue the HTTPS Certificate
Certbot’s snap package is the install method Let’s Encrypt recommends for Ubuntu. Run it on the server:
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot
sudo sed -i 's/server_name _;/server_name app.example.com;/' /etc/nginx/sites-available/ls-demo
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com \
-m [email protected] --agree-tos --no-eff-email --redirect -n
sudo certbot renew --dry-run
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/app.example.com/fullchain.pem
Congratulations! You have successfully enabled HTTPS on https://app.example.com
...
Congratulations, all simulated renewals succeeded
Snap installs a systemd timer that renews the certificate twice a day, so the dry run is your proof that renewal works. Confirm the redirect and the certificate from outside:
curl -sI http://app.example.com | head -n 3
curl -sI https://app.example.com | head -n 1
Remember the --redirect flag. It is right for a single server but needs undoing behind the load balancer later, and step 13 shows how to undo it.
Protect It With Snapshots and Alarms (Steps 11 and 12)
A server you cannot restore is a liability. These steps add Lightsail snapshots, prove that a restore works, and wire up alerts so a runaway process emails you before the site stops answering.
Step 11: Enable Lightsail Snapshots and Test a Restore
Enabling automatic snapshots costs nothing by itself. You pay only for the snapshot storage. Set the daily time in UTC, then take a manual snapshot before any risky change:
aws lightsail enable-add-on --resource-name "$NAME" \
--add-on-request "addOnType=AutoSnapshot,autoSnapshotAddOnRequest={snapshotTimeOfDay=06:00}"
aws lightsail create-instance-snapshot \
--instance-name "$NAME" \
--instance-snapshot-name "web-01-baseline-$(date +%Y%m%d)"
aws lightsail get-instance-snapshots \
--query 'instanceSnapshots[].[name,state,sizeInGb,createdAt]' --output table
AWS documents seven retained automatic snapshots at $0.05 per GB-month, according to the current FAQ. Snapshots of a 20 GB disk therefore stay in the low single dollars per month. Wait until the state reads available, then prove the restore path with a throwaway instance:
aws lightsail create-instances-from-snapshot \
--instance-names web-01-restore \
--availability-zone "$AZ" \
--instance-snapshot-name "web-01-baseline-$(date +%Y%m%d)" \
--bundle-id "$BUNDLE"
Restoring creates a new instance with a new public IP. Your static IP stays on web-01. For a real recovery you would detach the static IP from the broken instance and attach it to the restored one. Delete the test instance once you have confirmed that Nginx answers on its temporary address, since it bills hourly:
aws lightsail delete-instance --instance-name web-01-restore
This restore flow is also how you resize. To move from the $5 plan to a bigger one, snapshot the instance and create a new one from that snapshot with a larger --bundle-id. Lightsail only lets you go up, never down.
Step 12: Add CPU Alarms With Email Alerts
Lightsail exposes instance metrics for free and lets you attach alarms to them. First register an email contact method and confirm the link AWS mails you. Then create two alarms: sustained high CPU and burst capacity running low. The burst alarm matters on small plans, because a Lightsail instance that exhausts its CPU burst gets throttled hard.
aws lightsail create-contact-method --protocol Email \
--contact-endpoint [email protected]
aws lightsail put-alarm --alarm-name web-01-cpu-high \
--monitored-resource-name "$NAME" --metric-name CPUUtilization \
--comparison-operator GreaterThanOrEqualToThreshold \
--threshold 80 --evaluation-periods 3 --datapoints-to-alarm 3 \
--period 300 --treat-missing-data breaching \
--contact-protocols Email --notification-triggers ALARM OK \
--notification-enabled
aws lightsail put-alarm --alarm-name web-01-burst-low \
--monitored-resource-name "$NAME" --metric-name BurstCapacityPercentage \
--comparison-operator LessThanOrEqualToThreshold \
--threshold 20 --evaluation-periods 2 --datapoints-to-alarm 2 \
--period 300 --treat-missing-data notBreaching \
--contact-protocols Email --notification-triggers ALARM OK \
--notification-enabled
Check that metrics flow and read the last hour of CPU data:
aws lightsail get-instance-metric-data --instance-name "$NAME" \
--metric-name CPUUtilization --period 300 \
--start-time $(date -u -d '-1 hour' +%s) --end-time $(date -u +%s) \
--unit Percent --statistics Average \
--query 'metricData[].[timestamp,average]' --output table
An idle Nginx box sits near 1 to 3 percent. To prove the alarm fires, run yes > /dev/null & on the server for ten minutes and watch your inbox. Then kill the job.
Scale Out With a Lightsail Load Balancer (Step 13)
One instance is a single point of failure. A Lightsail load balancer spreads traffic across instances in the same region, runs health checks, and terminates HTTPS with a free managed certificate that AWS renews for you. It costs $18 a month on top of the instances. If you want to see how it stacks up against the heavier options, our ALB vs NLB vs Cloudflare load balancing comparison puts the pricing side by side.
Step 13: Build the Second Instance and the Load Balancer
First create web-02 in the same availability zone with the same launch script, then deploy the app with the script from step 8. It needs no static IP, because the load balancer reaches it by instance name.
aws lightsail create-instances --instance-names web-02 \
--availability-zone "$AZ" --blueprint-id "$BLUEPRINT" \
--bundle-id "$BUNDLE" --user-data file://launch.sh \
--tags key=project,value=ls-demo
aws lightsail put-instance-public-ports --instance-name web-02 \
--port-infos file://ports.json
Wait about two minutes, find the instance’s temporary IP, and deploy:
IP2=$(aws lightsail get-instance --instance-name web-02 \
--query 'instance.publicIpAddress' --output text)
ssh -i ~/.ssh/lightsail_ed25519 ubuntu@"$IP2" 'sudo bash -s' < deploy-app.sh
Now create the load balancer. The instance port is 80 and the health check hits the /healthz route from step 8.
aws lightsail create-load-balancer --load-balancer-name web-lb \
--instance-port 80 --health-check-path /healthz
aws lightsail attach-instances-to-load-balancer \
--load-balancer-name web-lb --instance-names web-01 web-02
aws lightsail create-load-balancer-tls-certificate \
--load-balancer-name web-lb --certificate-name web-lb-cert \
--certificate-domain-name app.example.com
The certificate request returns validation records. AWS’s SDK documentation describes the LoadBalancerTlsCertificate object that carries them. Pull the CNAME name and value, add them to your Lightsail DNS zone, and wait for validation:
aws lightsail get-load-balancer-tls-certificates --load-balancer-name web-lb \
--query 'tlsCertificates[0].[status,domainValidationRecords[0].name,domainValidationRecords[0].value]' \
--output text
PENDING_VALIDATION _a1b2c3.app.example.com. _d4e5f6.acm-validations.aws.
Create that CNAME in the zone, wait until the status flips to ISSUED, and then attach the certificate and turn on the HTTP to HTTPS redirect at the load balancer:
aws lightsail attach-load-balancer-tls-certificate \
--load-balancer-name web-lb --certificate-name web-lb-cert
aws lightsail update-load-balancer-attribute \
--load-balancer-name web-lb --attribute-name HttpsRedirection \
--attribute-value true
Before you flip DNS, remove the redirect from the instances, since the load balancer provides its own integrated certificate management. Reset the site config on both instances:
for H in "$IP" "$IP2"; do
ssh -i ~/.ssh/lightsail_ed25519 ubuntu@"$H" 'sudo bash -s' < deploy-app.sh
done
ssh -i ~/.ssh/lightsail_ed25519 ubuntu@"$IP" \
'sudo certbot delete --cert-name app.example.com -n'
Re-running deploy-app.sh rewrites the Nginx site to the plain-HTTP version from step 8, which drops the Certbot edits. The certbot delete line removes the instance certificate, so its renewal timer stops trying to validate a hostname that now points at the load balancer. Finally, point the hostname at the load balancer using an alias record and watch the targets turn healthy:
LBDNS=$(aws lightsail get-load-balancer --load-balancer-name web-lb \
--query 'loadBalancer.dnsName' --output text)
aws lightsail delete-domain-entry --region us-east-1 --domain-name example.com \
--domain-entry "name=app.example.com,type=A,target=$IP"
aws lightsail create-domain-entry --region us-east-1 --domain-name example.com \
--domain-entry "name=app.example.com,type=A,target=$LBDNS,isAlias=true"
aws lightsail get-load-balancer-target-health --load-balancer-name web-lb \
--query 'targetHealthDescriptions[].[target.id,targetHealth.state]' --output table
-------------------------
| web-01 | healthy |
| web-02 | healthy |
-------------------------
Prove that traffic alternates between the two servers:
for i in 1 2 3 4 5 6; do curl -s https://app.example.com/ | jq -r .host; done
ip-172-26-9-14
ip-172-26-3-201
ip-172-26-9-14
ip-172-26-3-201
ip-172-26-3-201
ip-172-26-9-14
Stop ls-demo on one server and repeat the loop. Within a minute or so every response should come from the survivor. That is the payoff for the extra $18 a month.
The Complete Working Project in One Script
Steps 3 to 6, 11 and 12 are pure CLI work, so they collapse into one repeatable script. Save the following as provision.sh next to launch.sh, ports.json, and deploy-app.sh. It creates the instance, waits for it, attaches the static IP, closes the firewall, enables daily snapshots, and registers the CPU alarm. Run it with a different NAME and you get a second environment in about four minutes.
#!/bin/bash
set -euo pipefail
export AWS_PROFILE=${AWS_PROFILE:-lightsail-admin}
export AWS_REGION=${AWS_REGION:-us-east-1}
AZ=${AWS_REGION}a
NAME=${1:-web-01}
BLUEPRINT=${BLUEPRINT:-ubuntu_24_04}
BUNDLE=${BUNDLE:-nano_3_0}
EMAIL=${ALERT_EMAIL:?set ALERT_EMAIL}
aws lightsail create-instances --instance-names "$NAME" \
--availability-zone "$AZ" --blueprint-id "$BLUEPRINT" \
--bundle-id "$BUNDLE" --user-data file://launch.sh >/dev/null
until [ "$(aws lightsail get-instance-state --instance-name "$NAME" \
--query state.name --output text)" = running ]; do sleep 5; done
aws lightsail create-contact-method --protocol Email \
--contact-endpoint "$EMAIL" >/dev/null || true
aws lightsail allocate-static-ip --static-ip-name "$NAME-ip" >/dev/null
aws lightsail attach-static-ip --static-ip-name "$NAME-ip" \
--instance-name "$NAME" >/dev/null
aws lightsail put-instance-public-ports --instance-name "$NAME" \
--port-infos file://ports.json >/dev/null
aws lightsail enable-add-on --resource-name "$NAME" \
--add-on-request "addOnType=AutoSnapshot,autoSnapshotAddOnRequest={snapshotTimeOfDay=06:00}" >/dev/null
aws lightsail put-alarm --alarm-name "$NAME-cpu-high" \
--monitored-resource-name "$NAME" --metric-name CPUUtilization \
--comparison-operator GreaterThanOrEqualToThreshold --threshold 80 \
--evaluation-periods 3 --datapoints-to-alarm 3 --period 300 \
--contact-protocols Email --notification-enabled >/dev/null
aws lightsail get-static-ip --static-ip-name "$NAME-ip" \
--query 'staticIp.ipAddress' --output text
The matching teardown script matters just as much, since the $18 load balancer and any idle static IP keep billing until you delete them. Read it before you run it, because it removes the DNS zone too.
#!/bin/bash
set -euo pipefail
export AWS_PROFILE=${AWS_PROFILE:-lightsail-admin}
export AWS_REGION=${AWS_REGION:-us-east-1}
aws lightsail delete-load-balancer --load-balancer-name web-lb || true
for N in web-01 web-02; do
aws lightsail detach-static-ip --static-ip-name "$N-ip" || true
aws lightsail release-static-ip --static-ip-name "$N-ip" || true
aws lightsail delete-alarm --alarm-name "$N-cpu-high" || true
aws lightsail delete-instance --instance-name "$N" --force-delete-add-ons || true
done
aws lightsail get-instance-snapshots \
--query 'instanceSnapshots[].name' --output text | tr '\t' '\n' |
while read -r S; do aws lightsail delete-instance-snapshot --instance-snapshot-name "$S"; done
aws lightsail delete-domain --region us-east-1 --domain-name example.com || true
Your project folder should now hold five files: launch.sh, ports.json, deploy-app.sh, provision.sh, and teardown.sh. Commit them to a private Git repository. That folder is the whole infrastructure definition, and it restores your stack faster than any wiki page.
When AWS Lightsail Is the Wrong Tool
Lightsail earns its place for small, steady workloads. It stops fitting when you need autoscaling, private networking between many services, or fine-grained IAM on each resource. AWS’s own VPC peering for Lightsail lets you reach resources in a default VPC, but the service has no equivalent of EC2 auto scaling groups.
Three signals mean it is time to move. First, you add a third or fourth instance and start hand-editing load balancer targets. Second, a traffic spike forces you to resize by snapshot-and-restore during an outage. Third, compliance asks for security groups, flow logs, or private subnets you cannot configure here. When the second signal hits, containers are the natural next step, and our ECS Fargate deployment guide walks through the same kind of workload on a platform that scales on its own.
Event-driven or spiky traffic points the other way, toward no servers at all. A service that handles a few thousand requests a day costs less on functions than on an always-on $5 box. Our AWS SAM deployment tutorial covers that route.
None of this means Lightsail is on its way out. As of October 25, 2026, no official AWS result confirms that Lightsail was renamed, retired, or discontinued. AWS’s product page still invites new users to sign up, and the service reportedly picked up new regions in June and Marketplace AMI launches in August 2026. Treat it as a stable small-workload platform.
Cost Control and Teardown
Three habits keep an AWS Lightsail bill predictable. First, tag everything with the same project tag, as the commands above do, so one query finds every resource. Second, set a billing budget in AWS Billing as a backstop, since Lightsail alarms watch metrics rather than money. Third, watch the transfer allowance. The $5 plan includes 1 TB, and every gigabyte beyond a plan’s allowance starts at $0.09.
A second-order cost surprise is snapshots that outlive their instance. Deleting an instance does not delete manual snapshots, and AWS bills them at $0.05 per GB-month until you remove them. List them once a month:
aws lightsail get-instance-snapshots \
--query 'instanceSnapshots[].[name,sizeInGb,createdAt]' --output table
aws lightsail get-static-ips --query 'staticIps[?isAttached==`false`].name' --output text
aws lightsail get-load-balancers --query 'loadBalancers[].name' --output text
The second command catches unattached static IPs, which bill $0.005 an hour. The third lists load balancers, the single biggest fixed line in this project. If you only need failover for a hobby site, a hot standby plus a manual DNS change costs $18 a month less. For long-running production, price the same capacity on EC2 using our Reserved vs Savings Plans vs Spot breakdown before deciding Lightsail is the cheap option. Its flat pricing wins at small scale, then loses once you can commit to a one- or three-year term.
Common Pitfalls
Most failed Lightsail setups trace back to six mistakes. Each one is cheap to avoid when you know it exists.
Overwriting the firewall by accident. put-instance-public-ports replaces every rule. Run it with a partial JSON file and you silently close ports you opened an hour ago. Keep ports.json as the single source of truth and always apply the whole file.
Locking yourself out of SSH. Pinning port 22 to your home address works until your ISP rotates it. Keep the lightsail-connect alias in the rule so the browser-based client still works, and keep a snapshot from before you changed anything.
Forgetting the static IP. The default public IP changes on stop and start, which breaks your A record. Attach the static IP before you create DNS entries, not after.
Running certbot with a redirect behind a load balancer. Leave the HTTPS redirect off the instances, because the load balancer handles certificates itself.
Choosing an IPv6-only bundle. The cheapest-looking bundle can be IPv6 only, and AWS says a static IP cannot attach to those instances. Check the bundle ID for ipv6 before you create anything.
Skipping swap on the smallest plan. A 0.5 GB server runs out of memory during apt upgrade or any build step. A 1 GB swap file costs nothing and prevents a whole category of mystery crashes.
Troubleshooting AWS Lightsail Setup Problems
Work through the table from the symptom column. Each fix maps to a command used earlier in the tutorial.
| Symptom | Likely cause | Fix |
|---|---|---|
| — | — | — |
ssh: connect to host ... port 22: Connection timed out | Firewall rule excludes your current IP | Re-run put-instance-public-ports with a fresh checkip result, or use the browser SSH client |
Permission denied (publickey) | Launch script did not run, or wrong key or user | Use user ubuntu, point -i at the private key, and check /var/log/cloud-init-output.log from the browser console |
502 Bad Gateway from Nginx | App service is down or not on the port Nginx expects | systemctl status ls-demo and journalctl -u ls-demo -n 30 |
Certbot says Challenge failed | A record not propagated, or port 80 closed | Confirm dig +short returns the static IP and the firewall allows 80 |
Load balancer targets show unhealthy | Health check path wrong, or instance redirects to HTTPS | Check /healthz returns 200 over plain HTTP, and remove the Certbot redirect |
Load balancer certificate stuck on PENDING_VALIDATION | CNAME validation record missing in DNS | Add the exact name and value from get-load-balancer-tls-certificates |
apt or pip dies with Killed | Out of memory on the 512 MB plan | Add the swap file from step 7, or move up one plan size |
Domain not found on a domain command | Command ran outside us-east-1 | Add --region us-east-1 to every *-domain* command |
| Static IP will not attach | Instance is IPv6-only, or the IP is already attached | Recreate on a dual-stack bundle, or detach first |
| Restored instance has a different address | Snapshots create new instances | Detach the static IP from the old instance and attach it to the restored one |
| Unexpected transfer charges | Traffic past the plan’s monthly allowance | Check NetworkOut metric, add a CDN, or move up a plan |
When none of those match, ask for the launch log first. It records every command the launch script ran, and it shows a failed package install in plain text.
sudo tail -n 60 /var/log/cloud-init-output.log
sudo journalctl -u nginx -u ls-demo --since "30 min ago" --no-pager
sudo nginx -T | head -n 40
Advanced Tips for AWS Lightsail
Once the base stack runs, a handful of upgrades make it behave like production infrastructure.
Describe it as code. The CLI scripts above work, but Terraform gives you drift detection and a plan step. The AWS provider has Lightsail resources for instances, static IPs, and load balancers. Our Terraform on AWS tutorial covers the state backend and workflow that these resources need.
Put a CDN in front. A Lightsail CDN distribution lists at $2.50 a month in AWS’s own cost example, and it takes static asset traffic off your instance transfer allowance. That example prices a WordPress instance at $5, the CDN at $2.50, and an object storage bucket at $1, for $8.50 total.
Move data off the root disk. Block storage runs $0.10 per GB per month and starts at 8 GB. Mount it for uploads or databases so a snapshot restore of the instance does not roll your data back with it.
Use Marketplace images. AWS Marketplace reportedly supports launching eligible AMIs directly on Lightsail, including Ubuntu, cPanel and WHM, and Plesk. That skips the install-from-scratch step for panels and appliances.
Mix in containers. A Lightsail container service lists at $7 a month with a 500 GB monthly transfer quota. If your app already ships as a Docker image, that plan removes the patching chores you took on in step 7.
Track service changes. Lightsail bundle specs change quietly, as the $7 plan’s spec bump shows. A feed such as Cloud Releases for AWS collects AWS updates in one place, which beats rereading the pricing page every month.
Script a restore drill. Once a quarter, create an instance from the latest automatic snapshot, hit its temporary IP, and delete it. A backup you never restored is a guess.
Frequently Asked Questions
Is AWS Lightsail free?
No. The cheapest plan on AWS’s pricing page is $5 a month for a public-IPv4 Linux instance, and the FAQ still lists a $0.0047-an-hour plan capped at $3.50. Check the Lightsail console for any trial offer on your account before you pay anything.
How much does this AWS Lightsail setup cost per month?
Two $5 instances plus the $18 load balancer come to $28 a month. Add roughly $1 per 20 GB snapshot at the published $0.05 per GB-month rate. A single-instance version of the project costs about $5 plus snapshot storage.
How many static IPs can I have, and do they cost money?
AWS’s FAQ allows five per account. A static IP attached to an instance costs nothing, and an unattached one bills $0.005 an hour after the first idle hour.
Can I attach a Lightsail static IP to an IPv6-only instance?
No. AWS says static IPv4 addresses cannot attach to IPv6-only instances, so pick a dual-stack bundle when you need a fixed public IPv4 address.
Do Lightsail snapshots cost extra?
Turning on automatic snapshots is free, and you pay for the storage they use. AWS lists snapshots at $0.05 per GB-month and keeps seven automatic ones, according to its FAQ. Delete manual snapshots you no longer need.
Should I use Lightsail or EC2?
Use Lightsail for a few servers with predictable traffic and a fixed monthly bill. Use EC2 when you need autoscaling, custom VPC design, per-resource IAM, or reserved-capacity discounts.
Does the Lightsail load balancer support HTTPS?
Yes. Lightsail load balancers include free managed TLS certificates, and AWS handles renewals. You prove domain ownership with a DNS CNAME, then attach the certificate and switch on the HTTP to HTTPS redirect.
Is AWS Lightsail being discontinued?
No official AWS result as of October 25, 2026 says so. The service is listed as available, and no retirement or discontinuation is indicated.





