Deploy a Node.js API to ECS Fargate

Containerize a Node.js API and deploy it to AWS ECS Fargate via ECR.

Introduction

30 Second Summary

You have an app running on your computer. How do you get it running in the cloud so anyone in the world can access it?

In this project, you will build a Node.js Express API, package it into a Docker container, and deploy it to AWS ECS Fargate. You will hit a public IP address in your browser and see a live JSON response served from a container running in the cloud.

What You'll Build

A lightweight REST API with two endpoints that you will containerize, push to both a public and private image registry, and deploy on serverless cloud infrastructure.

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

  • A Node.js Express API running inside a Docker container, tested and verified locally.
  • The same Docker image pushed to both Docker Hub (public registry) and Amazon ECR (private registry).
  • A live ECS Fargate service accessible via a public IP address from anywhere on the internet.
  • Secret Mission: Add a logging sidecar container to your task definition to learn multi-container ECS task composition.

Are there any prerequisites?

You will need an AWS account (free tier eligible), a Docker Hub account, and Docker Desktop installed and running on your machine. Basic familiarity with Docker concepts like building images from a Dockerfile is recommended.

Before We Start

Before We Start...

Install Node.js and AWS CLI

Before you can build and deploy your Express API, you need two tools installed on your machine. Node.js runs your JavaScript code locally, and the AWS CLI lets you interact with AWS services from your terminal.

In this step, you will install both tools and configure the AWS CLI with your credentials so it can create cloud resources on your behalf.

In this step, get ready to:
  • Install Node.js 24 LTS and verify it works.
  • Install AWS CLI v2 and verify it works.
  • Configure your AWS credentials for CLI access.
Install Node.js 24 LTS

Node.js is the JavaScript runtime that powers your Express API. You need version 24 or higher for this project.

  • Check if Node.js is already installed by running this command in your terminal:
node --version

✔️ I see version 24 or higher

You are all set. Move on to the next substep below.

ⓧ I see an older version

You have Node.js installed, but you need version 24 or higher. Download and install the latest LTS version from the official site.

  • Visit nodejs.org and click the LTS download button for your operating system.
  • Run the installer and accept the default settings.
  • Close and reopen your terminal, then run node --version again to confirm you see v24 or higher.

Windows users with an old version

The new installer will replace your existing Node.js version automatically. No need to uninstall the old one first.

ⓧ Command not found

