Deploying a full-stack web application into production is often one of the most daunting steps for modern developers. You build a complete application locally, but when it comes to hosting it securely on cloud infrastructure with HTTPS, custom domains, and automated deployment pipelines, things can quickly get overwhelming.
In this post, I will walk you through how I built and deployed an end-to-end full-stack MERN application ("Little List")βfrom the frontend user interface and Express REST API to provisioning production AWS cloud infrastructure with Terraform and automating deployments with Keyless GitHub Actions OIDC.
Whether you are a beginner looking to understand full-stack architecture & cloud deployment or an experienced engineer reviewing multi-environment Terraform design, this guide covers the entire end-to-end workflow!
π± What is "Little List"? (Application Overview)
"Little List" is a full-stack personal productivity and management application built from scratch:
- React SPA Frontend (Vite): Modern, responsive UI with state management, JWT auth flows, interactive diary logging, and custom URL shortener interfaces.
- Node.js & Express API Backend: Modular REST API with secure authentication, refresh tokens, MongoDB integration, and route validation.
- MongoDB Database: Persistent storage for users, diary entries, and shortened link analytics.
π Prerequisites
Before diving in, here is what you will need if you want to replicate this deployment architecture:
- AWS Account: An active AWS account with permissions to manage EC2, S3, CloudFront, ECR, IAM, and Route 53.
-
Terraform CLI: Installed on your machine (
>= 1.16.2). -
AWS CLI: Installed and configured with your credentials (
aws configure). - Docker: Installed locally for testing container builds.
- Node.js & npm: Installed locally for building the frontend.
- GitHub Account: A repository containing your MERN codebase.
-
Custom Domain (Optional): A domain registered on Namecheap, GoDaddy, or Route 53 if you want custom SSL support (e.g.,
ideategudy.tech).
ποΈ The Cloud Architecture Overview
Before writing code, let us look at the high-level architecture diagram of what we are provisioning on AWS:
ββββββββββββββββββββββββ
β GitHub Actions β
ββββββββββββ¬ββββββββββββ
β
βββββββββββββββββββ΄ββββββββββββββββββ
β β
Frontend deploy Backend deploy
β β
βΌ βΌ
βββββββββββββββββ βββββββββββββββββ
β S3 β β ECR β
β React build β β Docker image β
βββββββββ¬ββββββββ βββββββββ¬ββββββββ
β β
β β docker pull
β βΌ
β βββββββββββββββββ
β β EC2 β
β β Express/Dockerβ
β βββββββββ¬ββββββββ
β β
β βββββββββ΄ββββββββ
β β ASG β
β β EC2 fleet β
β βββββββββ¬ββββββββ
β β
βΌ βββββββββΌββββββββ
βββββββββββββββββ β ALB β
β Route 53 DNS β β Load Balancer β
βββββββββ¬ββββββββ βββββββββ²ββββββββ
β β
βΌ β
βββββββββββββββββ β
β CloudFront ββββββββββββββββββββββββββββββ
β CDN (HTTPS) β Path-based routing (/api/*)
βββββββββββββββββ
Core Components Explained for Beginners:
-
AWS Route 53: Highly available DNS service that manages custom domain records (
dev.ideategudy.tech) and routes client requests directly to CloudFront via A-record Aliases. -
Amazon S3: Hosts the static, compiled single-page React frontend (
dist/build files) privately. - Amazon CloudFront: A global Content Delivery Network (CDN) that serves the React app over HTTPS and acts as a single reverse proxy for both client and backend requests.
- Amazon ECR (Elastic Container Registry): Private Docker image registry to store backend container builds.
- AWS ALB (Application Load Balancer): Receives API traffic from CloudFront and balances requests across EC2 instances.
- AWS Auto Scaling Group (ASG): Manages an auto-healing fleet of EC2 instances running Canonical Ubuntu 24.04 LTS and Docker.
- GitHub OIDC (OpenID Connect): Allows GitHub Actions to obtain short-lived security tokens to deploy to AWS without storing permanent AWS Secret Access Keys in GitHub.
π Repository & Project Structure
Here is how the project files are structured:
.
βββ client/ # Vite + React SPA Frontend
βββ server/ # Node.js + Express API Backend (Dockerized)
βββ infra/ # Modular Terraform Infrastructure
β βββ main.tf # Root Terraform Module
β βββ variables.tf # Root Variables & Configuration Defaults
β βββ outputs.tf # Root Terraform Outputs
β βββ versions.tf # AWS & Provider Requirements
β βββ dev.tfvars.example # Development Environment Settings
β βββ prod.tfvars.example # Production Environment Settings
β βββ modules/
β βββ vpc/ # VPC, Public Subnets (2 AZs), IGW, Route Tables
β βββ backend/ # ECR, ALB, ASG, Launch Template, EC2 IAM Role
β βββ frontend/ # S3 Bucket, CloudFront OAC, CDN Distribution, Route53/ACM
β βββ github_oidc/ # GitHub OIDC Identity Provider & IAM Role
βββ .github/
βββ workflows/
βββ deploy.yml # Parallel CI/CD Workflow (Frontend & Backend)
π§± Key Technical Deep Dives
1. Unified Single-Domain Routing with CloudFront
One common challenge with SPA + API setups is CORS and handling multiple URLs (api.domain.com vs domain.com). We solved this by serving both frontend and backend under the exact same domain (https://dev.ideategudy.tech) via CloudFront Path-Based Routing:
| Request Path Pattern | Origin Target | Description |
|---|---|---|
/ or /assets/*
|
s3-frontend |
Serves static React SPA files |
/api/*, /s/*, /healthz
|
alb-api |
Forwards API requests directly to the EC2/Express backend |
π‘ Pro-Tip: When forwarding requests through CloudFront to an ALB, ensure your CloudFront cache behavior explicitly forwards the
Authorization,Accept,Content-Type,Origin, andRefererheaders! Otherwise, CloudFront strips theAuthorization: Bearer <jwt>header by default, leading to401 Unauthorizederrors on authenticated routes.
2. Zero Secrets in GitHub via AWS OIDC
Instead of creating long-lived IAM user keys (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) and pasting them into GitHub Secrets, we configured an AWS IAM OIDC Identity Provider.
GitHub Actions assumes a temporal AWS IAM Role dynamically per pipeline run using OpenID Connect authentication. If your pipeline is compromised, there are no static credentials to leak!
π Custom Domain, Subdomain Setup (dev.ideategudy.tech) & SSL Configuration
Primary infrastructure resources (VPC, EC2, ASG, ALB, ECR, S3) are deployed in eu-north-1 (Stockholm). HTTPS is terminated at the CloudFront CDN level using AWS Certificate Manager (ACM) in us-east-1 (required by CloudFront for global edge distributions).
Why Use a Subdomain (dev.ideategudy.tech)?
Instead of pointing the root/apex domain (ideategudy.tech) directly to this environment, we configured a dedicated subdomain (dev.ideategudy.tech) for several key reasons:
-
Environment Isolation (
devvsprod): Using subdomains allows us to run isolated development (dev.ideategudy.tech) and production (ideategudy.techorapp.ideategudy.tech) infrastructure in separate Terraform workspaces without DNS conflicts. -
Subdomain NS Delegation in Namecheap: By adding custom
NSrecords fordev.ideategudy.techin Namecheap, we delegate only thedevsubdomain DNS resolution to AWS Route 53 while keeping the parent apex domain (ideategudy.tech) managed at Namecheap. - Multi-Environment Testing: It enables continuous testing of AWS infrastructure changes (CloudFront, ALB, ASG) on a real HTTPS domain without risking downtime for main production domain traffic.
Connecting Namecheap Subdomain (dev.ideategudy.tech) to AWS:
- In
infra/dev.tfvars:
domain_name = "dev.ideategudy.tech"
create_route53_zone = true
- Run
terraform apply --var-file="dev.tfvars". - Copy the 4 output nameservers from Terraform:
route53_nameservers = [
"ns-1143.awsdns-14.org",
"ns-1698.awsdns-20.co.uk",
"ns-263.awsdns-32.com",
"ns-847.awsdns-41.net"
]
- Log into Namecheap -> Domain List -> Manage
ideategudy.tech-> Advanced DNS -> Add 4NSRecords for Hostdevpointing to the Route 53 Nameservers.
Figure: Configured NS records in Namecheap delegating
dev.ideategudy.techto AWS Route 53.
- In minutes, free auto-renewing SSL is active across
https://dev.ideategudy.tech!
β‘ Automated CI/CD Pipeline (deploy.yml)
When code is pushed to main, GitHub Actions triggers .github/workflows/deploy.yml which executes two parallel jobs:
π‘ Note for Beginners: You do not need to manually build the React frontend or Docker container on your local computer before pushing. GitHub Actions automatically compiles the React production bundle and builds/pushes the Docker image in the cloud during the pipeline run!
-
deploy-frontend:- Builds Vite/React bundle.
- Authenticates keylessly to AWS via OIDC.
- Syncs static assets to the private S3 bucket (
aws s3 sync). - Invalidates CloudFront edge cache (
aws cloudfront create-invalidation).
-
deploy-backend:- Builds Docker image for Express API.
- Authenticates to Amazon ECR via OIDC.
- Pushes new Docker tag to ECR.
- Syncs runtime application secrets into AWS SSM Parameter Store (
/mern-deploy/dev/...). - Executes zero-downtime container updates across EC2 ASG instances via AWS SSM
RunShellScript.
π Step-by-Step Deployment Instructions
Step 1: Provision AWS Infrastructure with Terraform
cd infra
# Copy example variables
cp dev.tfvars.example dev.tfvars
# Edit values in dev.tfvars (domain_name, github_org, github_repo)
terraform init
terraform plan --var-file="dev.tfvars"
terraform apply --var-file="dev.tfvars" --auto-approve
Figure 1: Successful
terraform applyshowing outputs for S3, CloudFront, ECR, and ALB.
Step 2: Configure GitHub Repository Secrets & Variables
In your GitHub repo under Settings -> Secrets and variables -> Actions:
π‘ Tip: You can retrieve all your Terraform output values anytime by running
terraform outputinside theinfra/folder.
Figure 2: GitHub Actions repository variables and secrets configured for OIDC deployment.
-
Secrets:
-
MONGODB_URI: MongoDB connection string -
ACCESS_TOKEN_SECRET: JWT access secret -
REFRESH_TOKEN_SECRET: JWT refresh secret
-
-
Variables:
-
AWS_ROLE_ARN: Outputgithub_actions_role_arn -
AWS_REGION:eu-north-1 -
ECR_REPOSITORY: Outputecr_repository_name -
FRONTEND_BUCKET: Outputfrontend_bucket -
CLOUDFRONT_DISTRIBUTION_ID: Outputcloudfront_distribution_id -
API_ASG_NAME: Outputapi_asg_name -
SSM_PREFIX: Outputssm_prefix -
CLIENT_URL:https://dev.ideategudy.tech -
SERVER_URL:https://dev.ideategudy.tech
-
Step 3: Trigger Automated Build & Deployment
Push your updates to the repository:
git add .
git commit -m "Deploy production architecture to AWS"
git push origin main
Figure 3: Parallel CI/CD execution for
deploy-frontendanddeploy-backendjobs.
π Live Application Demo
Figure 4: The live full-stack application running on
https://dev.ideategudy.techserved securely over CloudFront HTTPS.
β‘ Key Benefits of This Architecture
Choosing this cloud setup over monolithic single-server hosting provides several high-value production advantages:
π Zero-Downtime & Ultra-Fast Global Delivery:
By hosting the React SPA on Amazon S3 + CloudFront, static assets are cached globally at edge locations close to users. CloudFront delivers lightning-fast page loads while shielding your backend EC2 instances from static file requests.-
π° Extreme Cost Optimization (Free Tier Friendly):
- S3 & CloudFront: Nearly zero cost for static SPA bandwidth.
- EC2 Auto Scaling: Scales t3.micro instances based on CPU utilization and traffic demands without paying for oversized idle instances.
- Amazon ECR Lifecycle Policies: Configured to auto-expire old image tags, keeping container storage well under free-tier limits.
π Enterprise-Grade Keyless Security (OIDC):
By avoiding permanentAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYcredentials in GitHub Secrets, your deployment security posture is greatly hardened. GitHub Actions requests temporary, short-lived IAM credentials via OIDC scoped strictly to the resources it needs.π‘οΈ High Availability & Auto-Healing:
The Application Load Balancer (ALB) and Auto Scaling Group (ASG) automatically monitor EC2 instance health. If an instance crashes or fails health checks, ASG automatically terminates it and launches a fresh, containerized replacement instance seamlessly.π¦ Modular & Multi-Environment Ready:
The modular Terraform layout (vpc,backend,frontend,github_oidc) makes it effortless to spin up separatedev,staging, andproductionenvironments in minutes with zero manual AWS Console clicking.
π― Conclusion
Building cloud infrastructure using modular Terraform modules and automated keyless CI/CD pipelines transforms complex AWS operations into predictable, repeatable, and maintainable software engineering workflows.
By decoupling the architecture into public VPC subnets, S3/CloudFront SPA hosting, and auto-scaling EC2 container fleets, the application stays fast, cost-optimized, and resilient.
π Related Documentation & Alternative Architecture
Looking for a simpler, single-server EC2 deployment pattern? Check out the companion article:
This guide covers deploying the full-stack MERN application on a single AWS EC2 instance using Docker Compose, Nginx reverse proxy, Certbot (Let's Encrypt SSL), and SSH-based GitHub Actions CI/CD.
π€ Connect & Follow Me
If you found this guide helpful or have any questions about AWS, Terraform, Docker, or MERN stack architecture, let us connect!
- π GitHub: github.com/ideateGudy
- πΌ LinkedIn: linkedin.com/in/ideategudy
Drop a reaction on Dev.to and feel free to star the GitHub repository! Happy coding! π





Top comments (0)