Home/Blog

Is AWS Overkill for a Small Startup? When a VPS Is Enough

When one VPS is enough for a startup and when AWS earns its cost: a decision table by stage, a minimum production VPS setup, and a cheap path to AWS.

Is AWS Overkill for a Small Startup? When a VPS Is Enough

For most small startups with one product, modest traffic and no compliance audit on the calendar, AWS is more platform than you need. A well-run VPS, one server with off-site backups, patching, TLS and monitoring, carries a product a long way at a flat monthly price. AWS earns its cost when you need managed database failover, compliance evidence a customer will check, several regions, or scoped access for a growing team. The VPS is only enough if you also do the boring parts, and the most important one is a restore you have actually tested.

Key takeaways

  • AWS sells managed failure handling, compliance evidence and breadth. If you need none of those yet, you still pay for them in configuration, IAM and billing complexity.
  • A single VPS is a single point of failure. That is acceptable only with tested off-site backups and a written rebuild plan whose duration the business can live with.
  • A VPS bill is flat and bundles traffic. An AWS bill has many meters, and NAT gateways, public IPv4 addresses and egress surprise small teams more than compute does.
  • Build on portable pieces: containers, Postgres, S3-compatible object storage and Terraform. Then moving to AWS later is a migration, not a rewrite.
  • Move when a trigger appears, such as an enterprise security review, a database you cannot afford to lose for an hour, or traffic one box cannot serve. Not because the credits arrived.

The short answer

Stay on a VPS while customers would tolerate an hour or two of downtime during a rebuild, no contract or regulation demands controls you cannot show on one server, and one machine with headroom handles your peak. Move to AWS, or another hyperscaler, when the database must survive a machine failure without a human restoring it, when a customer's security review asks for audit logs and access controls you would have to build, when regulated data needs a provider agreement, or when traffic needs capacity in minutes or several regions.

There is a middle path. Lightsail is AWS's own VPS product, with flat pricing inside an AWS account, and many VPS providers sell managed Postgres, which removes the hardest job on the list below.

What AWS gives you that a VPS does not

CapabilityOn AWSOn a single VPS
Database failoverRDS Multi-AZ keeps a standby in another availability zone and fails over automaticallyYou restore from backup onto a new machine
Point-in-time recoveryBuilt into RDS automated backupsYou set up WAL archiving yourself, or accept nightly granularity
Access controlIAM roles per service and per person, every API call logged in CloudTrailSSH keys and sudo; audit trail is whatever you configure
Compliance evidenceProvider audit reports through AWS Artifact; HIPAA-eligible services under a BAAYour provider's data center certifications, plus everything above the OS is on you
CapacityAutoscaling groups, load balancers across zones, many regionsResize the server, or add a second one by hand
Building blocksManaged queues, email, CDN, secrets and schedulersRun them yourself or buy them separately

What you pay for that, beyond the compute itself, is meters. In July 2026 the AWS pricing pages list these, and they are the ones small teams trip over:

  • NAT gateway. In US East (Ohio), $0.045 per hour plus $0.045 per GB processed: about $33 a month per gateway before any traffic, and the per-GB charge applies even to traffic that never leaves AWS.
  • Public IPv4 addresses. $0.005 per hour for every public IPv4 address, in use or idle. Default VPC subnets assign one to every instance you launch.
  • Internet egress. The first 100 GB a month to the internet is free across all services, then every gigabyte is billed. Video, downloads and file sync feel this first.

None of these meters exist in the same form on a typical VPS bill, which is a fixed monthly price for CPU, memory, disk and a traffic allowance. If you do go to AWS, our cost-conscious AWS architecture guide covers how to avoid the NAT and cross-zone traps.

What a VPS asks of you

A VPS is cheap because the provider sells you a machine and stops there. Everything AWS's managed services do, you now do, or consciously decide not to.

JobWho does it on managed AWS servicesWho does it on a VPS
OS security patchesAWS (RDS, Fargate)You, via unattended upgrades and reboots
SSH and firewallMostly not exposedYou
TLS certificatesACM on the load balancerYou, via Caddy or certbot
Database backupsRDS automated backupsYou, off-site, encrypted
Restore testingStill youStill you
FailoverMulti-AZA written runbook and a rehearsed rebuild
Monitoring and alertingCloudWatch, if you configure alarmsYou, with an external check
Disk spaceMostly managedYou; logs and images fill disks

A VPS is not free operations; it is cheaper operations for a smaller blast radius. A team that will not do the right-hand column should not run one in production.

A decision table by stage

SignalPoints toWhy
Pre-launch, validating the productVPSSpeed and a flat bill matter more than failover
First paying customers, team of a fewVPS, with tested backups and monitoringAn hour of downtime hurts but does not end the company
Enterprise prospect sends a security questionnaireAWS, or managed services for the database at leastAudit logs, access reviews and encryption at rest are documented answers there
Regulated data (health, payments scope)AWS or another provider with the right agreementsYou need contractual coverage, not only good practice
Database downtime of an hour costs real revenueManaged Postgres with failoverRestoring from backup by hand is too slow
Unpredictable traffic spikesAWSCapacity in minutes, not a manual resize
Steady, egress-heavy trafficVPS or a provider with bundled trafficPer-GB egress billing dominates the bill
Several engineers needing scoped, audited accessAWSIAM and CloudTrail beat shared sudo
Users in several continents with latency or residency needsAWSRegions and data residency options