Node.js is not installed yet. Download and install it from the official site.

  • Visit nodejs.org and click the LTS download button for your operating system.
  • Run the installer and accept the default settings.
  • Close and reopen your terminal after the installation finishes.
  • Run node --version to confirm you see v24 or higher.
  • Also verify that npm (Node's package manager) is available by running:
npm --version

You should see a version number printed in your terminal. This confirms Node.js and npm are ready to use.

What is npm?

npm is the package manager that comes bundled with Node.js. You will use it to install Express and manage your project dependencies.

Install AWS CLI v2

The AWS CLI is a command-line tool that lets you create and manage AWS resources directly from your terminal. You will use it to push images to ECR, create ECS clusters, and deploy your service.

  • Check if AWS CLI is already installed by running:
aws --version

✔️ I see aws-cli/2.x

Your AWS CLI v2 is installed and ready. Move on to the next substep below.

ⓧ I see aws-cli/1.x

You have an older version (v1) installed. You need to upgrade to v2.

macOS

  • Download and install the official macOS package by running these commands in your terminal:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
  • Close and reopen your terminal, then run aws --version to confirm you see aws-cli/2 at the start.

Windows

  • Download the MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
  • Run the downloaded .msi file and follow the on-screen prompts to complete the installation.
  • Close and reopen your terminal, then run aws --version to confirm you see aws-cli/2 at the start.

ⓧ Command not found

AWS CLI is not installed yet. Install it using the official installer for your platform.

macOS

  • Download and install the official macOS package by running these commands in your terminal:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
  • Close and reopen your terminal, then run aws --version to confirm you see aws-cli/2 at the start.

Windows

  • Download the MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
  • Run the downloaded .msi file and follow the on-screen prompts to complete the installation.
  • Close and reopen your terminal, then run aws --version to confirm you see aws-cli/2 at the start.

Still seeing version 1 after installing?

Your system may have the old aws command cached in its PATH. Close all terminal windows and open a fresh one. If you still see v1, your old installation may be shadowing the new one. Check which aws binary is being used by running which aws (macOS) or where aws (Windows).

help me fix my AWS CLI version conflict

Configure your AWS credentials

The AWS CLI needs your credentials to create resources in your AWS account. You will configure it with your Access Key ID, Secret Access Key, a default region, and an output format.

  • Run the configuration wizard by entering this command:
aws configure

The command will prompt you for four values. Enter them one at a time:

  • AWS Access Key ID: Paste your Access Key ID from the AWS console.
  • AWS Secret Access Key: Paste your Secret Access Key.
  • Default region name: Enter us-east-1.
  • Default output format: Enter json.

Where do I find my Access Key?

In the AWS Management Console, click your username in the top-right corner and select Security credentials. Scroll down to Access keys and click Create access key. Copy both the Access Key ID and Secret Access Key before closing the dialog.

Your Secret Access Key is only shown once at creation time. Store it somewhere safe.

  • Verify that your credentials are working by running:
aws sts get-caller-identity

You should see a JSON response containing your AWS account ID, user ARN, and user ID. This confirms the CLI can authenticate with your account.

Seeing an authentication error?

Double-check that you pasted the Access Key ID and Secret Access Key correctly. Run aws configure again to re-enter them. Make sure there are no extra spaces before or after the values.

help me fix my AWS CLI authentication error

Your development environment is ready. Next up, you will build the Express API that will eventually run inside a container on AWS.

Build the Express API

Your development tools are ready. Now you need something to actually deploy.

In this step, you'll build a lightweight Node.js API using Express, test it on your machine, and prepare it for containerization.

In this step, get ready to:
  • Create a Node.js Express API with health-check and greeting endpoints.
  • Test the API running locally on your machine.
  • Add a .dockerignore file to keep your Docker builds clean.
Set up the project and write the API

Every Node.js project starts with a project folder and a package.json file that tracks your dependencies.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir hello-ecs
cd hello-ecs
  • Initialize a new Node.js project and install Express by running these commands:
npm init -y
npm install express@5
  • You should see output confirming packages were added, ending with a line like added X packages.

What do these commands do?

  • npm init -y creates a package.json file with default settings. This file tracks your project's name, version, and dependencies.
  • npm install express@5 downloads Express 5 (the latest stable web framework for Node.js) into a node_modules folder and records it as a dependency.

Seeing 'npm: command not found'?

This means Node.js is not installed or not in your system PATH. Go back to Step 1 and verify your Node.js installation by running node --version.

help me fix npm command not found after installing Node.js

Now you need to create the API code. Open VS Code so you can create files in your project.

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

You should see package.json and a node_modules folder in the VS Code sidebar.

  • Create a new file by clicking File then New File.
  • Name the file index.js and save it in the hello-ecs folder.
  • Paste the following code into index.js:
const express = require("express");
const app = express();
const PORT = 3000;

app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString(),
    service: "hello-ecs"
  });
});

app.get("/api/greet/:name", (req, res) => {
  res.json({
    message: `Hello, ${req.params.name}! This response is served from ECS Fargate.`,
    timestamp: new Date().toISOString()
  });
});

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

What does this code do?

  • const PORT = 3000 sets the port your server listens on. Port 3000 is a common choice for local development.
  • GET /health returns a JSON object with the service status and current time. This is a standard health-check endpoint that load balancers and monitoring tools use to verify your app is alive.
  • GET /api/greet/:name is a dynamic route. The :name portion captures whatever the user types in the URL and includes it in the response.
  • app.listen(PORT, "0.0.0.0", ...) starts the server and binds to all network interfaces. Binding to 0.0.0.0 is essential for containers, where localhost alone would not be reachable from outside the container.
  • Save index.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).

VS Code showing red underlines?

If you see squiggly lines under require, make sure you saved the file with the .js extension. VS Code needs the file extension to apply the correct language mode.

help me fix red underlines in my Express code in VS Code

✔️ Awesome, I've got everything!

Great. Make sure index.js is saved before moving on.

ⓧ I'd like to double check the full code

Here is the full index.js file:

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

app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString(),
    service: "hello-ecs"
  });
});

app.get("/api/greet/:name", (req, res) => {
  res.json({
    message: `Hello, ${req.params.name}! This response is served from ECS Fargate.`,
    timestamp: new Date().toISOString()
  });
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`Server running on port ${PORT}`);
});
Test the API locally

Before packaging your app into a container, confirm it works on your machine first.

  • Open a terminal in VS Code by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
  • Start the server by running:
