๐Ÿš€ Noahops enterprise beta is now open โ€” Get early access โ†’
Back to blog

How to Set Up Isolated VPC Environments on AWS for Staging and Production

How to Set Up Isolated VPC Environments on AWS for Staging and Production

If you're running your startup on Railway or Render and starting to think about moving to AWS, VPC isolation is one of the first things that comes up โ€” and one of the most confusing.

Here's the short version: you want your staging environment and production environment to be completely separate at the network level. Not just different environment variables. Different VPCs. When something goes wrong in staging (and it will), it can't touch production. When someone accidentally runs a migration script, it hits the staging database, not the live one.

This guide walks you through the setup. It assumes you own your AWS account directly โ€” not running on someone else's infrastructure โ€” and that you don't have a dedicated DevOps person. Just you, your team, and an AWS console.


Why VPC Isolation Matters (and Why Railway Doesn't Give You This)

Railway and Render run your services on their infrastructure. You don't own the network. You don't get a VPC. You can't set security group rules at the network level. You can't enforce that your staging database is only reachable from staging services.

When you move to AWS with VPC isolation:

  • Staging and production live in separate network envelopes. Services in staging VPC cannot talk to resources in the production VPC without an explicit, intentional network peering connection (which you won't set up).
  • Database access is network-gated, not just credential-gated. Your production RDS instance isn't reachable from staging โ€” period.
  • Compliance auditors (SOC 2, ISO 27001) can verify network isolation with documentation. "We use environment variables" doesn't pass that test.

The Architecture You're Building

Two VPCs. One for staging, one for production.

Each VPC contains:

  • A public subnet for your load balancer (ALB)
  • A private subnet for your application (ECS/Fargate containers)
  • A private subnet for your database (RDS PostgreSQL)
  • A NAT Gateway so private subnet resources can make outbound requests (package installs, external APIs) without being publicly reachable

The load balancer sits in the public subnet, forwards traffic to containers in the private subnet, which talk to RDS in the private subnet. Nothing in the private subnets is directly accessible from the internet.

Internet
    |
   ALB (public subnet)
    |
  ECS/Fargate (private subnet)
    |
  RDS PostgreSQL (private subnet)

You'll provision this structure twice โ€” once for staging, once for production. The two VPCs are completely separate.


Step 1: Create Your Staging VPC

In the AWS console, go to VPC โ†’ Create VPC.

Settings:

  • Name: your-app-staging
  • IPv4 CIDR block: 10.0.0.0/16
  • Tenancy: Default

Then create subnets inside this VPC. You need at least two availability zones for RDS (it requires Multi-AZ or at least two AZs for subnet groups).

Public subnets:

  • staging-public-1a: 10.0.1.0/24 โ€” AZ: us-east-1a
  • staging-public-1b: 10.0.2.0/24 โ€” AZ: us-east-1b

Private subnets (app tier):

  • staging-private-app-1a: 10.0.11.0/24 โ€” AZ: us-east-1a
  • staging-private-app-1b: 10.0.12.0/24 โ€” AZ: us-east-1b

Private subnets (database tier):

  • staging-private-db-1a: 10.0.21.0/24 โ€” AZ: us-east-1a
  • staging-private-db-1b: 10.0.22.0/24 โ€” AZ: us-east-1b

Step 2: Set Up Internet Gateway and Route Tables

Your public subnets need an Internet Gateway so the ALB can receive traffic.

  1. Go to VPC โ†’ Internet Gateways โ†’ Create. Name it staging-igw. Attach it to your-app-staging VPC.

  2. Go to Route Tables. Find the main route table for your staging VPC. Add a route:

    • Destination: 0.0.0.0/0
    • Target: staging-igw

    Associate this route table with your two public subnets only.

  3. Create a separate route table for private subnets. Don't add the internet gateway route to it. (Private subnets route outbound traffic through the NAT Gateway โ€” next step.)


Step 3: Add a NAT Gateway

Your application containers need to make outbound internet requests (pulling npm packages, calling Stripe, hitting Slack webhooks). But they shouldn't be publicly reachable.

NAT Gateway sits in the public subnet and lets private subnet traffic reach the internet โ€” outbound only.

  1. Go to VPC โ†’ NAT Gateways โ†’ Create.

    • Subnet: staging-public-1a
    • Elastic IP: Allocate a new one
  2. Update the private route table:

    • Destination: 0.0.0.0/0
    • Target: your new NAT Gateway

Associate this route table with all four private subnets (both app tier and database tier).

Cost note: NAT Gateways cost ~$32/month per gateway plus data transfer. For staging, running one NAT Gateway covering both AZs is fine. Production should have one per AZ for high availability.


Step 4: Security Groups

Create three security groups inside your staging VPC.

ALB Security Group (staging-alb-sg):

  • Inbound: HTTP (80) and HTTPS (443) from 0.0.0.0/0
  • Outbound: All traffic (to forward to containers)

App Security Group (staging-app-sg):

  • Inbound: Your app port (e.g., 3000 or 8080) โ€” source: staging-alb-sg only
  • Outbound: All traffic (for database connections, outbound API calls)

Database Security Group (staging-db-sg):

  • Inbound: PostgreSQL port (5432) โ€” source: staging-app-sg only
  • Outbound: None needed

The key rule: the database is only reachable from the app layer. The app is only reachable from the load balancer. Nothing is open to the internet directly.


Step 5: Create Your RDS Subnet Group and Database

Before creating RDS, create a subnet group:

  1. Go to RDS โ†’ Subnet Groups โ†’ Create.

    • Name: staging-db-subnet-group
    • VPC: your-app-staging
    • Subnets: select your two private DB subnets (staging-private-db-1a and staging-private-db-1b)
  2. Create your RDS instance:

    • Engine: PostgreSQL
    • Template: Dev/Test (for staging, this saves cost)
    • DB subnet group: staging-db-subnet-group
    • VPC security group: staging-db-sg
    • Public access: No โ€” this is critical

Your database is now inside a private subnet, reachable only from resources with the staging-app-sg security group, and not accessible from the internet at all.


Step 6: Repeat for Production

Create a second VPC with non-overlapping CIDR ranges:

  • Production VPC CIDR: 10.1.0.0/16
  • Public subnets: 10.1.1.0/24, 10.1.2.0/24
  • Private app subnets: 10.1.11.0/24, 10.1.12.0/24
  • Private DB subnets: 10.1.21.0/24, 10.1.22.0/24

Everything else follows the same pattern. Name everything production-* instead of staging-*.

Non-overlapping CIDRs matter because: if you ever need to set up VPC peering (between your app VPC and a shared services VPC, for example), overlapping ranges prevent it.


Step 7: Inject Credentials via ECS Task Definitions (Not .env Files)

When you deploy on ECS/Fargate, your containers get environment variables from the task definition โ€” not .env files on disk. Store your database credentials in AWS Secrets Manager and reference them in the task definition.

In your ECS task definition:

"secrets": [
  {
    "name": "DATABASE_URL",
    "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:staging/your-app/db-url-ABC123"
  }
]

Your container sees DATABASE_URL as a normal environment variable. The credentials never sit in your repo or in plaintext config files.

Create separate Secrets Manager entries for staging and production. Your ECS task definitions reference the right one per environment.


Step 8: Verify Isolation

After setup, verify these two things:

  1. From a staging ECS container, attempt to connect to the production database endpoint. It should time out. If it doesn't, your security groups need review.

  2. From outside AWS (your laptop), attempt to connect to either database endpoint directly. It should time out. Public access is off; the only path in is through the ALB โ†’ app โ†’ database chain.

If you're using NoahOps, the VPC provisioning, subnet configuration, security group setup, and NAT Gateway creation happen automatically when you create a new environment. The verification steps still apply โ€” you should check the isolation regardless of how the infrastructure was provisioned.


Common Mistakes

Using the same VPC with different subnets. This is not VPC isolation. Services in different subnets of the same VPC can still reach each other. Separate VPCs are required for network-level isolation.

Enabling public access on RDS "just for debugging." Don't. Use an EC2 bastion host or AWS Systems Manager Session Manager to connect to private resources for debugging. The database should never be publicly reachable.

Forgetting to update the route table for private subnets. Private subnets without a NAT Gateway route will fail silently โ€” your containers will time out trying to reach external services and you'll spend time looking in the wrong place.

Using overlapping CIDR blocks. 10.0.0.0/16 for both staging and production will cause headaches if you ever need VPC peering. Use distinct ranges from the start.


What This Looks Like With NoahOps

NoahOps provisions isolated VPC environments per stage out of the box. When you create a production or staging environment in NoahOps, you get:

  • Separate VPC per environment
  • Public/private subnet split with ALB in public, containers in private
  • Security groups wired correctly (ALB โ†’ app โ†’ database)
  • NAT Gateway for outbound connectivity
  • RDS provisioned in private subnets with credentials auto-injected into ECS task definitions via Secrets Manager

The manual steps above are what NoahOps automates. Understanding them matters โ€” you should know what's in your AWS account and why. But you don't need to provision it manually.

Request a free demo at noahops.com to see VPC environment provisioning in action.


FAQ

Do I need a separate AWS account per environment, or separate VPCs within one account? Separate VPCs in one account is sufficient for most startups. Separate accounts add blast radius isolation (a compromised account can't affect the other) but add significant operational overhead. Start with separate VPCs; move to separate accounts when compliance requirements or team size demand it.

How much does this cost to run? For staging: ~$32โ€“40/month for the NAT Gateway, plus your RDS instance (db.t3.medium runs ~$50/month) and ECS task costs. For production, add a second NAT Gateway (~$32/month) for high availability. Total infrastructure for both environments: $150โ€“300/month depending on instance sizes and traffic.

Can I share one NAT Gateway between staging and production? Technically yes, but you'd need to set up VPC peering to route traffic through it โ€” which partially defeats the isolation. Separate NAT Gateways per VPC keep the architecture clean.

What's the difference between a security group and a network ACL? Security groups are stateful (return traffic is automatically allowed) and applied per resource. Network ACLs are stateless and applied per subnet. For startup-scale setups, security groups alone provide sufficient network control. NACLs add an extra layer but also add complexity.

How do I connect to my private RDS instance for debugging? Use AWS Systems Manager Session Manager with an EC2 instance in your private subnet as a jump host โ€” without opening SSH ports to the internet. NoahOps includes SSH access to instances for exactly this purpose.

Ready to ship to AWS?

Try NoahOps free โ€” VPC, ECS, RDS, CI/CD in your own AWS account in 15 minutes.