Admin

Startups

Startup Infrastructure on a Budget: Running Production Workloads Under $500/Month on AWS

Optimize cloud costs for early-stage startups. Covers architecture patterns, free tier maximization, spot instances, and when to scale infrastructure.

By Sujay SinghPublished: June 4, 202611 min read10 views✓ Fact Checked
Startup Infrastructure on a Budget: Running Production Workloads Under $500/Month on AWS
Startup Infrastructure on a Budget: Running Production Workloads Under $500/Month on AWS

Overview

For early-stage startups, every dollar counts. The dream of launching a groundbreaking product often collides with the harsh reality of infrastructure costs. While cloud providers like AWS offer unparalleled scalability and a vast array of services, navigating them to build a production-ready environment without breaking the bank can feel like a labyrinth. Many founders assume that robust, secure, and performant infrastructure is inherently expensive, pushing them towards compromises that can hinder growth or introduce significant technical debt.

At TechNews Venture, we frequently interact with startups grappling with this exact challenge. The good news? It is entirely possible to run production workloads on AWS for under $500 per month, provided you make smart, informed choices. This isn't about cutting corners; it's about optimizing resource allocation, leveraging cost-effective services, and maintaining a lean architecture that can gracefully scale as your business grows.

This article, penned for startup founders, CTOs, and technical leads, will guide you through building a resilient, budget-conscious AWS infrastructure. We'll focus on a common architecture involving a web application, a relational database, static file storage, and DNS management. Our goal is to empower you to deploy your application with confidence, knowing your infrastructure is both reliable and fiscally responsible.

Prerequisites

Before we dive into the technical setup, ensure you have the following in place:

  • AWS Account: An active AWS account with administrative privileges. If you're new to AWS, take advantage of the AWS Free Tier, which can cover some costs for the first 12 months.
  • AWS CLI Configured: The AWS Command Line Interface (CLI) installed and configured on your local machine. This allows you to interact with AWS services from your terminal. You can follow the official AWS documentation for installation and configuration.
  • Basic Linux Skills: Familiarity with Linux command-line operations (SSH, file system navigation, package management).
  • Docker & Docker Compose Knowledge: An understanding of containerization concepts and how to build/run applications using Docker and Docker Compose. This will be our primary deployment method for the application server.
  • Networking Fundamentals: Basic knowledge of IP addresses, subnets, security groups, and routing tables.
  • A Simple Application: A containerized web application (e.g., a Python Flask app, Node.js app, or a simple static site served by Nginx) ready for deployment. For demonstration purposes, we'll assume a basic web application that connects to a database.
  • Domain Name: A registered domain name that you can manage through AWS Route 53.

Detailed Steps with Commands

We will build our infrastructure in the us-east-1 (N. Virginia) region, a common choice due to its extensive service offerings and often competitive pricing. Remember to replace placeholder values like your-project-name, your-domain.com, and security credentials with your actual details.

1. Virtual Private Cloud (VPC) Setup

A VPC is your isolated network within AWS. We'll create a VPC, public and private subnets, an Internet Gateway, and route tables to control traffic flow. This setup allows your web server to be publicly accessible while keeping your database private.

Create VPC

First, define your network range.

aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=your-project-name-vpc}]'

Note down the `VpcId` from the output. Let's assume it's `vpc-0abcdef1234567890`.

Create Internet Gateway (IGW)

This allows communication between your VPC and the internet.

aws ec2 create-internet-gateway --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=your-project-name-igw}]'

Note down the `InternetGatewayId`. Let's assume it's `igw-0fedcba9876543210`.

Attach IGW to VPC

aws ec2 attach-internet-gateway --internet-gateway-id igw-0fedcba9876543210 --vpc-id vpc-0abcdef1234567890

Create Subnets

We'll create one public subnet (for the EC2 instance) and one private subnet (for the RDS database). For high availability, you'd typically use multiple subnets across different Availability Zones (AZs), but for budget constraints, we'll start with one of each in a single AZ.

# Public Subnet (e.g., in us-east-1a)
aws ec2 create-subnet --vpc-id vpc-0abcdef1234567890 --cidr-block 10.0.1.0/24 --availability-zone us-east-1a --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=your-project-name-public-subnet-1a}]'