node index.js
  • You should see this output in your terminal:
Server running on port 3000
  • Open your browser and visit http://localhost:3000/health.

You should see a JSON response that looks like this:

{
  "status": "healthy",
  "timestamp": "2026-05-12T10:30:00.000Z",
  "service": "hello-ecs"
}

Why test locally first?

If the API does not work on your machine, it will not work in a container either. Testing locally catches typos and missing dependencies before you add the complexity of Docker and AWS.

  • Go back to your terminal and stop the server by pressing Ctrl+C.

Not seeing the JSON response?

  • If the terminal says Cannot find module 'express', run npm install express@5 again and check that node_modules exists in your project folder.
  • If the terminal says EADDRINUSE, another process is using port 3000. Close any other terminals or apps that might be running a server, then try again.
  • Make sure the URL in your browser starts with http:// not https://.

help me debug my Express server not responding on localhost:3000

Create the .dockerignore file

When Docker builds an image, it copies your entire project folder into the container. The node_modules folder is large and unnecessary because Docker will install fresh dependencies inside the container.

  • In VS Code, create a new file called .dockerignore in your hello-ecs folder.
  • Paste the following into .dockerignore:
node_modules
npm-debug.log
.git
.gitignore

What does .dockerignore do?

It works like a .gitignore but for Docker. Files listed here are excluded from the build context, making your image smaller and your builds faster.

Excluding node_modules also prevents conflicts between your local machine's dependencies and the ones Docker installs fresh inside the container.

  • Save .dockerignore.
  • Confirm the file exists by checking the VS Code sidebar. You should see .dockerignore, index.js, package.json, and node_modules all listed in your project.

Don't see .dockerignore in the sidebar?

Files starting with a dot are sometimes hidden by default. In VS Code, they should still appear in the Explorer sidebar. Make sure you saved the file in the hello-ecs folder and not in a parent directory.

help me find my .dockerignore file in VS Code

✔️ Awesome, I've got everything!

Great. Make sure both index.js and .dockerignore are saved.

ⓧ I'd like to double check the full code

Here is .dockerignore:

node_modules
npm-debug.log
.git
.gitignore

Here is index.js:

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

app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString(),
    service: "hello-ecs"
  });
});

app.get("/api/greet/:name", (req, res) => {
  res.json({
    message: `Hello, ${req.params.name}! This response is served from ECS Fargate.`,
    timestamp: new Date().toISOString()
  });
});

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

Your package.json should have express listed under dependencies:

{
  "name": "hello-ecs",
  "version": "1.0.0",
  "description": "A simple Express API for ECS Fargate deployment",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^5.1.0"
  }
}

Your API is built and tested. Next, you'll package it into a Docker container so it can run identically on any machine.

Containerize with Docker

Your Express API works on your machine. Now you need to package it so it runs identically on any machine, whether that's a teammate's laptop or a cloud server in AWS.

A Dockerfile is a recipe that tells Docker exactly how to build a self-contained image of your app. In this step, you'll write that recipe and use it to build and run your API inside a container.

In this step, get ready to:
  • Create a Dockerfile that packages your Express API into a container image.
  • Build the Docker image and run it locally to verify it works.
Create the Dockerfile

The Dockerfile tells Docker which base image to start from, how to install your dependencies, and how to start the app. You'll use a lightweight Alpine Linux image to keep the final image small and fast to transfer.

  • In VS Code, create a new file called Dockerfile in your project folder (the same folder that contains index.js).
  • Paste the following code into Dockerfile:
FROM node:24-alpine

WORKDIR /app

COPY package.json package-lock.json* ./

RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]

What does this Dockerfile do?

  • FROM node:24-alpine starts from a minimal Node.js 24 image based on Alpine Linux (around 50 MB instead of 300+ MB for the full image).
  • WORKDIR /app creates and switches to the /app directory inside the container.
  • COPY package.json package-lock.json* ./ copies your dependency files first. The * after package-lock.json means Docker won't fail if the lock file doesn't exist yet.
  • RUN npm install --production installs only production dependencies (skipping dev tools).
  • COPY . . copies the rest of your app files. This is done after installing dependencies so Docker can cache the install layer.
  • CMD ["node", "index.js"] tells Docker what command to run when the container starts.
  • Save Dockerfile.
  • Confirm the file exists by running:
ls

You should see Dockerfile listed alongside index.js, package.json, and .dockerignore.

