~/nodejs/deploying-nodejs-aws.md

Deploying Node.js to AWS

3 min readApril 02, 2026by Gorav Singal#nodejs#aws#deployment
// 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.

$ subscribe --weekly

One deep dive a week, in your inbox

System design, AI engineering, trading systems and interview prep. Tested code, real numbers. Free, and you can unsubscribe any time.

or read it on gyanbyte.substack.com

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

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

In the previous article I argued that AI is not killing software engineering…

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

A few months back I approved a PR that was almost entirely AI-generated. It…

How to Debug AI-Generated Code: Understand First, Fix Last

How to Debug AI-Generated Code: Understand First, Fix Last

In the last article I wrote about reviewing AI code before it goes in. This one…

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

Every few weeks somebody sends me a scary headline — “AI will replace all…

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a…