Note down the `SubnetId` for the public subnet (e.g., `subnet-0123456789abcdef0`).

# Private Subnet (e.g., in us-east-1a)
aws ec2 create-subnet --vpc-id vpc-0abcdef1234567890 --cidr-block 10.0.2.0/24 --availability-zone us-east-1a --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=your-project-name-private-subnet-1a}]'

Note down the `SubnetId` for the private subnet (e.g., `subnet-0fedcba9876543210`).

Create Route Tables

A route table determines where network traffic from your subnets is directed.

# Public Route Table
aws ec2 create-route-table --vpc-id vpc-0abcdef1234567890 --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=your-project-name-public-rt}]'

Note down the `RouteTableId` (e.g., `rtb-0123456789abcdef0`).

# Add route to IGW for public route table
aws ec2 create-route --route-table-id rtb-0123456789abcdef0 --destination-cidr-block 0.0.0.0/0 --gateway-id igw-0fedcba9876543210
# Associate public subnet with public route table
aws ec2 associate-route-table --subnet-id subnet-0123456789abcdef0 --route-table-id rtb-0123456789abcdef0

For the private subnet, it will automatically get a route table with a local route. We don't need an explicit route to the internet for the private subnet as our database won't directly access the internet.

Create Security Groups

Security Groups act as virtual firewalls for your instances and databases.

# Security Group for Web Server (EC2)
aws ec2 create-security-group --group-name your-project-name-web-sg --description "Allow HTTP/HTTPS and SSH access" --vpc-id vpc-0abcdef1234567890

Note down the `GroupId` (e.g., `sg-0abcdef1234567890`).

# Allow SSH from your IP (replace 0.0.0.0/0 with your actual public IP for better security)
aws ec2 authorize-security-group-ingress --group-id sg-0abcdef1234567890 --protocol tcp --port 22 --cidr 0.0.0.0/0
# Allow HTTP
aws ec2 authorize-security-group-ingress --group-id sg-0abcdef1234567890 --protocol tcp --port 80 --cidr 0.0.0.0/0
# Allow HTTPS (recommended for production)
aws ec2 authorize-security-group-ingress --group-id sg-0abcdef1234567890 --protocol tcp --port 443 --cidr 0.0.0.0/0
# Security Group for Database (RDS)
aws ec2 create-security-group --group-name your-project-name-db-sg --description "Allow database access from web server" --vpc-id vpc-0abcdef1234567890

Note down the `GroupId` (e.g., `sg-0fedcba9876543210`).

# Allow PostgreSQL (port 5432) or MySQL (port 3306) access from the web server's security group
# Replace 5432 with 3306 if using MySQL
aws ec2 authorize-security-group-ingress --group-id sg-0fedcba9876543210 --protocol tcp --port 5432 --source-group sg-0abcdef1234567890

2. Compute (EC2)

We'll launch a single EC2 instance to run our containerized application. For budget efficiency, we'll use a `t3.micro` instance, which offers a balance of compute, memory, and burstable performance. We'll assign an Elastic IP to ensure a static public IP address.

Create Key Pair

You need a key pair to SSH into your instance.

aws ec2 create-key-pair --key-name your-project-name-key --query 'KeyMaterial' --output text > your-project-name-key.pem
chmod 400 your-project-name-key.pem

Allocate Elastic IP

This gives your EC2 instance a static public IP address, necessary for DNS mapping.

aws ec2 allocate-address --domain vpc --tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=your-project-name-eip}]'

Note down the `AllocationId` (e.g., `eipalloc-0123456789abcdef0`) and `PublicIp` (e.g., `3.8.113.123`).

Launch EC2 Instance

We'll use Amazon Linux 2 AMI, which is stable and well-supported.

# Find the latest Amazon Linux 2 AMI ID for us-east-1
# aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available" --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text
# As of writing, a common ImageId is 'ami-053b0d53c279acc90'

aws ec2 run-instances \
    --image-id ami-053b0d53c279acc90 \
    --instance-type t3.micro \
    --key-name your-project-name-key \
    --security-group-ids sg-0abcdef1234567890 \
    --subnet-id subnet-0123456789abcdef0 \
    --associate-public-ip-address \
    --block-device-mappings "DeviceName=/dev/xvda,Ebs={VolumeSize=30,VolumeType=gp2}" \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=your-project-name-web-server}]'

