You built the product. Now it needs to run somewhere reliable.
Manually clicking through AWS consoles works for demos. It breaks at 3 AM. It fails when you need a staging environment. Infrastructure as Code (IaC) with Terraform fixes all of this.
Here are 5 production-ready Terraform templates for indie hackers and solo devs.
Why Terraform Over Pulumi/CDK/SST?
- HCL is readable — even non-devs can review your infra
- 15,000+ free modules in the registry
- Providers for everything — AWS, GCP, Cloudflare, Vercel, GitHub
- 100% free — OpenTofu is the open-source fork with zero lock-in
- Massive community — every error is already solved on Stack Overflow
The 4 Commands You'll Use 95% of the Time
terraform init # Download providers
terraform plan # Preview changes (ALWAYS run first)
terraform apply # Create/update resources
terraform destroy # Remove everything
Template 1: AWS Minimal SaaS Stack (~$15-30/month)
Single server + PostgreSQL. Handles up to ~1K concurrent users.
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = var.aws_region }
variable "aws_region" { default = "eu-west-3" }
variable "app_name" { type = string }
variable "db_password" { sensitive = true; type = string }
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "${var.app_name}-vpc" }
}
resource "aws_instance" "app" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
subnet_id = aws_subnet.public_a.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
yum update -y && yum install -y docker
systemctl start docker && systemctl enable docker
EOF
tags = { Name = "${var.app_name}-server" }
}
resource "aws_eip" "app" {
instance = aws_instance.app.id
domain = "vpc"
}
resource "aws_db_instance" "main" {
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.micro"
identifier = "${var.app_name}-db"
username = "dbadmin"
password = var.db_password
allocated_storage = 20
storage_encrypted = true
deletion_protection = true
backup_retention_period = 7
skip_final_snapshot = false
}
output "server_ip" { value = aws_eip.app.public_ip }
output "db_endpoint" { value = aws_db_instance.main.endpoint }
Deploy in ~8 minutes:
terraform init
terraform plan -var="app_name=my-saas" -var="db_password=your-secret"
terraform apply
Template 2: GCP Cloud Run (Serverless — ~$0-5/month)
Zero server management. Perfect for early-stage MVPs.
terraform {
required_providers {
google = { source = "hashicorp/google", version = "~> 5.0" }
}
}
provider "google" {
project = var.project_id
region = "europe-west1"
}
resource "google_project_service" "run" {
service = "run.googleapis.com"
}
resource "google_cloud_run_service" "app" {
name = var.app_name
location = "europe-west1"
template {
spec {
containers {
image = var.docker_image
resources { limits = { cpu = "1000m", memory = "512Mi" } }
}
}
metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = "10"
"autoscaling.knative.dev/minScale" = "0"
}
}
}
traffic {
percent = 100
latest_revision = true
}
}
# Public access
resource "google_cloud_run_service_iam_member" "public" {
service = google_cloud_run_service.app.name
location = "europe-west1"
role = "roles/run.invoker"
member = "allUsers"
}
output "url" { value = google_cloud_run_service.app.status[0].url }
Template 3: Cloudflare + Vercel ($0/month)
terraform {
required_providers {
cloudflare = { source = "cloudflare/cloudflare", version = "~> 4.0" }
vercel = { source = "vercel/vercel", version = "~> 1.0" }
}
}
provider "cloudflare" { api_token = var.cloudflare_api_token }
provider "vercel" { api_token = var.vercel_api_token }
resource "vercel_project" "app" {
name = var.domain_name
framework = "nextjs"
git_repository = { type = "github", repo = var.github_repo }
}
resource "cloudflare_record" "root" {
zone_id = var.cloudflare_zone_id
name = "@"
type = "CNAME"
value = "${vercel_project.app.name}.vercel.app"
proxied = true
}
resource "cloudflare_page_rule" "https" {
zone_id = var.cloudflare_zone_id
target = "http://${var.domain_name}/*"
priority = 1
actions { always_use_https = true }
}
Template 4: Multi-Environment (Staging + Production)
terraform/
├── modules/app/ # Shared module
└── environments/
├── staging/main.tf # t3.micro, 1 instance
└── production/main.tf # t3.small, 2+ instances
# environments/staging/main.tf
module "app" {
source = "../../modules/app"
environment = "staging"
instance_type = "t3.micro"
min_capacity = 1
max_capacity = 2
domain_name = "staging.myapp.com"
}
# environments/production/main.tf
module "app" {
source = "../../modules/app"
environment = "production"
instance_type = "t3.small"
min_capacity = 2
max_capacity = 10
domain_name = "myapp.com"
}
Separate state per environment:
cd environments/staging && terraform apply # test here first
cd environments/production && terraform apply # then promote
Template 5: ECS Fargate Auto-Scaling (~$40-80/month)
Production-grade containers without managing Kubernetes.
resource "aws_ecs_cluster" "main" {
name = "${var.app_name}-cluster"
setting { name = "containerInsights"; value = "enabled" }
}
resource "aws_ecs_task_definition" "app" {
family = var.app_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
container_definitions = jsonencode([{
name = var.app_name
image = "${aws_ecr_repository.app.repository_url}:latest"
portMappings = [{ containerPort = 3000 }]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/${var.app_name}"
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
}])
}
# Auto-scale at 75% CPU
resource "aws_appautoscaling_policy" "cpu" {
name = "${var.app_name}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
target_value = 75.0
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
5 Non-Negotiable Rules
1. Never commit secrets
# .gitignore
*.tfvars
*.tfstate
.terraform/
2. Always plan before apply
terraform plan -out=tfplan && terraform apply tfplan
3. Enable deletion protection
resource "aws_db_instance" "main" {
deletion_protection = true
lifecycle { prevent_destroy = true }
}
4. Remote state with S3
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-3"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
5. CI/CD via GitHub Actions
- name: Terraform Apply
run: terraform apply tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Cost Reference 2026
| Stack | Monthly Cost | Best For |
|---|---|---|
| Cloud Run (GCP) | $0-5 | MVP, < 1K users |
| Vercel + Cloudflare | $0 | Static/Next.js |
| EC2 t3.micro + RDS | $15-30 | Up to 1K users |
| EC2 t3.small + RDS | $45-60 | Up to 5K users |
| ECS Fargate | $40-80 | Auto-scaling prod |
Start with Cloud Run or Vercel. Move to EC2 when you have paying users.
The complete Terraform Starter Kit for Indie Hackers (all 5 templates with full code, CI/CD pipelines, cost optimization guide, and 30-min quickstart) is available at guittet.gumroad.com.
What stack are you running your indie project on? Drop it below 👇
Top comments (0)