Don't see the Dockerfile?

Make sure the file is named exactly Dockerfile with a capital D and no file extension. It should not be Dockerfile.txt or dockerfile.

Also confirm you saved it in the same folder as index.js, not a subfolder.

Still stuck? Help me figure out why my Dockerfile isn't showing up when I run ls.

Build and run the Docker image

With the Dockerfile ready, you can build an image and spin up a container to prove the app works exactly the same way inside Docker as it does running directly on your machine.

  • Build the Docker image by running this command:
docker build -t hello-ecs .

What does this command do?

  • docker build reads your Dockerfile and assembles the image layer by layer.
  • -t hello-ecs tags (names) the resulting image so you can reference it easily later.
  • . tells Docker to use the current directory as the build context (where it looks for files to COPY).

The build takes 20-60 seconds the first time as Docker downloads the node:24-alpine base image. You should see output ending with a line that says the image was successfully tagged.

Seeing a build error?

Make sure Docker Desktop is running. You can check by looking for the Docker whale icon in your system tray (Windows) or menu bar (macOS).

If you see a permission error, try closing and reopening your terminal. Docker Desktop sometimes needs a fresh terminal session after starting up.

Need more help? Help me debug my Docker build error.

  • Run the container to test your image by running this command:
docker run -p 3000:3000 hello-ecs

What does this command do?

  • docker run creates and starts a new container from the hello-ecs image.
  • -p 3000:3000 maps port 3000 on your machine to port 3000 inside the container, so you can reach the API from your browser.
  • Open your browser and visit http://localhost:3000/health.

You should see the same JSON response as before, with "status": "healthy" and the "service": "hello-ecs" field. This time, the response is coming from inside the Docker container.

  • Stop the container by pressing Ctrl+C in your terminal.

Port already in use?

If you see an error about port 3000 being in use, your Express API from the previous step might still be running. Stop it first by pressing Ctrl+C in the terminal where you ran node index.js, then try the docker run command again.

Still stuck? Help me fix a port conflict when running my Docker container.

✔️ Awesome, I've got everything!

Great. Make sure you've saved your Dockerfile and confirmed the container runs correctly.

ⓧ I'd like to double check the full code

Here is the complete Dockerfile for comparison:

FROM node:24-alpine

WORKDIR /app

COPY package.json package-lock.json* ./

RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]

Your API is now containerized and proven to work inside Docker. Next up, you'll push this image to Docker Hub so it's available as a public reference, and then to Amazon ECR for your cloud deployment.

Push to Docker Hub

Your containerized API is running locally. The next step is to push the image to a public registry so it can be pulled from anywhere.

Docker Hub is the default public registry for Docker images. By pushing your image there, anyone (including AWS services in a later step) can pull and run your container.

In this step, get ready to:
  • Tag your Docker image for Docker Hub.
  • Log in and push the image to Docker Hub.
  • Verify the image appears in your online repository.
Tag your image for Docker Hub

Docker images need a specific naming format before they can be pushed to a registry. The format is username/repository:tag.

  • Tag your local image for Docker Hub by running this command:
docker tag hello-ecs [[DOCKERHUB_USERNAME="your Docker Hub username"]]/hello-ecs:latest

What does this command do?

The docker tag command creates a new reference to your existing hello-ecs image. It does not copy or rebuild anything.

The new name tells Docker Hub which account and repository to push to. The :latest tag is a convention indicating the most recent version of the image.

  • Confirm the new tag exists by running:
docker images

You should see both hello-ecs and your-username/hello-ecs listed. They share the same image ID because tagging only creates a new name.

Don't see the tagged image?

Make sure you replaced the username placeholder with your actual Docker Hub username. The name must match your Docker Hub account exactly (case-sensitive).

Help me troubleshoot my docker tag command.

Log in to Docker Hub and push the image

Before you can push, Docker needs to verify your identity with Docker Hub. You will log in from the terminal, then push the tagged image.

  • Log in to Docker Hub by running this command:
docker login

What will Docker ask me?

Docker will prompt you for your Docker Hub username and password. Enter your Docker Hub credentials when prompted. If you have two-factor authentication enabled, use a Personal Access Token instead of your password.

You should see Login Succeeded in your terminal output.

Login failed?

Double-check that you are entering your Docker Hub username (not your email) and the correct password. If you have two-factor authentication enabled on Docker Hub, you need to generate a Personal Access Token from your Docker Hub account settings and use that as your password.