Note down the `InstanceId` (e.g., `i-0fedcba9876543210`).

Associate Elastic IP with EC2 Instance

aws ec2 associate-address --instance-id i-0fedcba9876543210 --allocation-id eipalloc-0123456789abcdef0

Configure EC2 Instance (SSH and Docker)

SSH into your EC2 instance using the Elastic IP you allocated:

ssh -i your-project-name-key.pem ec2-user@3.8.113.123

Once connected, install Docker and Docker Compose:

sudo yum update -y
sudo amazon-linux-extras install docker -y
sudo service docker start
sudo usermod -a -G docker ec2-user
# Log out and log back in for group changes to take effect: exit then ssh again.
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker --version
docker-compose --version

Deploy Your Application

Create your application directory and place your `Dockerfile` and `docker-compose.yml` there. For example, a simple Flask app:

app.py:

from flask import Flask
import os
import psycopg2

app = Flask(__name__)

DB_HOST = os.environ.get('DB_HOST', 'localhost')
DB_NAME = os.environ.get('DB_NAME', 'mydatabase')
DB_USER = os.environ.get('DB_USER', 'myuser')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'mypassword')

@app.route('/')
def hello():
    try:
        conn = psycopg2.connect(host=DB_HOST, database=DB_NAME, user=DB_USER, password=DB_PASSWORD)
        cur = conn.cursor()
        cur.execute("SELECT version();")
        db_version = cur.fetchone()[0]
        cur.close()
        conn.close()
        return f"Hello from Flask! Connected to PostgreSQL: {db_version}"
    except Exception as e:
        return f"Hello from Flask! Could not connect to database: {e}", 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

requirements.txt:

Flask==2.3.2
psycopg2-binary==2.9.9

Dockerfile:

FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 80
CMD ["python", "app.py"]

docker-compose.yml:

version: '3.8'
services:
  web:
    build: .
    ports:
      - "80:80"
    environment:
      DB_HOST: your-project-name-db.czwxyzwxyzwx.us-east-1.rds.amazonaws.com # Replace with your RDS endpoint
      DB_NAME: mydatabase
      DB_USER: myuser
      DB_PASSWORD: mypassword
    restart: always

On your EC2 instance, create the files and then run:

mkdir your-app
cd your-app
# Use vi or nano to create app.py, requirements.txt, Dockerfile, docker-compose.yml
docker-compose up -d

3. Database (RDS PostgreSQL)

AWS Relational Database Service (RDS) provides managed databases. We'll use a `db.t3.micro` instance with PostgreSQL, deployed in our private subnet, ensuring it's not directly accessible from the internet.

Create DB Subnet Group

RDS instances need a DB Subnet Group, which specifies the subnets the database can use.

aws rds create-db-subnet-group \
    --db-subnet-group-name your-project-name-db-subnet-group \
    --db-subnet-group-description "Subnet group for your-project-name RDS" \
    --subnet-ids subnet-0fedcba9876543210 \
    --tag-specifications 'ResourceType=db-subnet-group,Tags=[{Key=Name,Value=your-project-name-db-subnet-group}]'

Create RDS Instance

Replace `masteruser` and `masterpassword` with strong, unique credentials.

aws rds create-db-instance \
    --db-instance-identifier your-project-name-db \
    --db-instance-class db.t3.micro \
    --engine postgres \
    --master-username myuser \
    --master-user-password mypassword \
    --allocated-storage 20 \
    --vpc-security-group-ids sg-0fedcba9876543210 \
    --db-subnet-group-name your-project-name-db-subnet-group \
    --publicly-accessible false \
    --engine-version 14.7 \
    --license-model postgresql-license \
    --multi-az false \
    --storage-type gp2 \
    --backup-retention-period 7 \
    --tag-specifications 'ResourceType=db-instance,Tags=[{Key=Name,Value=your-project-name-db}]'

This command will take some time to complete. You can check its status:

aws rds describe-db-instances --db-instance-identifier your-project-name-db --query "DBInstances[0].DBInstanceStatus" --output text

