October 20, 2026 · 26 min read
The VM Is One Line on the Bill: Learn Real Infrastructure on a $12 Droplet
A field guide for learners: why a running app costs more than its VM, how to list every resource before you price anything, how to keep a learning project from becoming a surprise bill, and a hands-on DigitalOcean lab covering SSH keys, hardening, firewalls, a private network and a database the internet can't reach. With a fair, sourced comparison against AWS and Azure, and a safe way to let an AI assistant help.
A cloud calculator shows you a VM for about $15 a month. Give that same VM a disk, a public address and some real traffic, and the estimate can pass $58. Nothing was hidden: every line was on the price page. The VM is simply one line on the bill. This guide teaches the rest of the bill, how to keep it small while you learn, and then has you build and lock down two real servers, with the actual output at every step.
You don't need cloud experience. If you can open a terminal, you can follow this. Every section ends with a one-line Remember, and the whole guide fits in 10 lines at the end.
- Part 1 · The bill: what a VM price buys, what a running app needs, and how to compare providers fairly, sections 1–5.
- Part 2 · Protect your wallet first: budgets, alerts, caps, and a checklist before you click Deploy, section 6.
- Part 3 · The lab: keys, a safe user, SSH hardening, two firewalls, a private network, a private database and a jump host, sections 7–12.
- Part 4 · Working with AI, cleaning up, what's next: sections 13–15.
Part 1
The bill
Price the whole service, not the machine.
1. You are not renting a computer
You are paying to make a service reachable, keep its data, move its traffic, protect it, watch it, and keep it running. The VM is the part that runs your code. Everything else is a separate thing with its own price:
| Part | What it does | How it's usually billed |
|---|---|---|
| Compute (the VM) | runs your code | per hour or second |
| Public entry point (an IPv4 address, or a load balancer) | lets the internet reach you | per hour, sometimes even when idle |
| Disk | keeps the OS and your data | per GB per month, attached or not |
| Outbound traffic | every response that leaves the cloud | per GB after a free allowance |
| DNS, domain, TLS | a name people can type, and HTTPS | domain per year; certificates can be free |
| Backups and snapshots | getting your data back | per GB, or a % of the server price |
| Firewalls, private networks | who can reach what | often free; some network services aren't |
| Monitoring, logs, alerts | knowing it broke before users tell you | free tiers, then per metric or GB |
| Databases, caches, queues | state your app depends on | per node or per use |
| Redundancy (a second server, zone or region) | surviving a failure | roughly doubles the parts it copies |
| Taxes, currency, region | the bill you actually pay | varies by country and region |
A provider can show every one of these on its price page and the first bill can still surprise you, because nobody added them up. They aren't hidden costs. They're costs outside the headline VM price.
2. Different workloads need different parts
Not every project needs every line. A static site, an API and a SQL database need very different inventories, and their exposure to the internet should be different too:
Clients call it over the internet, so it needs one public door. The app itself can listen privately behind a reverse proxy.
- Compute (a VM)neededruns the app as a service
- Public entry point (IPv4)neededpublicnginx on 443, nothing else open
- App processneededprivatebind to 127.0.0.1 behind the proxy
- DiskneededOS, app, logs
- Outbound trafficneededevery response leaves the cloud
- Monitoring + alertsdependsuptime check + a disk-full alert at minimum
- Load balancerdependsonly once you run more than one server
- Backupsdependsonly if the server holds data you can't rebuild
Two ideas fall out of this.
Public access is not the same as making every service public. A web app needs one public door: a public IP or a load balancer, with only ports 80 and 443 open. Everything behind that door (the app process, the database, the cache) can listen on a private address and be reached only by the machines that need it.
A SQL server doesn't need a public IP. Put it on the private network, allow its port only from the app's private address, and do your own admin work through a jump host (section 12). If a tutorial tells you to open 5432 or 3306 to 0.0.0.0/0, it is optimising for the demo, not for you.
3. The same server on three clouds
Here is one small server priced three ways, using official list prices for US East, checked on 20 October 2026: 2 GiB of memory, Ubuntu, one public IPv4, on for a whole month (730 hours). Move the slider to add traffic, and switch the disk between each provider's default size and a disk the size of the droplet's.
Basic droplet (plan price) $12.00 (disk, IPv4 and 2,000 GiB out are in the plan)
t3.small on-demand $15.18 + 50 GB gp3 $4.00 + public IPv4 $3.65
B1ms pay-as-you-go $15.11 + 64 GiB Standard SSD (E6) $4.80 + Standard static IPv4 $3.65
Not included: taxes · backups and snapshots · DNS and domain · disk operations on Azure Standard SSD · AWS T3 “unlimited” CPU above baseline ($0.05 per vCPU-hour) · anything else you add.
Read it in layers, because each layer answers a different question:
| DigitalOcean (Basic, 1 vCPU) | AWS (t3.small, 2 vCPU) | Azure (B1ms, 1 vCPU) | |
|---|---|---|---|
| 1. Compute only | $12.00 plan price | $15.18 | $15.11 |
| 2. Minimum to run it (small default disk + public IPv4) | $12.00 | $19.47 | $21.16 |
| 3. Disk matched to ~50 GB | $12.00 | $22.83 | $23.56 |
| 4. Plus 500 GB out a month | $12.00 | $58.83 | $58.36 |
What changes between rows:
- Row 1 → 2: AWS and Azure price the disk and the public IPv4 separately. A Basic droplet includes a 50 GiB disk, a public IPv4 and 2,000 GiB of outbound transfer in its plan price. (DigitalOcean's newer v5 configurations do bill IPv4 separately, so check which kind you're creating.)
- Row 3 → 4: outbound data. AWS and Azure each include 100 GB a month, then charge $0.09 and $0.087 per GB. The droplet has 2,000 GiB in the plan, then $0.01 per GiB.
And what these totals leave out: taxes, free credits for new accounts, savings plans, backups, DNS, and usage-based charges such as Azure disk operations or AWS T3 instances bursting above their baseline (they launch in "unlimited" mode by default, at $0.05 per vCPU-hour of surplus). The machines aren't identical either: t3.small has 2 vCPUs, the other two have 1. Treat the table as a worked example of how bills are built, not a quote.
The VM line is close everywhere. On Azure, a B2ats v2 (2 vCPU, 1 GiB) lists at about $6.86 a month, close to a $6 droplet. The bills diverge on everything around the VM.
4. Why a calculator can disagree with your bill
Cloud calculators are accurate about what you put in them. The gap comes from what you didn't:
- A part you forgot to model: the public IP, the disk, a snapshot, a second environment.
- Usage you guessed: traffic, disk operations and CPU bursts depend on what actually happened.
- The wrong size or region: prices differ by region, and a default you didn't notice can be bigger than you think.
- Time: a resource left running for a month costs a month, even if you used it for an hour.
- Your account: credits, discounts, taxes and currency conversion.
The fix is the habit from section 2: inventory first, then price each line, then compare.
5. Where to learn, and when to move
Why a droplet is a good place to start. The bundle makes the first bill easy to predict, the core pieces are few (a server, a firewall, a private network), and you have to do the parts a managed service hides: SSH, users, firewalls and network boundaries. Those skills carry straight into an Azure VNet or an AWS VPC.
When AWS or Azure makes more sense. If your job or your target role uses them, learn them; their free credits and free tiers help. And production at scale often needs what they're built for:
- Availability zones. DigitalOcean lists 16 datacenters in 13 regions and doesn't present separate availability zones within a region the way AWS and Azure do, so surviving a datacenter failure means designing across datacenters yourself.
- More building blocks. Fine-grained identity, many managed services, private connectivity between them, global networks and compliance programmes.
- How scaling works. On DigitalOcean, changing a droplet's CPU or memory needs a power-off, and a disk can only grow. Load balancers, autoscale pools and managed databases exist, but you assemble more yourself.
None of that means a droplet can't run a real application, or that a hyperscaler solves scaling for you. They offer different building blocks and different amounts of work.
Part 2
Protect your wallet first
Set the guard rails before you create anything.
6. Before you deploy anything
Do not deploy cloud resources by guesswork. Check every resource, its region, its price, who can reach it, and how you'll delete it.
Three tools sound alike and do different things:
- A budget is a number you expect to spend.
- An alert emails you when spending crosses a threshold. It does not stop anything.
- A hard cap stops or disables resources when money runs out. Most paid accounts don't have one.
| Provider | What you can set | Does it stop spending? |
|---|---|---|
| DigitalOcean | Spend Alerts: a monthly budget with percentage thresholds | No. It notifies, within about an hour of crossing a threshold. |
| AWS | Budgets with alerts; optional budget actions | Alerts don't. Budget actions can apply a deny policy to block new resources, or stop chosen EC2/RDS instances. That isn't a cap on every service. |
| Azure | Budgets in Cost Management | No. Microsoft's docs say resources aren't affected and consumption isn't stopped; cost data can lag 8–24 hours. The spending limit on free and credit-based subscriptions does stop services; pay-as-you-go can't have one. |
So set alerts low (50% and 80% of what you expect), and treat them as a smoke alarm, not a sprinkler. The real protection is choosing small resources and deleting them on time.
Before you click Deploy, check:
- The size is the smallest that fits the lesson (a $6 or $12 droplet is plenty here).
- The region is the one you meant, and every related resource is in it.
- The disk size is what you expect, and backups are off unless you want them.
- You know which resources get a public address, and nothing else does.
- You know the traffic allowance, and nothing in the lab streams large files.
- No extra services were added (a load balancer, a managed database, a volume).
- A spend alert exists, and a calendar reminder to clean up.
- You know the delete path for every resource on this list.
Part 3
The lab
From an empty account to two servers, one of them unreachable from the internet.
The lab: web-1, the public entry point and jump host, and db-1, a Redis database that only web-1 can reach.
How this was tested. I rehearsed every terminal command on Ubuntu 24.04.5 machines (Docker containers) wired like two droplets and a laptop: a public network and a private 10.124.0.0/20 network. All terminal output below is real output from that rehearsal. Containers aren't real VMs, though: there's no DigitalOcean metadata service, no Cloud Firewall and no real internet in my lab, and I trimmed container-only interfaces from the ip output. I used 203.0.113.x, a range reserved for examples, as the public side. The DigitalOcean control-panel steps and the doctl command were not run for this guide.
7. Make a key, then the droplet
An SSH key is a pair of files: a private key that stays on your laptop and a public key you can hand to any server. Run this on your laptop (macOS, Linux, or Windows PowerShell):
ssh-keygen -t ed25519 -C "aryan@laptop" # the comment is just a label: use yours
Generating public/private ed25519 key pair.
Your identification has been saved in /home/aryan/.ssh/id_ed25519
Your public key has been saved in /home/aryan/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:JYh8D0jFfwZ0XTSLpR7Lwd0Nyfwi71Yy8WBaYJXpUFA aryan@laptop
Press Enter to accept the path. A passphrase is optional but worth it: it protects the key if your laptop is stolen.
Your laptop signs it with the private key. The server checks the signature with the public key. No password crossed the network.
Create the droplets (not tested here; follow DigitalOcean's "How to Create a Droplet" doc if the screens differ):
- Image: Ubuntu 24.04 (LTS).
- Region: the one closest to you (for India, BLR1). Put both droplets in the same region, so they share its VPC.
- Size: Basic, Regular SSD, $6 or $12. Leave backups off for the lab.
- Authentication: SSH Key. Paste the contents of
~/.ssh/id_ed25519.pub, never the file without.pub. - Hostnames:
web-1, thendb-1.
Confirm it worked: both droplets show a private IP under the VPC tab, and the bill preview matches your plan. The CLI version, also untested here:
doctl compute droplet create web-1 --region blr1 --size s-1vcpu-2gb \
--image ubuntu-24-04-x64 --ssh-keys <your-key-fingerprint> --wait
8. First login, then stop using root
ssh root@<web-1-public-ip>
Warning: Permanently added '203.0.113.21' (ED25519) to the list of known hosts.
web-1
root
Ubuntu 24.04.5 LTS
The first time, ssh asks you to confirm the server's host key: the server proving its identity to you. Once accepted, it's saved in ~/.ssh/known_hosts, and ssh warns you loudly if it ever changes.
root can do anything, including destroy the server with one typo. Create a normal user with sudo rights and give it your key:
adduser deploy # choose a password: sudo will ask for it
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
From your laptop:
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),27(sudo),100(users)
total 12
drwx------ 2 deploy deploy 4096 Oct 20 14:23 .
drwxr-x--- 4 deploy deploy 4096 Oct 20 14:23 ..
-rw------- 1 deploy deploy 94 Oct 20 14:23 authorized_keys
27(sudo) means deploy can run admin commands with sudo. The .ssh folder is drwx------ and the key file -rw-------: only deploy can read them, and sshd refuses keys with looser permissions.
9. Lock the door, and verify it's locked
Two settings close the most common attacks: no root logins, and no passwords, only keys. On Ubuntu 24.04 you put them in a small file under /etc/ssh/sshd_config.d/, which the main config includes near its top.
Here is the first finding from the lab. The included files are read in name order, and for most settings sshd keeps the first value it reads. Cloud images ship 50-cloud-init.conf, which can contain PasswordAuthentication yes (it depends on how the server was created; I recreated that case in my lab). My 99-hardening.conf came after it:
permitrootlogin no
passwordauthentication yes
Root login was off, but password login was still on. The file existed, the syntax check passed, and the setting did nothing. Renaming it to 00-hardening.conf made it load first:
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
sudo sshd -t && sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication) '
sudo systemctl restart ssh
sshd -t: config OK
permitrootlogin no
passwordauthentication no
"First value wins" is the rule for most keywords, not all of them: a few (like Port or ListenAddress) can repeat, and Match blocks change settings for specific users or addresses. That's why you shouldn't reason from file names at all. Ask sshd: sshd -T prints the settings it will actually use.
Keep your current ssh session open while you test from a second terminal. If you locked yourself out, that open session is how you fix it; the DigitalOcean web console is the backup.
$ ssh root@web-1
root@203.0.113.21: Permission denied (publickey).
$ ssh -o PubkeyAuthentication=no deploy@web-1
deploy@203.0.113.21: Permission denied (publickey).
$ ssh deploy@web-1 hostname
web-1
Root is refused, a password-only attempt is refused, and the key still works.
10. Two firewalls, and what's listening
A firewall decides which traffic reaches a server. You get two layers:
- DigitalOcean Cloud Firewall: outside the droplet, free, denies everything you haven't allowed. Create it in the control panel and attach it to web-1 (not tested in my lab).
- ufw (uncomplicated firewall): inside Ubuntu, installed but off until you enable it.
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full' # 80 and 443, once nginx is installed
sudo ufw enable
sudo ufw status verbose
Rules updated
Rules updated (v6)
Rules updated
Rules updated (v6)
Firewall is active and enabled on system startup
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
80,443/tcp (Nginx Full) ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)
80,443/tcp (Nginx Full (v6)) ALLOW IN Anywhere (v6)
Allow SSH before you enable ufw, or you cut off your own connection. Then the second finding. A test app was running on port 8080, and here's what was listening on web-1:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=145,fd=5))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1,fd=3))
LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=335,fd=3))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=145,fd=6))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=1,fd=4))
0.0.0.0:8080 means "every IPv4 interface on this machine", the public one included. From the laptop:
$ curl -sI http://203.0.113.21 | head -1
HTTP/1.1 200 OK
$ curl -s --max-time 3 http://203.0.113.21:8080 (a test app on 8080, not allowed)
(no answer after 3 s: exit 28)
Port 80 answered, port 8080 didn't, and the only thing keeping that test app private was ufw. The app's startup message would only have said it was running.
Where a service should listen depends on who must reach it:
127.0.0.1: only this machine. Right for an app behind nginx on the same server.- A private address like
10.124.0.3: only the private network. Right for a database other servers use. 0.0.0.0: every interface. Right for nginx itself, the public door, with a firewall in front.
Layer 3, the process: what is it listening on? My test app listened on every interface (0.0.0.0:8080). In my lab, ufw alone kept it private.
11. Your private network
Every droplet created in a region joins that region's VPC: a private network only your droplets there can use. Traffic inside it is free and never crosses the public internet.
ip -br addr
ip route
$ ip -br addr
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0 UP 203.0.113.21/24
eth1 UP 10.124.0.2/20
$ ip route
default via 203.0.113.1 dev eth0
10.124.0.0/20 dev eth1 proto kernel scope link src 10.124.0.2
203.0.113.0/24 dev eth0 proto kernel scope link src 203.0.113.21
Read it like a map:
eth0holds the public address, anddefault via ... dev eth0sends everything bound for the internet out of it.eth1holds the private address,10.124.0.2/20. The/20means 4,096 addresses (10.124.0.0to10.124.15.255), and anything for that range goes straight out of eth1.
Then the database. Redis is a small in-memory store, and out of the box it only listens on the machine itself:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=90,fd=6))
LISTEN 0 511 [::1]:6379 [::]:* users:(("redis-server",pid=90,fd=7))
To let web-1 in, set db-1's private address and a password in /etc/redis/redis.conf (bind 127.0.0.1 10.124.0.3 and requirepass <a long password>), then sudo systemctl restart redis-server. In my lab I passed the same two settings as command-line flags:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 511 10.124.0.3:6379 0.0.0.0:* users:(("redis-server",pid=113,fd=7))
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=113,fd=6))
Then allow only the VPC through db-1's firewall:
sudo ufw allow OpenSSH
sudo ufw allow from 10.124.0.0/20 to any port 6379 proto tcp
sudo ufw enable
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
6379/tcp ALLOW 10.124.0.0/20
OpenSSH (v6) ALLOW Anywhere (v6)
From web-1, over the private network:
$ redis-cli -h 10.124.0.3 ping
PONG
$ redis-cli -h 10.124.0.3 incr visits
1
From the laptop, out on the internet:
$ redis-cli -h 203.0.113.22 ping (db-1 public IP)
(no reply after 5 s: exit 124)
$ ping -c 2 -W 1 10.124.0.3 (db-1 private IP)
2 packets transmitted, 0 received, 100% packet loss, time 1048ms
The public IP gave no reply, and the private IP isn't reachable from outside at all. Three locks held at once: the bind address, the password and the firewall.
12. A server with no public IP
The strongest lock is having no public address at all. DigitalOcean calls this a private droplet: untick public IPv4 and IPv6 when you create it. In my lab, db-1 was left with one card:
$ ip -br addr
lo UNKNOWN 127.0.0.1/8 ::1/128
eth1 UP 10.124.0.3/20
(On a real private droplet that single card is called eth0.) Your laptop can't reach it, so you hop through web-1, the jump host. ssh supports this natively; add it to ~/.ssh/config on your laptop:
$ cat ~/.ssh/config
Host web-1
HostName 203.0.113.21
User deploy
Host db-1
HostName 10.124.0.3
User deploy
ProxyJump web-1
$ ssh db-1 "hostname; ip -br addr"
Warning: Permanently added '10.124.0.3' (ED25519) to the list of known hosts.
db-1
lo UNKNOWN 127.0.0.1/8 ::1/128
eth1 UP 10.124.0.3/20
ssh db-1 connected through web-1 in one command, and the private key never left the laptop: web-1 only forwarded the encrypted connection.
One trade-off: a server with no public interface can't reach the internet for updates by itself either. DigitalOcean's NAT gateway solves that, from $40 a month. For learning, section 11's setup (a public IP with the firewall shut) is the cheaper way to practise.
Part 4
Working with AI, cleaning up, what's next
The operator is still you.
13. When an AI assistant does the deploying
More people now let an AI coding agent write deployment scripts, call a cloud API or SSH into a server. That can teach you a lot, and it can move fast. But an assistant optimises for the goals and limits it has been given. If your budget isn't one of them, it may add redundancy, monitoring or managed services a learning project doesn't need, open a port to make something work, use an unsafe default, or leave resources running. Not every assistant does this. You just can't assume cost control or safety unless you ask for it and check it.
Run every AI-assisted deployment this way:
- Ask for a resource inventory, with the reason each resource is needed.
- Ask for estimated recurring and one-time costs, the pricing assumptions, and the usage-based risks.
- Ask for the least complex architecture that meets your requirements.
- Require a plan or dry run first (
terraform plan, for example) where the tooling has one. - Read every command and infrastructure change before you approve it.
- Check firewall rules, bind addresses, authentication, secrets and public exposure yourself.
- Set budget alerts yourself, in the provider's console. Don't accept "I've set a limit" as proof.
- Give the agent the narrowest access that does the job.
- Afterwards, list the resources that actually exist, not the ones the agent says it made.
- Run the cleanup and check the billing page.
On point 8: SSH access and cloud API access are different privilege boundaries. SSH to one server lets an agent change that server. A cloud API token can create, resize and delete resources across your account, which is where money is spent. Prefer short-lived, narrowly scoped credentials, or run the commands yourself from the agent's reviewed plan.
A prompt you can reuse:
Help me deploy this application on a cloud provider under a strict learning budget.
First, inspect my requirements and propose the smallest architecture that satisfies
them. Do not provision anything or execute remote commands yet.
Produce a complete inventory of all resources that would be created: their purpose,
public/private exposure, region, estimated recurring cost, usage-based cost risks and
cleanup procedure. Include compute, disks, IP addresses, networking, bandwidth,
backups, monitoring, databases and other dependencies where relevant.
Identify what is necessary, optional and unnecessary for this workload. Check the
provider's current official pricing where available. Label estimates and assumptions.
Distinguish budget alerts from mechanisms that actually stop spending.
Prefer private access for backend services, least-privilege permissions, restrictive
firewall rules, secure SSH configuration and no exposed secrets. Explain each command
and infrastructure change. Do not run any command, create resources, modify a
production system or connect over SSH until I explicitly approve the reviewed plan.
After approval, make one change at a time, verify the result, report the actual
resources created, and finish with a tested cleanup checklist.
A good prompt improves the odds; it doesn't make the output correct. You are still the operator.
14. After the lab: clean up properly
Lab cleanup is not cloud cleanup. In my rehearsal, cleanup was docker rm. On a real account, check every resource you created, because "off" and "gone" are billed differently:
- DigitalOcean: a powered-off droplet is still billed, because its CPU and memory stay reserved. Destroy it to stop the charge. Then check for volumes, snapshots ($0.06 per GB per month), backups and reserved IPs, each listed separately.
- AWS: a stopped instance stops compute charges, but its EBS volume keeps billing, and so does an Elastic IP, attached or not ($0.005 an hour). An auto-assigned public IP is released when the instance stops. Terminate the instance, then check for leftover volumes, snapshots and Elastic IPs.
- Azure: a VM that's stopped but not deallocated is still billed for compute. Deallocating stops compute, but the disk and a static public IP still bill. Deleting the resource group removes everything in it.
Then:
- List every resource in the region (and every region you touched).
- Delete unattached disks and unused public IPs.
- Check snapshots, backups, load balancers and anything an AI agent created.
- Revoke API tokens you made for the lab.
- Look at the billing page again a day later, once usage has been reported.
If you want the lab back next weekend, snapshot first and recreate from the snapshot. A 25 GB snapshot is about $1.50 a month; a running $12 droplet is $12.
15. What to learn next
When the two servers feel easy, add one thing at a time, each with its own inventory and cleanup:
- A real domain, nginx and free HTTPS with Let's Encrypt.
- Your app as a
systemdservice that restarts when it crashes, listening on127.0.0.1. - Off-server backups, and a restore you actually test.
- Metrics and alerts: CPU, memory, disk full, and an uptime check.
- The whole lab in Terraform, with
terraform planbefore everyapplyandterraform destroyat the end. - A load balancer and a second web server, and what breaks when one dies.
- The same design on AWS or Azure: a VPC or VNet, security groups or NSGs, and their pricing calculator filled in from your inventory.
The whole guide in 10 lines
- A VM's price is compute. A running service also pays for an entry point, storage, traffic and its dependencies.
- List what the workload needs before you price it; give the internet one door.
- A SQL database doesn't need a public IP; reach it over the private network.
- Compare complete inventories, and write down what's included and excluded.
- An alert tells you; it doesn't stop you. Know how you'll delete everything before you create it.
- Check sshd with sshd -T, not by reading file names.
- Bind each service to the narrowest address that works, behind both firewalls.
- eth1 is your private network; databases live there.
- AI can deploy with you; you review, approve and verify.
- Off is not gone: destroy, check for leftovers, read the bill a day later.
How the numbers were checked. List prices for US East on 20 October 2026, Linux, 730 hours, USD, before tax and credits: DigitalOcean Droplet pricing (Basic plans include a public IPv4; per-second billing since January 2026). AWS: the EC2 on-demand price list (t3.small $0.0208/h), EBS volume types (gp3 $0.08/GB-month), VPC pricing (public IPv4 $0.005/h, in use or idle), the EC2 pricing page and AWS data transfer price list (100 GB free per month across services and regions, then $0.09/GB). Azure: the Retail Prices API (B1ms $0.0207/h, Standard static IPv4 $0.005/h, E4 $2.40, E6 $4.80) and bandwidth pricing (100 GB free, then $0.087/GB). "Default disk" means 8 GiB on AWS (a common size for Ubuntu AMIs) and the smallest Standard SSD tier that fits a typical Ubuntu image on Azure (E4, 32 GiB); check the size on your own launch screen. GB and GiB are treated as equal in the egress maths. To recheck any figure, use the same pages or each provider's pricing calculator with your own inventory.
Platform facts are from DigitalOcean's docs as of October 2026: regional availability, resizing, Droplet billing, VPC pricing, Cloud Firewalls, private Droplets and Spend Alerts. Budget behaviour: AWS budget actions, Azure budgets and the Azure spending limit; T3 unlimited mode from AWS's EC2 docs. Terminal output is real, from Ubuntu 24.04.5 containers wired as two droplets and a laptop; the DigitalOcean control-panel steps, Cloud Firewall and doctl were not run.