Also confirm Docker Desktop is running. The Docker CLI needs the Docker daemon to authenticate.

Help me fix my Docker Hub login.

  • Push the tagged image to Docker Hub by running this command:
docker push [[DOCKERHUB_USERNAME="your Docker Hub username"]]/hello-ecs:latest

You will see progress bars for each layer being uploaded. Once complete, Docker prints a digest (a unique hash) confirming the push succeeded.

Push denied or access forbidden?

This means the username in your image tag does not match the account you logged in with. Re-run the docker tag command with your correct Docker Hub username, then push again.

Help me fix my docker push error.

Verify the image on Docker Hub
  • Open your browser and go to https://hub.docker.com.
  • Sign in to your Docker Hub account.
  • Click on Repositories in the top navigation.

You should see a repository called hello-ecs listed under your account. Clicking into it shows the latest tag you just pushed.

Why push to a public registry?

Docker Hub makes your image available to anyone. This is useful for open-source projects and quick sharing. In the next step, you will push the same image to Amazon ECR, a private registry that restricts access to your AWS account only.

Your image is now publicly available on Docker Hub. Next up, you will push the same image to Amazon ECR, a private registry that integrates directly with AWS services like ECS.

Push to Amazon ECR

Your image is now on Docker Hub, a public registry anyone can pull from. In production, AWS services like ECS pull images from Amazon ECR instead.

ECR is a private registry that lives inside your AWS account. It integrates directly with ECS, so your containers can pull images without extra authentication steps at deploy time.

In this step, get ready to:
  • Create a private ECR repository.
  • Authenticate Docker to your ECR registry.
  • Tag and push your image to ECR.
Create an ECR repository

Before you can push an image to ECR, you need a repository to hold it. Think of it like creating a folder for your image in the AWS cloud.

  • Create a private ECR repository called hello-ecs by running this command:
aws ecr create-repository --repository-name hello-ecs --region us-east-1

What does this command do?

This creates a private container image repository named hello-ecs in the us-east-1 region of your AWS account.

The response includes a repositoryUri field. This URI is the address you will use to tag and push your Docker image. It follows the format YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/hello-ecs.

  • Look at the JSON output in your terminal. Find the repositoryUri value and copy it somewhere safe. You will need it in the next two commands.

Find your AWS account ID

Your 12-digit AWS account ID is the number at the start of the repositoryUri (everything before .dkr.ecr). You will use this ID in the next commands.

Seeing a RepositoryAlreadyExistsException?

This means a repository named hello-ecs already exists in your account. You can safely continue to the next step since your repository is already ready.

help me fix an error when creating my ECR repository

Authenticate Docker to ECR

ECR is a private registry, so Docker needs temporary credentials to push images to it. The AWS CLI generates a 12-hour authentication token that Docker uses to log in.

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

What does this command do?

  • aws ecr get-login-password --region us-east-1 retrieves a temporary authentication token from AWS.
  • The | pipe passes that token directly to the next command.
  • docker login --username AWS --password-stdin uses the token to authenticate Docker to your private ECR registry.

The token is valid for 12 hours. After that, you would need to run this command again.

  • Confirm you see Login Succeeded in your terminal output.

Login failed or access denied?

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

Double check that you replaced the account ID placeholder with your actual 12-digit AWS account ID (no spaces or dashes).