On credits: in July 2026 a new AWS account gets $100 in Free Tier credits at signup and can earn up to $100 more, with the Free plan lasting up to six months or until the credits run out. AWS Activate offers self-funded startups up to $5,000 (starting at $1,000) and up to $200,000 through an approved investor or accelerator. Take them if AWS is the right home anyway, but do not let them choose your architecture: they expire, and the bill does not.

You do not need Kubernetes on either side of this decision. Our Kubernetes vs serverless comparison explains why a container host or a managed container service covers most early products.

The minimum production setup on a VPS

This is the setup we consider the floor for a production VPS running a current Ubuntu LTS release with Docker. It assumes one server for the app and database, object storage at a different provider for backups, and monitoring that runs elsewhere.

1. SSH: keys only, no root

# /etc/ssh/sshd_config.d/00-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

The file name matters. OpenSSH uses the first value it reads for each setting, and cloud images often ship /etc/ssh/sshd_config.d/50-cloud-init.conf with PasswordAuthentication yes. A file that sorts earlier wins. Check the effective configuration, not the file:

sudo sshd -t && sudo systemctl restart ssh
sudo sshd -T | grep -Ei '^(passwordauthentication|permitrootlogin)'

2. Firewall, and the Docker trap

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Then the part most guides skip: Docker's documentation states that published container ports bypass ufw, because Docker routes that traffic before ufw's rules see it. A 5432:5432 port mapping added for debugging puts your database on the internet whatever the firewall says. Publish only the reverse proxy, and bind anything else you must publish to 127.0.0.1.

3. Patching and reboots

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Security updates then install daily, but a new kernel only runs after a reboot. Watch for /var/run/reboot-required, or let the system reboot itself in a quiet window via /etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

4. One Compose file, one published proxy

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
 
  app:
    image: ghcr.io/your-org/app:1.4.2
    restart: unless-stopped
    env_file: .env
    depends_on:
      - db
 
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
 
secrets:
  db_password:
    file: ./secrets/db_password.txt
 
volumes:
  caddy_data:
  caddy_config:
  pgdata:

Only Caddy publishes ports. The database has no ports entry, so it is reachable only from the app container. Caddy obtains and renews certificates on its own:

app.example.com {
	reverse_proxy app:3000
	encode gzip
}

Pin image tags to versions, not latest, so a restart never upgrades something by surprise.

5. Logs that cannot fill the disk

Docker's default json-file logging driver performs no rotation, and a full disk takes Postgres down with it. Docker's docs recommend the local driver, which rotates by default. In /etc/docker/daemon.json:

{
  "log-driver": "local"
}

Restart Docker, then run docker compose up -d --force-recreate, because existing containers keep their old logging settings.

6. Backups that leave the building

Back up to object storage at a different provider. Provider snapshots are handy for rollbacks, but they live in the same account as the server, so a locked account or a compromised login takes both.

#!/usr/bin/env bash
# /usr/local/bin/backup-db.sh
set -euo pipefail
 
# RESTIC_REPOSITORY, RESTIC_PASSWORD_FILE, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
set -a; . /root/.backup.env; set +a
 
mkdir -p /var/backups/pg
cd /srv/app
docker compose exec -T db pg_dump -U app -Fc app > /var/backups/pg/app.dump
 
restic backup /var/backups/pg
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
 
# tell the heartbeat monitor the backup finished
curl -fsS -m 10 --retry 5 -o /dev/null "https://hc-ping.com/$HC_UUID"

RESTIC_REPOSITORY points at an S3-compatible bucket, for example s3:https://your-storage-endpoint/bucket-name, and HC_UUID lives in the same env file. Schedule it in /etc/cron.d/backup-db:

15 3 * * * root /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1

The heartbeat matters more than the script. Because of set -e, a failed dump never reaches the ping, so the heartbeat service alerts the next morning instead of you discovering the gap on the day you need the backup.

7. A restore you have actually run

Once a month, restore the latest backup into a scratch database and query it:

restic restore latest --target /tmp/restore-test
cd /srv/app
docker compose exec -T db createdb -U app restore_check
docker compose exec -T db pg_restore -U app -d restore_check < /tmp/restore-test/var/backups/pg/app.dump
docker compose exec -T db psql -U app -d restore_check -c "select count(*) from users;"
docker compose exec -T db dropdb -U app restore_check

Time it. That number, plus the time to provision a new server, is your real recovery time. Our disaster recovery guide covers how to set recovery time and recovery point targets the business agrees to.

8. Monitoring from outside

