Deploy Next.js on AWS with GitHub Actions

▲ Next.js ☁️ Amazon Web Services ⚙️ GitHub Actions

Configuration Files

4 files
Production-ready configuration files with detailed comments and best practices. Each file works together as a complete deployment solution.
Next.js configuration for static export to AWS
javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  images: {
    unoptimized: true,
  },
  trailingSlash: true,
  // Optional: Add CDN domain for optimized images
  // assetPrefix: process.env.NODE_ENV === 'production' ? 'https://d1234567890.cloudfront.net' : '',
}

module.exports = nextConfig

Pro Tips

  • ⚡ output: "export" generates static HTML (no server needed)
  • 🖼️ unoptimized: true required for static export
  • 🔗 trailingSlash: true ensures consistent URLs
  • 💰 Static export = no Lambda costs (~$0 vs $5-50/month)
  • ⚠️ No API routes or server-side features with static export
  • 💡 For SSR, use AWS Amplify or Lambda@Edge instead
Complete AWS infrastructure for Next.js hosting
terraform
# Terraform for Next.js on AWS S3 + CloudFront
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  default = "us-east-1"
}

variable "app_name" {
  default = "my-nextjs-app"
}

# S3 Bucket
resource "aws_s3_bucket" "website" {
  bucket = "${var.app_name}-website"
}

resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "404.html"
  }
}

resource "aws_s3_bucket_public_access_block" "website" {
  bucket = aws_s3_bucket.website.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.website.arn}/*"
      }
    ]
  })

  depends_on = [aws_s3_bucket_public_access_block.website]
}

# CloudFront Distribution
resource "aws_cloudfront_distribution" "website" {
  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"

  origin {
    domain_name = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id   = "S3-${var.app_name}"
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-${var.app_name}"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
    compress               = true
  }

  # Next.js pages routing
  custom_error_response {
    error_code         = 404
    response_code      = 404
    response_page_path = "/404.html"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

# Outputs
output "s3_bucket_name" {
  value = aws_s3_bucket.website.id
}

output "cloudfront_distribution_id" {
  value = aws_cloudfront_distribution.website.id
}

output "website_url" {
  value = "https://${aws_cloudfront_distribution.website.domain_name}"
}

Pro Tips

  • 💰 Costs ~$1-5/month for small sites (S3 + CloudFront)
  • 🌍 Global CDN with 450+ edge locations
  • ⚡ CloudFront caching = 90%+ cost reduction
  • 🔒 HTTPS included with CloudFront certificate
  • 💡 Add custom domain with ACM certificate
  • 📊 PriceClass_100 = US, Canada, Europe only
  • 🎯 Run "terraform apply" to create infrastructure
  • ⚠️ Static export only - no SSR or API routes
Next.js dependencies and build scripts
json
{
  "name": "my-nextjs-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "deploy": "npm run build && aws s3 sync out/ s3://my-nextjs-app-website --delete"
  },
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "autoprefixer": "^10.4.16",
    "eslint": "^8.54.0",
    "eslint-config-next": "^14.0.0",
    "postcss": "^8.4.32",
    "tailwindcss": "^3.3.6",
    "typescript": "^5.3.0"
  }
}

Pro Tips

  • 📦 Next.js 14+ with App Router
  • 🚀 "deploy" script for manual deployments
  • ⚡ Tailwind CSS included for styling
  • 📝 TypeScript support built-in
  • 💡 Update S3 bucket name in deploy script
  • 🎯 Use GitHub Actions for automated deployments
Environment variables for AWS deployment
bash
# AWS Configuration
AWS_REGION=us-east-1
S3_BUCKET=my-nextjs-app-website
CLOUDFRONT_DISTRIBUTION_ID=E1234567890ABC

# Public variables (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_SITE_URL=https://d1234567890.cloudfront.net
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

# Build-time variables
NEXT_PUBLIC_BUILD_TIME=${new Date().toISOString()}

Pro Tips

  • 🔒 NEXT_PUBLIC_ prefix makes variables available in browser
  • ⚠️ Static export = all variables are PUBLIC
  • 💡 Copy to .env.local for local development
  • 🚫 Never commit .env.local to git
  • 🎯 Use AWS Secrets Manager for sensitive data
  • 📝 GitHub Actions uses repository secrets

Prerequisites

  • AWS account with billing enabled
  • AWS CLI installed and configured
  • Terraform installed (optional, for infrastructure)
  • GitHub repository with admin access
  • Node.js 18+ installed

Deployment Steps

  • Create Next.js app: npx create-next-app@latest
  • Update next.config.js for static export
  • Run terraform apply to create S3 + CloudFront
  • Get S3 bucket name and CloudFront ID from outputs
  • Add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to GitHub Secrets
  • Add S3_BUCKET and CLOUDFRONT_DISTRIBUTION_ID to GitHub Secrets
  • Create .github/workflows/deploy.yml
  • Push to main branch to deploy
  • Access site at CloudFront URL

Additional Notes

  • 💰 Costs ~$1-5/month for small sites
  • 🌍 Global CDN with 450+ edge locations
  • 🔒 Automatic HTTPS with CloudFront
  • ⚡ Static export = no server costs
  • ⚠️ No SSR or API routes with static export
  • 💡 For SSR, use AWS Amplify or Vercel instead
  • 📊 CloudFront caching reduces costs by 90%+
  • 🎯 Perfect for blogs, marketing sites, documentation