help me troubleshoot my ECR login failure

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 for ECR by running this command:
docker tag hello-ecs [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/hello-ecs:latest

What does this command do?

This creates a new tag that points your existing hello-ecs image to your ECR repository address. The image data stays the same. Only the reference changes so Docker knows 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.us-east-1.amazonaws.com/hello-ecs:latest

What does this command do?

This uploads each layer of your Docker image to the ECR repository. You will see progress bars as each layer is pushed. Once complete, the image is stored in your private AWS registry and ready for ECS to pull.

  • Wait for the push to complete. You should see each layer showing Pushed followed by a final digest line.
  • Verify the image is in ECR by running:
aws ecr describe-images --repository-name hello-ecs --region us-east-1
  • You should see a JSON response containing your image with the latest tag listed under imageTags.

Push failed with authorization errors?

Your authentication token may have expired. Run the aws ecr get-login-password command from the previous substep again to refresh it.

If you see a "repository does not exist" error, confirm that the account ID in your tag matches the one in the repositoryUri from when you created the repository.

help me fix my ECR push failure

Your image is now stored in both Docker Hub and Amazon ECR. Next up, you will deploy this image as a running container on ECS Fargate.

Deploy on ECS Fargate

Your Docker image is sitting in Amazon ECR, ready to be pulled by any AWS service that needs it.

Now comes the exciting part. You will launch that image as a running container in the cloud using Amazon ECS Fargate. Fargate is a serverless compute engine that runs your containers without requiring you to manage servers or clusters of EC2 instances.

In this step, get ready to:
  • Create an ECS cluster and configure the required IAM role.
  • Write and register a task definition that tells ECS how to run your container.
  • Launch a Fargate service and access your live API via a public IP.
Create an ECS cluster

An ECS cluster is a logical grouping of tasks and services. Think of it as the home for your running containers.

  • Create your ECS cluster by running this command:
aws ecs create-cluster --cluster-name hello-ecs-cluster

What does this command do?

This creates an empty ECS cluster named hello-ecs-cluster in your default region (us-east-1). The cluster itself has no cost. You only pay when you run tasks inside it.

  • You should see a JSON response with "clusterName": "hello-ecs-cluster" and "status": "ACTIVE" in the output.

Seeing an access denied error?

Make sure your AWS CLI credentials have permissions to create ECS resources. Your IAM user needs the AmazonECS_FullAccess managed policy or equivalent permissions.

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

Before you can run a Fargate task, ECS needs an IAM role called ecsTaskExecutionRole that grants permission to pull images from ECR and write logs to CloudWatch.

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

✔️ I see the role details

The role already exists. You can move on to the next substep.

ⓧ I see a NoSuchEntity error

You need to create the role. Follow these steps to set it up.

  • Create a file named ecs-tasks-trust-policy.json in your project folder with this content:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
  • Create the role by running:
aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document file://ecs-tasks-trust-policy.json
  • Attach the AWS managed policy to the role by running:
aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

What does ecsTaskExecutionRole do?

This role lets the ECS agent pull your container image from ECR and send container logs to CloudWatch Logs. Without it, your Fargate task cannot start.

Create the task definition and register it

A task definition is a blueprint that tells ECS exactly how to run your container. It specifies which image to use, how much CPU and memory to allocate, which ports to expose, and where to send logs.

  • First, create the CloudWatch Logs log group that your task will write to:
aws logs create-log-group --log-group-name /ecs/hello-ecs --region us-east-1
  • Create a file named task-definition.json in your project folder with the following content:
{
  "family": "hello-ecs-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "hello-ecs",
      "image": "YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/hello-ecs:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/hello-ecs",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}
  • Replace both instances of YOUR_ACCOUNT_ID with your actual 12-digit AWS account ID (the same one you used when pushing to ECR).

What does this task definition do?

  • cpu: 256 and memory: 512 allocate 0.25 vCPU and 0.5 GB RAM. This is the smallest Fargate configuration.
  • networkMode: awsvpc gives each task its own network interface, which is required for Fargate.
  • containerPort: 3000 matches the port your Express app listens on.
  • The logConfiguration section routes container output to CloudWatch Logs so you can debug remotely.
  • Register the task definition with ECS by running:
aws ecs register-task-definition --cli-input-json file://task-definition.json
  • You should see a JSON response containing "family": "hello-ecs-task" and "revision": 1 confirming the registration succeeded.

Seeing a ClientException about the role?

If you see an error mentioning the execution role ARN, double-check that you replaced YOUR_ACCOUNT_ID with your actual account ID and that ecsTaskExecutionRole was created successfully in the previous substep.

help me fix an error with my ECS task execution role

Create the ECS service and access your live API

A service tells ECS to keep your task running. If the task crashes, the service automatically restarts it. You will create a service that launches one task in a public subnet so it gets a public IP address.

  • First, find a public subnet ID from your default VPC by running:
aws ec2 describe-subnets --filters "Name=default-for-az,Values=true" --query "Subnets[0].SubnetId" --output text
  • Copy the subnet ID from the output (it looks like subnet-0abc1234def56789).
  • Next, find the default security group for your VPC by running:
aws ec2 describe-security-groups --filters "Name=group-name,Values=default" --query "SecurityGroups[0].GroupId" --output text
  • Copy the security group ID from the output (it looks like sg-0abc1234def56789).

Why use the default VPC?

Every AWS account comes with a default VPC in each region. Its subnets are public by default, meaning they have a route to the internet through an internet gateway. This makes them perfect for a quick deployment where you need a public IP without extra networking setup.

aws ec2 authorize-security-group-ingress --group-id [[SECURITY_GROUP_ID="your security group ID"]] --protocol tcp --port 3000 --cidr 0.0.0.0/0
  • Create the ECS service by running this command (replace the subnet and security group IDs with the values you copied):
aws ecs create-service --cluster hello-ecs-cluster --service-name hello-ecs-service --task-definition hello-ecs-task:1 --desired-count 1 --launch-type "FARGATE" --network-configuration "awsvpcConfiguration={subnets=[[[SUBNET_ID="your subnet ID"]]],securityGroups=[[[SECURITY_GROUP_ID="your security group ID"]]],assignPublicIp=ENABLED}"

What does this command do?

  • It launches one task (--desired-count 1) using your registered task definition.
  • assignPublicIp=ENABLED gives the task a public IP so you can reach it from the internet.
  • The service will keep exactly one copy of your container running at all times.

It takes about 30-60 seconds for Fargate to pull your image and start the container. Wait a minute, then retrieve the public IP.

  • Get the task ARN by running:
aws ecs list-tasks --cluster hello-ecs-cluster --service-name hello-ecs-service --query "taskArns[0]" --output text
  • Copy the task ARN from the output.
  • Get the network interface ID from the task by running (replace the task ARN with yours):
aws ecs describe-tasks --cluster hello-ecs-cluster --tasks [[TASK_ARN="your task ARN"]] --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text
  • Copy the ENI ID from the output (it looks like eni-0abc1234def56789).
  • Get the public IP from the ENI by running:
aws ec2 describe-network-interfaces --network-interface-ids [[ENI_ID="your ENI ID"]] --query "NetworkInterfaces[0].Association.PublicIp" --output text
  • Copy the public IP address from the output.
  • Open your browser and navigate to http://YOUR_PUBLIC_IP:3000/health (replacing YOUR_PUBLIC_IP with the IP you just copied).

You should see a JSON response with "status": "healthy" and "service": "hello-ecs". Your API is now live on the internet, served from a container running on AWS Fargate.

  • Try the greeting endpoint too: http://YOUR_PUBLIC_IP:3000/api/greet/YourName.

Not seeing a response from the public IP?

  • Wait 60 seconds and try again. Fargate needs time to pull the image and start the container.
  • Verify your task is running by checking aws ecs describe-tasks --cluster hello-ecs-cluster --tasks YOUR_TASK_ARN. The lastStatus should say RUNNING.
  • Check that your security group allows inbound TCP on port 3000 from 0.0.0.0/0.

help me troubleshoot why my ECS Fargate task is not accessible

✔️ Awesome, I've got everything!

Great. Double check you have saved task-definition.json in your project folder.

ⓧ I'd like to double check the full code

Here is the complete task-definition.json file for reference:

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

Your containerized Express API is now live on the internet. Anyone with the public IP can hit your health check and greeting endpoints. Next up, you will learn how to cleanly tear down all these resources when you are done.

Clean Up Your AWS Resources

Your API is live on ECS Fargate, accessible from anywhere with an internet connection.

Fargate charges per-second for running tasks, so leaving your service running will accumulate costs. In this step, you will tear down the AWS resources you created to stop all charges immediately.

In this step, get ready to:
  • Delete the ECS service.
  • Delete the ECS cluster.
  • Delete the ECR repository and all its images.
Stop and delete the ECS service

Before you can delete a service, ECS requires that it has zero running tasks. You will first scale the service down to zero, then delete it.

  • Scale the service down to zero running tasks by running this command:
aws ecs update-service --cluster hello-ecs-cluster --service hello-ecs-service --desired-count 0

What does this command do?

The --desired-count 0 flag tells ECS to stop maintaining any running tasks for this service. ECS will gracefully shut down the running container within a few seconds.

  • Wait about 30 seconds for the task to drain, then delete the service by running:
aws ecs delete-service --cluster hello-ecs-cluster --service hello-ecs-service
  • You should see a JSON response with "status": "DRAINING" confirming the service is being deleted.

Seeing an error about active tasks?

If you get an error saying the service still has running tasks, wait another 30 seconds and try the delete command again. The task needs time to fully stop after you set the count to zero.

Alternatively, you can add the --force flag to delete the service even if tasks are still draining.

help me delete my ECS service that still has running tasks

Delete the ECS cluster

With the service gone, the cluster is now empty and can be safely deleted.

  • Delete the ECS cluster by running:
aws ecs delete-cluster --cluster hello-ecs-cluster
  • You should see a JSON response with "status": "INACTIVE" confirming the cluster has been deleted.

Cluster won't delete?

If you see an error about active services, go back and make sure the delete-service command from the previous substep completed successfully. You can verify by running aws ecs list-services --cluster hello-ecs-cluster to check that no services remain.

help me troubleshoot why my ECS cluster won't delete

Delete the ECR repository and images

The ECR repository stores your Docker image in AWS. Deleting it removes both the repository and all images inside it.

  • Delete the ECR repository and all its images by running:
aws ecr delete-repository --repository-name hello-ecs --region us-east-1 --force

What does the --force flag do here?

The --force flag tells ECR to delete the repository even if it still contains images. Without it, you would need to manually delete each image first.

  • You should see a JSON response containing the repository details, confirming it has been deleted.

Repository not found?

Make sure you are using the correct region. If you created the repository in a different region, replace us-east-1 with your actual region.

help me find and delete my ECR repository

All AWS resources have been cleaned up. No further Fargate, ECR, or ECS charges will accrue on your account from this project.

Secret mission

Multi-Container ECS Task with a Logging Sidecar

In production, containers rarely run alone. Add a second container to your ECS task that acts as a logging sidecar, tailing request logs from a shared volume. This is how real teams handle observability in containerized systems.

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 ECS Fargate, which charges per-second for running tasks. ECR storage is free for 500 MB in the first year.

Cost warning

Your ECS Fargate task costs approximately $0.004/hour while running. If you leave the service active 24/7, that adds up to about $3/month. Delete or pause your resources now to avoid unexpected charges.

Resources you used:

  • ECS Fargate service and task (charges per-second for vCPU and memory).
  • ECS cluster (no charge for the cluster itself).
  • ECR private repository (500 MB free for first year, then $0.10/GB/month).
  • CloudWatch log group (created by the task definition).
  • Docker Hub image (free for public repositories).
  • Local project files on your machine.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your API accessible via its public IP.

  • Your Fargate task continues running and accumulating charges (~$0.004/hour).
  • Your ECR image stays stored (free under 500 MB for the first year).
  • Your Docker Hub image remains in your public repository at no cost.
  • Node.js and AWS CLI remain installed on your machine.

Pause - I'll come back to this later

Shut down the running task to stop Fargate charges. Your images and cluster remain so you can redeploy later.

  • Scale the service to zero tasks by running:
  • This stops all Fargate compute charges immediately. Your ECR image stays stored (free under 500 MB).
aws ecs update-service --cluster hello-ecs-cluster --service hello-ecs-service --desired-count 0
  • To resume later, set the desired count back to 1.

Delete - I don't want to use this again

Remove all project resources and stop all charges immediately. This is irreversible.

  • Scale the service to zero and delete it by running these commands:
aws ecs update-service --cluster hello-ecs-cluster --service hello-ecs-service --desired-count 0
aws ecs delete-service --cluster hello-ecs-cluster --service hello-ecs-service --force
  • Delete the ECS cluster by running:
aws ecs delete-cluster --cluster hello-ecs-cluster
  • Delete the ECR repository and all its images by running:
aws ecr delete-repository --repository-name hello-ecs --region us-east-1 --force
  • Delete the CloudWatch log group created by the task definition by running:
aws logs delete-log-group --log-group-name /ecs/hello-ecs
  • Remove the Docker Hub image by logging in at hub.docker.com, navigating to your repository, clicking Settings, then clicking Delete Repository.
  • Remove local Docker images by running:
docker rmi hello-ecs
  • Delete the local project folder by running:

macOS

rm -rf ~/Desktop/hello-ecs

Windows

Remove-Item -Recurse -Force ~\Desktop\hello-ecs

That's a Wrap!

That's a Wrap!

Nice work. You just shipped a containerized API to the cloud using the real production pipeline that teams use every day.

You've learned how to:

  • Build and containerize a Node.js Express API using Docker.
  • Push the same image to both Docker Hub (public registry) and Amazon ECR (private registry).
  • Deploy a running container on ECS Fargate with a public IP accessible from anywhere on the internet.
  • Secret Mission: Add a multi-container logging sidecar to learn ECS task composition.

Ready to quiz yourself?