Aryan Tripathi — Writing
← All writing

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.

#cloud#digitalocean#linux#ssh#networking#cost#beginners

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:

PartWhat it doesHow it's usually billed
Compute (the VM)runs your codeper hour or second
Public entry point (an IPv4 address, or a load balancer)lets the internet reach youper hour, sometimes even when idle
Diskkeeps the OS and your dataper GB per month, attached or not
Outbound trafficevery response that leaves the cloudper GB after a free allowance
DNS, domain, TLSa name people can type, and HTTPSdomain per year; certificates can be free
Backups and snapshotsgetting your data backper GB, or a % of the server price
Firewalls, private networkswho can reach whatoften free; some network services aren't
Monitoring, logs, alertsknowing it broke before users tell youfree tiers, then per metric or GB
Databases, caches, queuesstate your app depends onper node or per use
Redundancy (a second server, zone or region)surviving a failureroughly doubles the parts it copies
Taxes, currency, regionthe bill you actually payvaries 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.

RememberA VM's price is the price of compute. A running service also pays for an entry point, storage, traffic and whatever it depends on.

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:

Interactivewhat each workload actually needs

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
Make this list before you open any pricing calculator. A calculator can only price the parts you remember to add.

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.

RememberList what your workload needs before you price it. Give the internet one door, and keep everything behind it private.

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.

Interactivelist prices · US East · checked 20 Oct 2026
Disk:
0 (a learning lab)3,000 GB
DigitalOcean · Basic droplet · 1 vCPU · 2 GiB$12.00

Basic droplet (plan price) $12.00 (disk, IPv4 and 2,000 GiB out are in the plan)

AWS · t3.small · 2 vCPU · 2 GiB$22.83

t3.small on-demand $15.18 + 50 GB gp3 $4.00 + public IPv4 $3.65

Azure · B1ms · 1 vCPU · 2 GiB$23.56

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.

An estimate, not a quote: list prices for 730 hours, before tax, without free credits, savings plans or the usage-based charges listed below. The point is the shape of each bill, not the cents.

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.

RememberCompare complete inventories, not VM prices, and write down what you included and what you didn't.

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.

RememberA calculator can only price what you tell it about. Inventory first, calculator second.

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.

RememberStart small and understandable. Move when your workload, your job or your reliability target needs what a larger cloud offers.

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.
ProviderWhat you can setDoes it stop spending?
DigitalOceanSpend Alerts: a monthly budget with percentage thresholdsNo. It notifies, within about an hour of crossing a threshold.
AWSBudgets with alerts; optional budget actionsAlerts 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.
AzureBudgets in Cost ManagementNo. 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.
RememberAn alert tells you; it doesn't stop you. Pick small resources, set low alerts, and know how you'll delete everything before you create it.

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.

The mental modelthe lab in this guide · allowed paths in solid lines
INTERNETyour laptopanywhereCloud FirewallPRIVATE NETWORK (VPC) 10.124.0.0/20web-1public entry point · jump hostnginx · ufw · sshdeth0 public203.0.113.21eth1 private10.124.0.2db-1Redis · no public IP10.124.0.3① ssh 22 · http 80/443② app → redis 6379③ ssh via ProxyJump✕ laptop → db-1 directly: no route
One public door. web-1 is the public entry point and doubles as the jump host. db-1 lives only on the private network: the app reaches Redis over eth1, and your laptop can't reach db-1 at all except by hopping through web-1.

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.

Animatedwhat ssh-keygen and ssh actually do
your laptopid_ed25519 (private)id_ed25519.pub (public)never copy the private key anywheredropletauthorized_keyspublic key, oncechallengesigned answer ✓

Your laptop signs it with the private key. The server checks the signature with the public key. No password crossed the network.

step 4 of 4
The private key proves who you are without ever being sent. That's why key login is safer than any password.

