Migrate a Web App to Amazon ECS

Containerize a Node.js app and deploy it as a Fargate service on Amazon ECS.

Introduction

30 Second Summary

Every application needs to run somewhere. In production, teams package their apps into containers and deploy them to managed cloud services so they can scale without babysitting servers.

In this project, you will containerize a Node.js web application with Docker and deploy it to Amazon ECS using Fargate. By the end, you will have a live container running in AWS that you can access from your browser.

What You'll Build

A simple Node.js Express web application containerized with Docker, pushed to Amazon Elastic Container Registry (ECR), and deployed as a Fargate service on Amazon ECS.

By the end of this project, you'll have:

  • A containerized Node.js web app image stored in Amazon ECR.
  • A running ECS Fargate service accessible via a public IP address in your browser.
  • Hands-on experience with the full ECS deployment pipeline from registry to cluster to service.
  • Secret Mission: Add an Application Load Balancer for production-style traffic routing and health checks.

Are there any prerequisites?

This project assumes you are familiar with Docker and AWS. You will need an AWS account with admin access and the AWS CLI configured on your machine.

Verify Your Environment

Before you build and deploy a container to AWS, you need to confirm that your local tools can communicate with each other and with the cloud.

In this step, you will verify that Docker is running, that your AWS CLI is configured correctly, and that your project workspace is ready in VS Code.

In this step, get ready to:
  • Verify Docker Desktop is running.
  • Confirm your AWS CLI configuration and note your account ID.
  • Open your project folder in VS Code and verify Node.js.
Verify Docker Desktop is running

Docker needs to be actively running for you to build container images later in this project. The docker info command confirms the Docker daemon is online and ready to accept commands.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Confirm Docker is running by running this command:
docker info

✔️ I see Docker Engine info

You should see output that includes Server Version, Storage Driver, and other details about your Docker installation.

Docker is ready. Move on to the next substep.

ⓧ Cannot connect to the Docker daemon

This means Docker Desktop is not running yet.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Docker Desktop and press Enter to launch it.
  • Wait about 30 seconds for the Docker engine to start. You will see the whale icon in your system tray turn solid.
  • Run docker info again to confirm it works.

ⓧ Command not found

This means Docker Desktop is not installed on your machine.

  • Download Docker Desktop from docker.com/products/docker-desktop.
  • Run the installer and follow the prompts.
  • Launch Docker Desktop after installation completes.
  • Run docker info again to confirm it works.
Confirm your AWS CLI configuration

The AWS CLI is how you will create cloud resources throughout this project. This command checks that your CLI can authenticate with your AWS account.

  • Verify your AWS identity by running this command:
aws sts get-caller-identity

✔️ I see my account details

You should see JSON output containing three fields: Account, Arn, and UserId.

Your AWS CLI is properly configured. Keep this terminal open.

ⓧ Unable to locate credentials

This means your AWS CLI has not been configured with access keys.

  • Run the configure command:
aws configure
  • Enter your AWS Access Key ID when prompted.
  • Enter your AWS Secret Access Key when prompted.
  • Enter your preferred region (e.g. us-east-1).
  • Press Enter to skip the default output format.
  • Run aws sts get-caller-identity again to confirm it works.

Note your AWS account ID

The 12-digit number in the Account field is your AWS account ID. You will use this value several times later when building image URLs and task definitions. Keep it somewhere you can easily copy it.

Open your project folder and verify Node.js

Your ecs-migration folder is where you will write the application code, Dockerfile, and task definition. Opening it in VS Code gives you an integrated terminal for running commands directly from your workspace.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the ecs-migration folder, then click Open.