Once available, retrieve its endpoint:

aws rds describe-db-instances --db-instance-identifier your-project-name-db --query "DBInstances[0].Endpoint.Address" --output text

This endpoint (e.g., `your-project-name-db.czwxyzwxyzwx.us-east-1.rds.amazonaws.com`) is what you'll use in your application's `docker-compose.yml` for `DB_HOST`.

4. Storage (S3)

Amazon S3 is ideal for storing static assets (images, CSS, JS), user-uploaded files, and backups. It's highly durable, scalable, and cost-effective.

Create S3 Bucket

Bucket names must be globally unique.

aws s3 mb s3://your-startup-static-assets-bucket-unique-name

To make static assets publicly accessible (e.g., for a website):

# Block all public access by default for new buckets
aws s3api put-public-access-block \
    --bucket your-startup-static-assets-bucket-unique-name \
    --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# If you need public access for specific objects (e.g., static website hosting),
# you'd adjust the public access block and add a bucket policy.
# For a lean startup, often S3 is used for private storage or accessed via CloudFront (later stage).
# For now, let's assume private storage.

You can upload files using:

aws s3 cp local-file.jpg s3://your-startup-static-assets-bucket-unique-name/images/local-file.jpg

5. DNS (Route 53)

Route 53 is AWS's highly available and scalable Domain Name System (DNS) web service. We'll use it to point your domain to your EC2 instance's Elastic IP.

Create Hosted Zone

If your domain is already registered with Route 53, you can skip this. Otherwise, you'll need to create a hosted zone and update your domain registrar's NS records.

aws route53 create-hosted-zone --name your-domain.com --caller-reference "$(date +%s)" --vpc Id=vpc-0abcdef1234567890,VPCRegion=us-east-1

Note down the `Id` (e.g., `/hostedzone/Z1A2B3C4D5E6F7`). You'll get a list of Name Servers (NS) in the output; update your domain registrar with these.

Create A Record

This record maps your domain to your EC2's Elastic IP.

aws route53 change-resource-record-sets \
    --hosted-zone-id /hostedzone/Z1A2B3C4D5E6F7 \
    --change-batch '{
        "Comment": "Create A record for your-domain.com",
        "Changes": [
            {
                "Action": "UPSERT",
                "ResourceRecordSet": {
                    "Name": "your-domain.com",
                    "Type": "A",
                    "TTL": 300,
                    "ResourceRecords": [
                        {
                            "Value": "3.8.113.123"
                        }
                    ]
                }
            }
        ]
    }'

You might also want a `www` record:

aws route53 change-resource-record-sets \
    --hosted-zone-id /hostedzone/Z1A2B3C4D5E6F7 \
    --change-batch '{
        "Comment": "Create A record for www.your-domain.com",
        "Changes": [
            {
                "Action": "UPSERT",
                "ResourceRecordSet": {
                    "Name": "www.your-domain.com",
                    "Type": "A",
                    "TTL": 300,
                    "ResourceRecords": [
                        {
                            "Value": "3.8.113.123"
                        }
                    ]
                }
            }
        ]
    }'

After DNS propagation (which can take a few minutes to a few hours), your application should be accessible via `http://your-domain.com`.

6. Monitoring (CloudWatch)

AWS CloudWatch provides monitoring for AWS resources. By default, it collects basic metrics for EC2 and RDS. Setting up alarms is crucial for proactive issue detection.

Create Billing Alarm

This is a critical first step to ensure you don't exceed your budget.

aws cloudwatch put-metric-alarm \
    --alarm-name "BudgetAlarm_500USD" \
    --alarm-description "Alarm when AWS estimated charges exceed $4
📧

Enjoyed this article?

Get articles like this delivered to your inbox daily. Join 10,000+ tech professionals.

Written By

Sujay Singh

Technology Expert / Cloud Architect at Virtual Venture covering AI, cloud computing, cybersecurity, and emerging tech trends.

Sources & References

• Official company announcements and press releases

• Industry reports from Gartner, IDC, and Statista

• Peer-reviewed research and technical documentation

• On-record statements from industry experts

Last verified: June 4, 2026

Fact-checked by TechNews Venture editorial team

Leave a Comment

Comments are moderated and will appear after review.