Create the droplets (not tested here; follow DigitalOcean's "How to Create a Droplet" doc if the screens differ):

  1. Image: Ubuntu 24.04 (LTS).
  2. Region: the one closest to you (for India, BLR1). Put both droplets in the same region, so they share its VPC.
  3. Size: Basic, Regular SSD, $6 or $12. Leave backups off for the lab.
  4. Authentication: SSH Key. Paste the contents of ~/.ssh/id_ed25519.pub, never the file without .pub.
  5. Hostnames: web-1, then db-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
RememberMake an ed25519 key, give servers only the .pub half, and keep related droplets in one region.

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.

RememberLog in once as root to create a sudo user with your key, then use that user for everything.

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.

RememberDon't trust a config file because it exists. Check what sshd will really do with sshd -T, then test from a second terminal.

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.
Animateddefence in depth on web-1 · ports from my lab
1. Cloud Firewall2. ufw3. listening on22 sshallowallowsshd on 0.0.0.080 httpallowallownginx on 0.0.0.08080 test appdenydenypython on 0.0.0.0

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.

step 4 of 4
Allow what you need in the Cloud Firewall, repeat it in ufw, and bind services to the narrowest address that works.
RememberAllow only what you serve in both firewalls, bind each service to the narrowest address that works, and check with ss -tlnp.

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:

  • eth0 holds the public address, and default via ... dev eth0 sends everything bound for the internet out of it.
  • eth1 holds the private address, 10.124.0.2/20. The /20 means 4,096 addresses (10.124.0.0 to 10.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.

Remembereth1 is your private address. Bind databases there, require a password, and allow only the VPC range in 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.

RememberNo public IP is the strongest lock. Reach it through a jump host, and plan how it will get updates.

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:

  1. Ask for a resource inventory, with the reason each resource is needed.
  2. Ask for estimated recurring and one-time costs, the pricing assumptions, and the usage-based risks.
  3. Ask for the least complex architecture that meets your requirements.
  4. Require a plan or dry run first (terraform plan, for example) where the tooling has one.
  5. Read every command and infrastructure change before you approve it.
  6. Check firewall rules, bind addresses, authentication, secrets and public exposure yourself.
  7. Set budget alerts yourself, in the provider's console. Don't accept "I've set a limit" as proof.
  8. Give the agent the narrowest access that does the job.
  9. Afterwards, list the resources that actually exist, not the ones the agent says it made.
  10. 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.

RememberAI can build infrastructure with you. You still review, approve and verify what it creates, what it exposes and what it costs.

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.

RememberOff is not gone. Delete what you don't need, check for leftovers, and look at the bill a day later.

15. What to learn next

When the two servers feel easy, add one thing at a time, each with its own inventory and cleanup:

  1. A real domain, nginx and free HTTPS with Let's Encrypt.
  2. Your app as a systemd service that restarts when it crashes, listening on 127.0.0.1.
  3. Off-server backups, and a restore you actually test.
  4. Metrics and alerts: CPU, memory, disk full, and an uptime check.
  5. The whole lab in Terraform, with terraform plan before every apply and terraform destroy at the end.
  6. A load balancer and a second web server, and what breaks when one dies.
  7. The same design on AWS or Azure: a VPC or VNet, security groups or NSGs, and their pricing calculator filled in from your inventory.
RememberGrow one piece at a time, and give every new piece an inventory line, a cost and a cleanup step.

The whole guide in 10 lines

  1. A VM's price is compute. A running service also pays for an entry point, storage, traffic and its dependencies.
  2. List what the workload needs before you price it; give the internet one door.
  3. A SQL database doesn't need a public IP; reach it over the private network.
  4. Compare complete inventories, and write down what's included and excluded.
  5. An alert tells you; it doesn't stop you. Know how you'll delete everything before you create it.
  6. Check sshd with sshd -T, not by reading file names.
  7. Bind each service to the narrowest address that works, behind both firewalls.
  8. eth1 is your private network; databases live there.
  9. AI can deploy with you; you review, approve and verify.
  10. 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.