Four checks, all alerting to a phone:

  • An HTTP uptime check that runs from a different provider, so it still works when your server does not.
  • Host metrics with alerts on disk usage, memory and CPU. Disk is the one that bites.
  • The backup heartbeat above.
  • Certificate expiry, in case automatic renewal breaks.

9. A rebuild runbook

Write down, and rehearse, the steps to replace the server: provision from Terraform or cloud-init, install Docker, pull the images, restore the database, switch DNS on a low TTL. A rehearsed rebuild is a plan. An unrehearsed one is a hope.

Keeping the move to AWS cheap later

The decisions that make a later move cheap are the same ones that make a VPS pleasant to run.

On the VPSOn AWSMove effort
Docker images in a registryECS on FargateReuse the images; write task definitions
Postgres in a containerRDS for PostgreSQL or Aurora PostgreSQLpg_dump and restore, or logical replication for a short cutover
S3-compatible object storageS3Same SDK; change endpoint and credentials; copy with rclone
CaddyApplication Load Balancer with ACM certificatesConfiguration only
.env filesSecrets Manager or Parameter StoreChange how the app reads secrets at startup
Terraform with your VPS host's provider pluginTerraform with the AWS providerSame workflow, new resources
cronEventBridge SchedulerRewrite the schedule definitions

Habits that keep the door open:

  • Store user files in object storage from day one, never on the server's disk.
  • Configure everything through environment variables, so the same image runs anywhere.
  • Avoid provider-specific features you cannot replace in a day.
  • Keep DNS at an independent provider, so a move is a record change.
  • Keep schema changes in migrations in the repo, not applied by hand.

For the rest of the zero-to-production list, see our startup infrastructure checklist.

What goes wrong in production

The disk fills up. Unrotated container logs, old images and dump files accumulate until Postgres cannot write. Rotate logs, run docker image prune after deploys, and alert on disk well before it is full.

A database port quietly goes public, or password SSH comes back. Both happen through files you did not write: a debug port mapping that Docker routes around ufw, a cloud-init drop-in that wins over your hardening file. Scan your own public IP from outside now and then, and check sshd -T rather than the file.

Backups that were never restorable. The dump ran as the wrong user, wrote an empty file, or the cron job lost its environment variables. Only a restore proves a backup.

Kernel patches installed, never applied. Unattended upgrades install the new kernel; the server runs the old one until it reboots. Schedule reboots.

The provider account is the single point of failure. An expired card, an abuse complaint or a phished login locks the account, and the snapshots inside it. Keep backups and DNS elsewhere.

On the AWS side, the bill is the outage. NAT gateway processing charges, idle public IPv4 addresses and resources left running in a region nobody looks at. Set a budget alert on day one and tag everything.

Frequently Asked Questions

Is AWS overkill for a small startup?

Often, yes. With one product, modest traffic, no compliance audit and a small team, most of what AWS offers adds configuration, billing meters and IAM work you do not need yet, and a well-run VPS with tested backups and monitoring is enough. It stops being enough when you need managed database failover, compliance evidence, several regions or scoped access for a growing team.

When should a startup move to AWS?

Move when a concrete trigger appears: a customer's security review asks for controls you cannot show, an hour of database downtime would cost real revenue, traffic outgrows one server, or regulated data needs a provider agreement such as a HIPAA BAA. Credits alone are a weak reason, because they expire and the architecture stays. If you built on containers and Postgres, the move is a migration, not a rewrite.

Is a single VPS reliable enough for production?

It is reliable enough if the business can tolerate the time it takes to rebuild it. A single server is a single point of failure, so what matters is how fast you can provision a replacement, how recent your last off-site backup is, and whether you have ever restored it. Rehearse the rebuild and decide whether that time is acceptable before your customers decide for you.

What do you give up by not using AWS?

You give up managed database failover, point-in-time recovery you do not have to build, fine-grained IAM with a full audit trail, autoscaling, many regions and a ready answer to most security questionnaires. You also give up startup credits. In exchange you get a flat bill, far fewer moving parts and an environment one engineer can understand completely.

Are AWS startup credits worth it?

They are worth taking if you were going to use AWS anyway. In July 2026, AWS Activate offers self-funded startups up to $5,000 in credits, starting at $1,000, and up to $200,000 through an approved investor or accelerator. Credits expire, and an architecture chosen because it was free can be expensive once they do, so design as if you were paying from day one.

How do you monitor a VPS without a full observability stack?

Cover four things: an uptime check that runs from outside the server, host metrics with alerts on disk, memory and CPU, a heartbeat that alerts when the nightly backup does not report in, and certificate expiry. Small tools such as Uptime Kuma, Netdata or Beszel, plus a heartbeat service such as Healthchecks.io, do this without Prometheus and Grafana. Send alerts to a phone, not only an inbox.

Need a second opinion on your infrastructure?

We run single servers and AWS estates, and we are on the pager for what we build, so our advice leans toward the setup you can actually operate. If you are deciding where your product should live, or whether it is time to move, we will look at what you have and tell you plainly.