nodejs3 Min Read

Deploying Node.js to AWS

Gorav Singal

April 02, 2026

TL;DR

Use ECS Fargate for containerized APIs (auto-scaling, no server management), Lambda for event-driven functions, and Elastic Beanstalk for quick deployments. Set up CI/CD with GitHub Actions and monitor with CloudWatch.

Deploying Node.js to AWS

Choosing Your Deployment Strategy

Service Best For Scaling Cold Start
ECS Fargate Containerized APIs, microservices Auto-scaling tasks None
Lambda Event-driven, short-lived functions Automatic, per-request ~200-500ms
Elastic Beanstalk Quick deploys, small teams Auto-scaling EC2 None

ECS Fargate — Containerized APIs

Fargate runs your Docker containers without managing servers.

ECS Fargate Architecture

Task Definition

{
  "family": "api-service",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::role/ecsTaskRole",
  "containerDefinitions": [
    {
      "name": "api",
      "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:latest",
      "portMappings": [
        { "containerPort": 3000, "protocol": "tcp" }
      ],
      "environment": [
        { "name": "NODE_ENV", "value": "production" },
        { "name": "PORT", "value": "3000" }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:ssm:us-east-1::parameter/prod/database-url"
        }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "wget -q --spider http://localhost:3000/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 60
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/api-service",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "api"
        }
      }
    }
  ]
}

Auto-Scaling

# Target tracking: maintain 70% CPU utilization
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/my-cluster/api-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 10

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-cluster/api-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu-tracking \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 70.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
  }'

AWS Lambda with Node.js

Lambda Handler

// handler.js
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');

const dynamodb = new DynamoDBClient({});

// Reuse connections outside handler (warm start optimization)
exports.handler = async (event) => {
  try {
    const { pathParameters, httpMethod, body } = event;

    switch (httpMethod) {
      case 'GET': {
        const result = await dynamodb.send(new GetItemCommand({
          TableName: 'Users',
          Key: { id: { S: pathParameters.id } },
        }));

        if (!result.Item) {
          return { statusCode: 404, body: JSON.stringify({ error: 'Not found' }) };
        }

        return {
          statusCode: 200,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(result.Item),
        };
      }

      default:
        return { statusCode: 405, body: 'Method not allowed' };
    }
  } catch (err) {
    console.error('Error:', err);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' }),
    };
  }
};

Reducing Cold Starts

// 1. Keep functions warm with provisioned concurrency
// 2. Minimize package size — use esbuild/webpack to bundle
// 3. Initialize SDK clients OUTSIDE the handler
// 4. Use ARM64 (Graviton2) — 20% cheaper, often faster

// serverless.yml
// functions:
//   api:
//     handler: dist/handler.handler
//     runtime: nodejs20.x
//     architecture: arm64
//     memorySize: 256
//     timeout: 10
//     provisionedConcurrency: 2

CI/CD with GitHub Actions

CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy to ECS

on:
  push:
    branches: [main]

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: my-api
  ECS_CLUSTER: production
  ECS_SERVICE: api-service
  TASK_DEFINITION: api-service

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install and test
        run: |
          npm ci
          npm run lint
          npm test

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::role/GitHubActionsRole
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push Docker image
        env:
          ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Update ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: api
          image: ${{ steps.ecr-login.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}

      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

Environment Management

# Store secrets in AWS SSM Parameter Store
aws ssm put-parameter \
  --name "/prod/database-url" \
  --type "SecureString" \
  --value "postgres://user:pass@rds-host:5432/myapp"

aws ssm put-parameter \
  --name "/prod/jwt-secret" \
  --type "SecureString" \
  --value "your-jwt-secret-here"

CloudWatch Monitoring

// Custom CloudWatch metrics
const { CloudWatchClient, PutMetricDataCommand } = require('@aws-sdk/client-cloudwatch');
const cw = new CloudWatchClient({});

async function publishMetric(name, value, unit = 'Count') {
  await cw.send(new PutMetricDataCommand({
    Namespace: 'MyApp/API',
    MetricData: [{
      MetricName: name,
      Value: value,
      Unit: unit,
      Timestamp: new Date(),
    }],
  }));
}

// Track response times
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    publishMetric('ResponseTime', Date.now() - start, 'Milliseconds');
    if (res.statusCode >= 500) {
      publishMetric('5xxErrors', 1);
    }
  });
  next();
});

Cost Optimization

  1. Use Fargate Spot for non-critical workloads (up to 70% cheaper)
  2. Right-size tasks — start with 0.25 vCPU / 512MB and scale up based on metrics
  3. Use ARM64 (Graviton) instances — 20% cheaper than x86
  4. Set up auto-scaling to scale down during off-peak hours
  5. Use CloudWatch Logs Insights instead of a dedicated logging service

ECS Fargate provides the best balance of simplicity and control for production Node.js APIs. Lambda shines for event-driven workloads with sporadic traffic.

Share

Related Posts

Docker and Containerization for Node.js

Docker and Containerization for Node.js

Why Docker for Node.js Docker eliminates “works on my machine” problems by…

File Uploads and S3 Integration in Node.js

File Uploads and S3 Integration in Node.js

File Upload Architecture There are two main patterns for handling file uploads…

WebSockets with Socket.io in Node.js

WebSockets with Socket.io in Node.js

WebSocket vs HTTP Traditional HTTP follows a request/response model — the client…

Testing Node.js — Unit, Integration, and E2E

Testing Node.js — Unit, Integration, and E2E

Testing Strategy A solid testing strategy follows the testing pyramid — many…

Redis — Caching, Sessions, Pub/Sub in Node.js

Redis — Caching, Sessions, Pub/Sub in Node.js

Why Redis for Node.js Redis is an in-memory data store that serves as a cache…

Database Integration — PostgreSQL with Node.js

Database Integration — PostgreSQL with Node.js

Choosing Your PostgreSQL Client Node.js has three main approaches to working…

Latest Posts

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI Video Generation in 2025 — Models, Costs, and How to Build a Cost-Effective Pipeline

AI video generation went from “cool demo” to “usable in production” in 2024-202…

AI Models in 2025 — Cost, Capabilities, and Which One to Use

AI Models in 2025 — Cost, Capabilities, and Which One to Use

Choosing the right AI model is one of the most impactful decisions you’ll make…

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

AI Image Generation in 2025 — Models, Costs, and How to Optimize Spend

Generating one image with AI costs between $0.002 and $0.12. That might sound…

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

AI Coding Assistants in 2025 — Every Tool Compared, and Which One to Actually Use

Two years ago, AI coding meant one thing: GitHub Copilot autocompleting your…

AI Agents Demystified — It's Just Automation With a Better Brain

AI Agents Demystified — It's Just Automation With a Better Brain

Let’s cut through the noise. If you read Twitter or LinkedIn, you’d think “AI…

Supply Chain Security — Protecting Your Software Pipeline

Supply Chain Security — Protecting Your Software Pipeline

In 2024, a single malicious contributor nearly compromised every Linux system on…