Now open the integrated terminal to verify Node.js.

  • Press Ctrl+` (backtick) to open the VS Code integrated terminal.
  • Verify your Node.js version by running this command:
node --version
  • Verify npm is available by running this command:
npm --version

✔️ I see version numbers for both

You should see a version number for each command (e.g. v24.x.x for Node.js and 10.x.x for npm). Your exact version numbers may differ.

Node.js is ready. Your workspace is fully set up.

ⓧ Command not found

This means Node.js is not installed on your machine.

  • Download Node.js from nodejs.org. Choose the LTS version.
  • Run the installer and follow the prompts.
  • Close and reopen your VS Code terminal after installation.
  • Run node --version and npm --version again to confirm.

Your environment is confirmed and your workspace is open. Next up, you will build the web application and package it into a Docker container.

Build the Web Application and Dockerfile

Your environment is verified and your project folder is ready.

Now you need the actual application code and a way to package it into a container. In this step, you will create a Node.js web server, write a Dockerfile to containerize it, and confirm everything works locally before pushing to AWS.

In this step, get ready to:
  • Create an Express web server that returns an HTML status page.
  • Write a Dockerfile that packages the application into a container image.
  • Build and test the Docker image locally.
Create the Express web server

Every Node.js project starts with a package.json file that defines the project name, version, and dependencies.

  • In VS Code, create a new file called package.json inside your ecs-migration folder.
  • Paste the following into the file:
{
  "name": "ecs-migration-app",
  "version": "1.0.0",
  "description": "A simple web app for ECS migration",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.21.0"
  }
}

What does this file do?

The dependencies section tells npm which packages to install. Here you are declaring Express version 4.21.0 as the web framework for your server.

The scripts.start field defines the command that runs your app. Docker will use this pattern when starting the container.

  • Save package.json.

Now create the application itself. This is a simple Express server that returns an HTML page displaying deployment information.

  • Create a new file called server.js inside your ecs-migration folder.
  • Paste the following into the file:
const express = require('express');
const app = express();
const PORT = 3000;

// Main route - returns an HTML page with deployment info
app.get('/', (req, res) => {
  res.send(`
    <html>
      <head>
        <title>ECS Migration App</title>
        <style>
          body { font-family: Arial, sans-serif; margin: 0; padding: 40px; background: #1a1a2e; color: #eee; text-align: center; }
          .container { max-width: 600px; margin: 80px auto; padding: 40px; background: #16213e; border-radius: 12px; }
          h1 { color: #0f3460; background: #e94560; padding: 12px; border-radius: 8px; display: inline-block; }
          p { font-size: 1.2em; line-height: 1.6; }
          .info { background: #0f3460; padding: 16px; border-radius: 8px; margin-top: 20px; font-size: 0.9em; }
        </style>
      </head>
      <body>
        <div class="container">
          <h1>ECS Migration Success!</h1>
          <p>This application is running on <strong>Amazon ECS with Fargate</strong>.</p>
          <div class="info">
            <p>Container Host: ${require('os').hostname()}</p>
            <p>Node.js Version: ${process.version}</p>
            <p>Time: ${new Date().toISOString()}</p>
          </div>
        </div>
      </body>
    </html>
  `);
});

// Health check endpoint for load balancers and monitoring
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

// Start the server on all network interfaces
app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});

What does this code do?

The / route returns a styled HTML page that displays the container hostname, Node.js version, and current time. Once deployed to ECS, this page proves your container is running in the cloud.

The /health endpoint returns a simple JSON response. Load balancers use this to check whether your container is healthy.

Binding to 0.0.0.0 ensures the server accepts connections from outside the container, not just localhost.

  • Save server.js.

With both files saved, install the Express dependency so you can test locally.

  • Install the project dependencies by running this command in your VS Code terminal:
npm install
  • You should see output listing added packages. A node_modules folder now appears in your project.

Seeing 'npm: command not found'?

Make sure Node.js is installed. Run node --version to check. If you do not see a version number, revisit Step 1 to install Node.js.

Also confirm your terminal is open inside the ecs-migration folder. Run pwd (macOS/Linux) or cd (Windows) to verify.

Help me fix npm install issues in my ECS migration project.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is package.json:

{
  "name": "ecs-migration-app",
  "version": "1.0.0",
  "description": "A simple web app for ECS migration",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.21.0"
  }
}

Here is server.js:

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send(`
    <html>
      <head>
        <title>ECS Migration App</title>
        <style>
          body { font-family: Arial, sans-serif; margin: 0; padding: 40px; background: #1a1a2e; color: #eee; text-align: center; }
          .container { max-width: 600px; margin: 80px auto; padding: 40px; background: #16213e; border-radius: 12px; }
          h1 { color: #0f3460; background: #e94560; padding: 12px; border-radius: 8px; display: inline-block; }
          p { font-size: 1.2em; line-height: 1.6; }
          .info { background: #0f3460; padding: 16px; border-radius: 8px; margin-top: 20px; font-size: 0.9em; }
        </style>
      </head>
      <body>
        <div class="container">
          <h1>ECS Migration Success!</h1>
          <p>This application is running on <strong>Amazon ECS with Fargate</strong>.</p>
          <div class="info">
            <p>Container Host: ${require('os').hostname()}</p>
            <p>Node.js Version: ${process.version}</p>
            <p>Time: ${new Date().toISOString()}</p>
          </div>
        </div>
      </body>
    </html>
  `);
});

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});
Write the Dockerfile

A Dockerfile is a set of instructions that tells Docker how to build your application into a container image. Each line creates a layer, and Docker caches layers to speed up rebuilds.

  • Create a new file called Dockerfile (no file extension) inside your ecs-migration folder.
  • Paste the following into the file:
# Use lightweight Node.js Alpine image
FROM node:24-alpine

# Set working directory inside the container
WORKDIR /app

# Copy package.json first and install dependencies (leverages Docker layer caching)
COPY package.json ./
RUN npm install --production

# Copy application code
COPY server.js ./

# Document the port the app listens on
EXPOSE 3000

# Start the application
CMD ["node", "server.js"]

What does this Dockerfile do?

  • FROM node:24-alpine starts from a minimal Node.js 24 image. Alpine keeps the image small (about 50 MB vs 900 MB for the full image).
  • COPY package.json is separated from COPY server.js on purpose. Docker caches the npm install layer so it only reruns when dependencies change, not when your code changes.
  • --production skips dev dependencies, keeping the image lean for deployment.
  • Save Dockerfile.

✔️ Awesome, I've got everything!

Great. Double check you saved the file without any extension (it should be named exactly Dockerfile).

ⓧ I'd like to double check the full code

FROM node:24-alpine

WORKDIR /app

COPY package.json ./
RUN npm install --production

COPY server.js ./

EXPOSE 3000

CMD ["node", "server.js"]
Build and test the Docker image locally

Before pushing anything to AWS, confirm the container runs correctly on your machine. This catches configuration errors early.

  • Build the Docker image by running this command in your terminal:

macOS

docker build --platform linux/amd64 -t migration-app .

Why the platform flag?

Apple Silicon Macs use ARM architecture, but Fargate runs on x86_64 (amd64) by default. The --platform linux/amd64 flag ensures the image is compatible with AWS.

If you are on an Intel Mac, you can omit --platform linux/amd64 since Docker builds for amd64 by default on Intel.

Windows

docker build -t migration-app .

On Windows x86_64 machines, Docker builds for linux/amd64 by default. No extra flag needed.

  • Wait for the build to finish. You should see FINISHED in the output.

Seeing 'Cannot connect to the Docker daemon'?

Docker Desktop must be running. Open it from your system tray (Windows) or menu bar (macOS) and wait for the engine to start.

Also make sure your terminal is inside the ecs-migration folder where the Dockerfile lives. Docker needs to find the Dockerfile in the current directory.

Help me fix Docker build errors in my ECS migration project.

Now run the image as a container to verify the app works.

  • Start a container from your new image by running:
docker run -d -p 3000:3000 --name migration-test migration-app

What does this command do?

  • -d runs the container in the background (detached mode).
  • -p 3000:3000 maps port 3000 on your machine to port 3000 inside the container.
  • --name migration-test gives the container a name so you can stop it easily later.
  • Open your browser and go to http://localhost:3000.
  • You should see a dark-themed page with a pink heading that says ECS Migration Success! along with your container hostname, Node.js version, and the current time.

Page not loading?

Check that the container is running by typing docker ps in your terminal. You should see migration-test listed with status Up.

If the container exited immediately, check its logs with docker logs migration-test. A typo in server.js or a missing dependency in package.json is the most common cause.

Help me figure out why my Docker container is not serving the app on localhost:3000.

Once you have confirmed the app is working, stop and remove the test container so port 3000 is free for later.

  • Stop and remove the container by running:
docker stop migration-test && docker rm migration-test

Your app runs perfectly in a local container. Next up, you will push this image to Amazon ECR so ECS can pull and run it in the cloud.

Push the Image to Amazon ECR

Your app runs perfectly in a local container. Now you need to store that container image somewhere AWS can access it.

Amazon Elastic Container Registry (ECR) is a fully managed Docker registry that stores, manages, and deploys container images. In this step, you will create a repository in ECR and push your local image to it.

In this step, get ready to:
  • Create an ECR repository for your application image.
  • Authenticate Docker with your ECR registry.
  • Tag and push the image to ECR.
Create an ECR repository

Before you can push an image, you need a repository to store it. Think of an ECR repository as a dedicated folder for a single application's container images.

  • Create an ECR repository named migration-app by running this command:
aws ecr create-repository --repository-name migration-app --region [[AWS_REGION="your AWS region e.g. us-east-1"]]

What does this command do?

This creates a private repository called migration-app in your AWS account. The --region flag specifies which AWS region hosts the repository.

Use the same region your AWS CLI is configured for. You can check it by running aws configure get region.

  • Look at the JSON output. You should see a repositoryUri field that looks like 123456789012.dkr.ecr.us-east-1.amazonaws.com/migration-app.
  • Copy and save the repositoryUri value. You will need it in the next substeps.

Seeing a RepositoryAlreadyExistsException?

This means you already have a repository named migration-app in this region. That is fine. You can continue to the next substep and push your image to the existing repository.

If you see an access denied error, confirm your AWS CLI credentials have permissions to create ECR repositories. Help me troubleshoot my ECR access error.

Authenticate Docker with ECR

ECR is a private registry. Docker needs a temporary authentication token before it can push images there.

  • Authenticate Docker with your ECR registry by running this command:
aws ecr get-login-password --region [[AWS_REGION="your AWS region e.g. us-east-1"]] | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com

What does this command do?

This command has two parts joined by a pipe (|).

  • aws ecr get-login-password fetches a temporary authentication token from AWS.
  • docker login uses that token to authenticate Docker with your private ECR registry. The --password-stdin flag passes the token securely through the pipe instead of typing it on the command line.
  • You should see Login Succeeded in your terminal output.

Find your AWS account ID

If you are not sure of your 12-digit account ID, run aws sts get-caller-identity and look at the Account field in the output.

Login did not succeed?

Make sure Docker Desktop is running. The docker login command requires the Docker daemon to be active.

Double check that your account ID and region are correct. The registry URL must match the format ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com exactly. Help me fix my ECR login command.

Tag and push the image to ECR

Docker needs to know where to push your image. You do this by tagging the local image with the full ECR repository URI.

  • Tag your local image with the ECR repository URI by running this command:
docker tag migration-app:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com/migration-app:latest

What does tagging do?

Tagging does not create a new image. It adds an additional name (an alias) to your existing migration-app:latest image. The new name includes the full ECR registry path, which tells Docker where to push it.

  • Push the tagged image to ECR by running this command:
docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com/migration-app:latest
  • Wait for the push to complete. You should see multiple layers being uploaded, followed by a latest: digest: sha256:... confirmation line.
  • Verify the image is in ECR by running this command:
aws ecr list-images --repository-name migration-app --region [[AWS_REGION="your AWS region e.g. us-east-1"]]
  • You should see a JSON response with an imageIds array containing your image tagged as latest.

Push failed or timed out?

If you see no basic auth credentials, your login token may have expired. Re-run the authentication command from the previous substep.

If you are on Apple Silicon (M1/M2/M3) and later see issues running the image on ECS, you may need to rebuild with the platform flag: docker build --platform linux/amd64 -t migration-app . and then re-tag and push. Help me fix my Docker push to ECR.

Your container image is now stored securely in AWS. Next up, you will create an ECS cluster and define how Fargate should run your container.

Create the ECS Cluster and Task Definition

Your container image is safely stored in ECR. Now you need to tell AWS how to actually run it.

In this step, you will create an ECS cluster (the logical grouping for your containers) and a task definition (the blueprint that tells ECS which image to use, how much CPU and memory to allocate, and where to send logs).

In this step, get ready to:
  • Create an ECS cluster using the AWS CLI.
  • Write a Fargate task definition JSON file.
  • Register the task definition with ECS.
Create the ECS cluster

An ECS cluster is the logical home for your running containers. Every task and service you deploy belongs to a cluster.

  • Create a new cluster named migration-cluster by running this command:
aws ecs create-cluster --cluster-name migration-cluster

What does this command do?

This creates an empty ECS cluster with the Fargate capacity provider available by default. The cluster starts with zero running tasks. You will deploy into it once the task definition is ready.

You should see JSON output containing "status": "ACTIVE" and "clusterName": "migration-cluster".

Seeing an access denied error?

Make sure the IAM user or role configured in your AWS CLI has permission to call ecs:CreateCluster. If you are using an admin-level account, this should work without issues.

help me fix an access denied error when creating an ECS cluster

Write the task definition file

A task definition is the blueprint for your container. It specifies which image to run, how much CPU and memory to allocate, which ports to expose, and where to send logs.

Before writing the task definition, you need an IAM role called ecsTaskExecutionRole. This role lets Fargate pull your image from ECR and write logs to CloudWatch.

  • Check if the role already exists in your account by running:
aws iam get-role --role-name ecsTaskExecutionRole

✔️ I see role details in the output

The role already exists. You can move on to creating the task definition file below.

ⓧ I see a NoSuchEntity error

The role does not exist yet. You need to create it.

  • Create the role with the ECS tasks trust policy by running:
aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
  • Attach the managed policy that grants ECR pull and CloudWatch Logs permissions by running:
aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

You should see no error output from either command. The role is now ready to use.

Getting an error on Windows?

If you are using Command Prompt (CMD) instead of PowerShell, single quotes around JSON may not work. Switch to PowerShell, or save the trust policy JSON to a file called trust-policy.json and use file://trust-policy.json instead of the inline JSON string.

help me create the ecsTaskExecutionRole on Windows

What is ecsTaskExecutionRole?

This IAM role is used by the ECS agent (not your application code). It grants two key permissions: pulling container images from ECR and writing container logs to CloudWatch Logs.

Without this role, Fargate cannot start your container because it has no permission to download the image.

  • In VS Code, create a new file called task-definition.json in your ecs-migration folder.
  • Paste the following task definition into the file:
{
  "family": "migration-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::[[AWS_ACCOUNT_ID=\"your 12-digit AWS account ID\"]]:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "migration-app",
      "image": "[[AWS_ACCOUNT_ID=\"your 12-digit AWS account ID\"]].dkr.ecr.[[AWS_REGION=\"your AWS region e.g. us-east-1\"]].amazonaws.com/migration-app:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/migration-task",
          "awslogs-region": "[[AWS_REGION=\"your AWS region e.g. us-east-1\"]]",
          "awslogs-stream-prefix": "ecs",
          "awslogs-create-group": "true"
        }
      }
    }
  ]
}

What does this task definition do?

  • family names this task definition so you can reference it later.
  • networkMode: awsvpc gives the task its own network interface and IP address. This is required for Fargate.
  • cpu: 256 and memory: 512 allocate 0.25 vCPU and 0.5 GB of memory. This is the smallest Fargate configuration.
  • executionRoleArn points to the role that lets Fargate pull your image and write logs.
  • logConfiguration with awslogs-create-group: true automatically creates a CloudWatch log group for your container output.
  • Save task-definition.json.
  • Confirm the file exists by running:
ls

You should see task-definition.json alongside your other project files (server.js, package.json, Dockerfile).

Don't see the file?

Make sure your terminal is inside the ecs-migration folder. Run pwd to check your current directory.

help me find my task-definition.json file

✔️ Awesome, I've got everything!

Great. Double check you have saved the file with your actual account ID and region substituted in.

ⓧ I'd like to double check the full code

{
  "family": "migration-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "migration-app",
      "image": "ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/migration-app:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/migration-task",
          "awslogs-region": "REGION",
          "awslogs-stream-prefix": "ecs",
          "awslogs-create-group": "true"
        }
      }
    }
  ]
}

Replace ACCOUNT_ID with your 12-digit AWS account ID and REGION with your AWS region (e.g. us-east-1).

Register the task definition with ECS

With the JSON file saved, you can now register it with ECS so the service knows how to launch your container.

  • Register the task definition by running:
aws ecs register-task-definition --cli-input-json file://task-definition.json

What does this command do?

This uploads your task definition JSON to ECS and creates revision 1 of the migration-task family. Each time you register a new version, ECS increments the revision number.

You should see a large JSON response. Look for "family": "migration-task" and "revision": 1 near the top of the output.

Seeing a ClientException or validation error?

  • Check that your executionRoleArn contains your actual 12-digit account ID, not the placeholder text.
  • Check that the image field matches your ECR repository URI exactly (account ID, region, and repository name).
  • Verify task-definition.json is valid JSON. Missing commas or extra trailing commas will cause a parse error.

help me fix a task definition registration error

Your cluster and task definition are ready. Next up, you will deploy a running service and access your app from a public IP address.

Deploy as a Fargate Service and Verify

Your cluster is live and your task definition is registered. Now it's time to launch the actual container and see your app running on AWS.

To deploy a Fargate task, ECS needs to know which network to place it in and which firewall rules to apply. In this step, you'll configure networking, create the service, and access your live application from a browser.

In this step, get ready to:
  • Identify your default VPC subnet and configure a security group for port 3000.
  • Create an ECS service that launches your Fargate task with a public IP.
  • Verify the task is running and access your app in the browser.
Identify your default VPC and subnet

Fargate uses awsvpc networking, which means each task gets its own network interface inside a VPC. You need to tell ECS which subnet to place the task in.

  • Retrieve your default VPC ID by running this command:
aws ec2 describe-vpcs --filters "Name=is-default,Values=true" --query "Vpcs[0].VpcId" --output text

You should see a VPC ID like vpc-0abc1234def56789. Copy this value.

  • Retrieve a public subnet ID from your default VPC by running this command:
aws ec2 describe-subnets --filters "Name=vpc-id,Values=[[VPC_ID="your VPC ID from above"]]" "Name=default-for-az,Values=true" --query "Subnets[0].SubnetId" --output text

You should see a subnet ID like subnet-0abc1234def56789. Copy this value too.

What is a VPC subnet?

A VPC is your private network in AWS. Subnets are subdivisions within that network, each placed in a specific availability zone. Your default VPC has public subnets already configured to allow internet access.

Create a security group for your task

A security group acts as a firewall for your task. The default VPC security group blocks inbound traffic, so you need one that allows connections on port 3000.

  • Create a new security group in your default VPC by running this command:
aws ec2 create-security-group --group-name ecs-migration-sg --description "Allow inbound on port 3000" --vpc-id [[VPC_ID="your VPC ID"]]

The output includes a GroupId value like sg-0abc1234def56789. Copy this security group ID.

  • Add an inbound rule allowing TCP traffic on port 3000 from anywhere by running this command:
aws ec2 authorize-security-group-ingress --group-id [[SG_ID="your security group ID"]] --protocol tcp --port 3000 --cidr 0.0.0.0/0

What does this command do?

This opens port 3000 to all IP addresses (0.0.0.0/0). For a production app, you would restrict this to specific IP ranges or place a load balancer in front. For testing purposes, opening to all is fine.

Getting an InvalidGroup error?

Make sure the security group ID you pasted starts with sg- and matches the output from the previous command. Also check that the VPC ID you used to create it matches your default VPC.

Help me fix my security group ingress rule

Create the ECS service

An ECS service keeps your task running and replaces it if it fails. It ties together your cluster, task definition, and networking configuration into a single deployable unit.

  • Create the service by running this command (replace the subnet and security group placeholders with your values):
aws ecs create-service --cluster migration-cluster --service-name migration-service --task-definition migration-task:1 --desired-count 1 --launch-type FARGATE --platform-version LATEST --network-configuration "awsvpcConfiguration={subnets=[[[SUBNET_ID="your subnet ID"]]],securityGroups=[[[SG_ID="your security group ID"]]],assignPublicIp=ENABLED}"

What does this command do?

  • --cluster migration-cluster tells ECS which cluster to deploy into.
  • --task-definition migration-task:1 references the task definition you registered in the previous step.
  • --desired-count 1 means ECS will keep exactly one copy of your task running.
  • assignPublicIp=ENABLED gives the task a public IP so you can reach it from your browser.

You should see a large JSON response with the service details. Look for "status": "ACTIVE" in the output to confirm it was created successfully.

Getting an InvalidParameterException?

Check that your subnet ID starts with subnet- and your security group ID starts with sg-. Make sure there are no extra spaces inside the curly braces of the network configuration.

If you see a role error, confirm that ecsTaskExecutionRole exists in your account from the previous step.

Help me debug my ECS create-service command

Verify your app is live

ECS is now pulling your image from ECR and starting the container. It takes 30-60 seconds for the task to reach RUNNING status.

  • Get the task ARN for your running task by running this command:
aws ecs list-tasks --cluster migration-cluster --service-name migration-service --query "taskArns[0]" --output text

You should see a task ARN like arn:aws:ecs:us-east-1:123456789:task/migration-cluster/abc123.... Copy this value.

Seeing "None" instead of a task ARN?

The task might not have started yet. Wait 30 seconds and run the command again. If it still shows nothing, check the service events for error details:

aws ecs describe-services --cluster migration-cluster --services migration-service --query "services[0].events[0:3]"
  • Wait for the task to reach RUNNING status by running this command:
aws ecs wait tasks-running --cluster migration-cluster --tasks [[TASK_ARN="your task ARN from above"]]

This command waits silently until the task is running. When your terminal prompt returns with no error, the task is live.

  • Get the network interface ID attached to your task by running this command:
aws ecs describe-tasks --cluster migration-cluster --tasks [[TASK_ARN="your task ARN"]] --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text

You should see an ENI ID like eni-0abc1234def56789. Copy this value.

  • Retrieve the public IP address from the network interface by running this command:
aws ec2 describe-network-interfaces --network-interface-ids [[ENI_ID="your ENI ID from above"]] --query "NetworkInterfaces[0].Association.PublicIp" --output text

You should see a public IP address like 3.92.145.67. This is the address of your live container.

Why so many steps to get the public IP?

Each Fargate task gets its own Elastic Network Interface (ENI) in your VPC. The public IP is assigned to the ENI, not directly to the task. That is why you need to look up the ENI first, then query EC2 for its public IP.

In production, you would use an Application Load Balancer to provide a stable DNS endpoint instead of relying on task-level public IPs that change on every deployment.

  • Open your browser.
  • Navigate to your app by entering this URL (replacing the IP with yours):
http://[[PUBLIC_IP="your public IP address"]]:3000

You should see the ECS Migration Success page with a dark background, a pink heading, and information about the container hostname, Node.js version, and current time.

Page not loading?

  • Make sure you're using http:// (not https) and including :3000 at the end.
  • Verify your security group allows inbound TCP on port 3000 from 0.0.0.0/0. Run aws ec2 describe-security-groups --group-ids YOUR_SG_ID to check.
  • Confirm the task is still running. If it stopped, check the CloudWatch logs at /ecs/migration-task for error output.

My ECS Fargate app is not loading in the browser

Secret mission

Add an Application Load Balancer

In production, teams don't expose containers directly via their task IP addresses. They place an Application Load Balancer in front of their ECS service to get health checks, failover, and a stable DNS endpoint. Ready to set this up yourself?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project uses AWS Fargate, which bills per-second for vCPU and memory while your task is running.

Cost warning

Your Fargate task costs approximately $0.01/hour (0.25 vCPU / 0.5 GB memory). If you leave it running overnight or over a weekend, charges will accumulate.

If you completed the Secret Mission, the Application Load Balancer also incurs hourly charges. Pause or delete your resources when you are done experimenting.

Resources you used:

  • ECS Fargate task (billed per-second for vCPU and memory).
  • ECR repository (free for up to 500 MB of storage per month with free tier).
  • CloudWatch log group (minimal cost).
  • ECS cluster (no cost by itself, only running tasks cost money).
  • Security group inbound rule for port 3000.
  • Application Load Balancer and target group (Secret Mission only, billed hourly).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your ECS deployment.

  • Your Fargate task will continue running and remain accessible via its public IP.
  • Expect approximately $0.01/hour in Fargate charges while the task is active.
  • If you completed the Secret Mission, the ALB also incurs hourly charges.

Pause - I'll come back to this later

Stop your running task to halt all Fargate charges while keeping your cluster, task definition, and ECR image intact.

  • Set your service desired count to 0 by running:
aws ecs update-service --cluster migration-cluster --service migration-service --desired-count 0
  • This stops your Fargate task and halts per-second billing.
  • Your ECR image, cluster, and task definition remain available for when you return.
  • To resume later, run the same command with --desired-count 1.

What about the ALB?

If you completed the Secret Mission, the Application Load Balancer still incurs hourly charges even with zero tasks. Delete the ALB and target group if you want to fully pause costs.

Delete - I don't want to use this again

Remove all project resources completely. This is recommended once you are done with the project to avoid any ongoing charges.

  • Delete the ECS service by running:
aws ecs delete-service --cluster migration-cluster --service migration-service --force
  • Delete the ECS cluster by running:
aws ecs delete-cluster --cluster migration-cluster
  • Deregister the task definition by running:
aws ecs deregister-task-definition --task-definition migration-task:1
  • Delete the ECR repository and all images by running:
aws ecr delete-repository --repository-name migration-app --force
  • Delete the CloudWatch log group by running:
aws logs delete-log-group --log-group-name /ecs/migration-task
  • If you added an inbound rule to your security group for port 3000, revoke it by running:
aws ec2 revoke-security-group-ingress --group-id [[SG_ID="your security group ID"]] --protocol tcp --port 3000 --cidr 0.0.0.0/0

Secret Mission resources

If you completed the Secret Mission, also delete your ALB listener, load balancer, and target group using the AWS CLI or the EC2 console. Delete the listener first, then the load balancer, then the target group.

  • Finally, delete your local project folder if you no longer need the source files.

That's a Wrap!

That's a Wrap!

Nice work! You just completed a full cloud migration to Amazon ECS with Fargate. You containerized an application, pushed it to a registry, and deployed it as a managed service on AWS.

You've learned how to:

  • Built a Node.js web application and containerized it with Docker.
  • Pushed a container image to Amazon ECR and registered a Fargate task definition with ECS.
  • Deployed a live Fargate service and accessed it via its public IP address.
  • Added an Application Load Balancer for production-grade traffic routing and health checks.

Ready to quiz yourself?