Deploy a Containerized App to AWS

Package a web app in Docker and deploy it live on AWS with ECS Express Mode.

Introduction

30 Second Summary

Every app you build eventually needs to leave your laptop and go live. The challenge is making sure it works the same way in the cloud as it does on your machine.

In this project, you will package a web app inside a Docker container and deploy it to AWS using ECS Express Mode. By the end, you will have a live public URL that anyone can visit.

What You'll Build

You will type your app's live URL into a browser and watch it load from the cloud, complete with real-time API data, automatic HTTPS, and infrastructure that scales on its own.

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

  • A live public URL where anyone can visit your app running on AWS, served with automatic HTTPS.
  • The ability to run the same Docker container on your laptop and in the cloud with identical behaviour, proving that containers guarantee consistency.
  • A deployment backed by automatic load balancing and auto-scaling, all provisioned by a single CLI command.
  • Secret Mission: Update your app, rebuild the container, and redeploy to practice the real-world container update workflow.

Are there any prerequisites?

You need an AWS account and Docker Desktop installed on your machine before starting. You do not need Node.js installed locally because Docker handles the runtime for you.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Tools

The AWS CLI lets you communicate with AWS services directly from your terminal.

Docker Desktop is already on your machine, but without the AWS CLI installed and configured with your credentials, none of the deployment commands in this project will work. In this step, you'll get the CLI ready so your terminal is fully connected to your AWS account.

In this step, get ready to:
  • Install and verify AWS CLI v2.
  • Configure your AWS credentials.
  • Verify Docker Desktop is running.
Install AWS CLI v2

The AWS CLI is a command-line tool that lets you manage AWS resources without leaving your terminal. You need version 2 for the ECS commands used later in this project.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check if the AWS CLI is already installed by running this command:
aws --version

Look at the output and pick the tab that matches what you see.

✔️ I see version 2.x

Your output looks something like this:

aws-cli/2.27.41 Python/3.11.6 ...

The AWS CLI v2 is already installed. Skip ahead to the next substep where you configure your credentials.

ⓧ I see version 1.x

Your output starts with something like aws-cli/1.x.x. You need version 2 for this project.

Version 2 replaces version 1 when you install it. Follow the instructions in the next tab (Command not found) to install version 2 using your platform's installer.

ⓧ Command not found

The AWS CLI is not installed yet. Follow the instructions below for your operating system.

macOS

  • Download and install the AWS CLI by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
What do these commands do?

The first command downloads the official AWS CLI installer package. The second command runs the macOS installer, which places the CLI in /usr/local/aws-cli and creates a symlink at /usr/local/bin/aws.

💡 What is sudo?

Your Mac will ask for your password after the sudo command. This grants the installer permission to write to system folders. Type your Mac login password and press Enter (you won't see characters appear as you type).

  • Confirm the installation succeeded by running:
aws --version

You should see output starting with aws-cli/2. followed by version details.

Windows

  • Download the MSI installer from this URL in your browser:
https://awscli.amazonaws.com/AWSCLIV2.msi
  • Run the downloaded AWSCLIV2.msi file and follow the on-screen prompts to complete the installation.
  • Close your PowerShell window completely, then reopen it (press the Windows key, type PowerShell, press Enter).
  • Confirm the installation succeeded by running:
aws --version

You should see output starting with aws-cli/2. followed by version details.

Still seeing command not found?

  • On Windows, make sure you closed and reopened your terminal after installing. The new PATH entry only loads in fresh terminal sessions.
  • On macOS, try opening a new terminal tab or window. If it still fails, check that /usr/local/bin is in your PATH by running echo $PATH.

Help me troubleshoot why aws --version is not working after installing the AWS CLI

Configure your AWS credentials

The AWS CLI is installed, but it does not know which AWS account to talk to yet. You need to give it your access credentials so it can authenticate your commands.

  • Start the configuration wizard by running:
aws configure

What will aws configure ask me?

The command prompts you for four values, one at a time. Type each value and press Enter to move to the next prompt.

  • Enter your AWS Access Key ID when prompted.
  • Enter your AWS Secret Access Key when prompted.
  • Enter us-east-1 for the default region.
  • Enter json for the default output format.

Where to find your Access Keys

In the AWS Management Console, click your username in the top-right corner, then select Security credentials. Scroll down to the Access keys section and click Create access key. Copy both values before closing the modal.

Before you verify, what do you think this next command will return if your credentials are correct?

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

You should see a JSON response containing your Account ID, UserId, and Arn. This confirms your terminal is authenticated with your AWS account.

Seeing an authentication error?

  • Double-check that you pasted your Access Key ID and Secret Access Key without extra spaces.
  • Make sure you copied both values from the same access key pair.
  • If your key was just created, wait 30 seconds and try again.

Help me fix my AWS CLI authentication error after running aws sts get-caller-identity

Verify Docker Desktop is running

Docker Desktop is already installed on your machine. You need to confirm it is actively running, because Docker commands only work when the Docker daemon is up.

  • Make sure Docker Desktop is open. If it is not running, press Cmd+Space (macOS) or the Windows key (Windows), type Docker Desktop, and press Enter.
  • Verify Docker is running by executing this command:
docker --version

You should see a version string like Docker version 27.x.x.

  • Confirm the Docker daemon is responding by running:
docker ps

You should see an empty table with column headers (CONTAINER ID, IMAGE, etc.) and no running containers listed. This means Docker is running and ready.

Seeing a connection error from docker ps?

  • Make sure Docker Desktop is fully started. Look for the Docker whale icon in your system tray or menu bar. It may take a minute after launch to become ready.
  • On Windows, make sure you are running PowerShell (not Command Prompt) and that Docker Desktop is set to use the default Linux containers mode.

Help me fix docker ps connection errors when Docker Desktop is installed

Your tools are locked in. Both the AWS CLI and Docker are ready to go. Next up, you'll build your web app and package it inside a Docker container.

Build Your App in a Docker Container

Your AWS CLI is configured and Docker Desktop is running. Now you need an app to deploy.

The problem is that apps depend on specific runtimes and libraries. Without Docker, anyone who wants to run your app needs to install the exact same Node.js version and dependencies. Docker packages everything together so the app runs identically everywhere.

In this step, get ready to:
  • Create a full-stack web app with an Express.js backend and HTML/CSS/JS frontend.
  • Write a Dockerfile that packages the app into a portable container image.
  • Build and run the container locally to verify it works before deploying.
Create the project folder and app files

You need a project folder on your Desktop to hold the app code. You will create the folder structure, then add the backend and frontend files.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder, move into it, and create the public subfolder by running these commands:
mkdir deploy-app
cd deploy-app
mkdir public
  • Open the deploy-app folder in VS Code.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the deploy-app folder on your Desktop, then click Open.

You should see the deploy-app folder name in the VS Code sidebar with a public subfolder inside it.

  • Create a new file called server.js in the root of deploy-app and paste the following code:
const express = require('express');
const path = require('path');

const app = express();
const port = 3000;

app.use(express.static(path.join(__dirname, 'public')));

app.get('/api/status', (req, res) => {
  res.json({
    status: 'running',
    timestamp: new Date().toISOString(),
    message: 'Hello from a Docker container deployed on AWS!',
    environment: process.env.APP_ENV || 'development'
  });
});

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

What does this code do?

  • express.static serves your frontend files (HTML, CSS, JS) from the public folder.
  • The /api/status route returns a JSON response with the app's status, timestamp, and a message. This is the data your frontend will display.
  • The server listens on port 3000. Binding to 0.0.0.0 makes it accessible from outside the container (important for Docker).
  • Create a new file called package.json in the root of deploy-app and paste the following:
{
  "name": "deploy-app",
  "version": "1.0.0",
  "description": "A simple app for learning Docker and AWS deployment",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "5.2.1"
  }
}

What does package.json do?

This file tells Node.js which packages your app needs. When Docker runs npm install, it reads this file and downloads Express version 5.2.1. Without it, the container would not know what to install.

  • Save both files.
  • Confirm they exist by running this in your terminal:
ls

You should see package.json, public, and server.js listed in the output.

  • Now create a file called index.html inside the public folder and paste this code:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Deployed with Docker + AWS</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>Deployed with Docker + AWS</h1>
    <p class="subtitle">This app is running inside a container on AWS ECS</p>
    <div id="status" class="status-box">
      <p>Loading status from API...</p>
    </div>
    <button id="refresh-btn">Refresh Status</button>
  </div>
  <script src="script.js"></script>
</body>
</html>

What does this HTML do?

This is the page your users see. It has a title, a status box that shows data from your API, and a refresh button. The script.js file (which you will create next) fetches data from your backend and fills in the status box.

  • Create a file called style.css inside the public folder. Paste the first section of styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #1a1a2e;
  color: #eee;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.container {
  text-align: center;
  padding: 2rem;
  max-width: 600px;
}
  • Continue adding the component styles below the code you just pasted:
h1 {
  font-size: 2rem;
  margin-bottom: 0.5rem;
  color: #00d4aa;
}

.subtitle {
  color: #aaa;
  margin-bottom: 2rem;
}

.status-box {
  background: #16213e;
  border: 1px solid #0f3460;
  border-radius: 8px;
  padding: 1.5rem;
  margin-bottom: 1.5rem;
  text-align: left;
}

.status-box p {
  margin-bottom: 0.5rem;
  font-size: 0.95rem;
}

.status-box .label {
  color: #00d4aa;
  font-weight: bold;
}
  • Complete the stylesheet by pasting the button styles below everything else:
button {
  background: #00d4aa;
  color: #1a1a2e;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 6px;
  font-size: 1rem;
  font-weight: bold;
  cursor: pointer;
}

button:hover {
  background: #00b894;
}

What do these styles do?

The CSS creates a dark-themed page with centered content. The status box has a darker background and subtle border to separate it from the main area. The green accent color (#00d4aa) appears in the heading, status labels, and button.

  • Save style.css.
  • Confirm your public files exist by running:
ls public

You should see index.html and style.css listed.

  • Create the last frontend file: script.js inside the public folder. Paste the following:
// Fetch status data from the backend API
async function fetchStatus() {
  const statusBox = document.getElementById('status');
  try {
    const response = await fetch('/api/status');
    const data = await response.json();
    statusBox.innerHTML = `
      <p><span class="label">Status:</span> ${data.status}</p>
      <p><span class="label">Timestamp:</span> ${data.timestamp}</p>
      <p><span class="label">Message:</span> ${data.message}</p>
      <p><span class="label">Environment:</span> ${data.environment}</p>
    `;
  } catch (error) {
    statusBox.innerHTML = `<p>Error fetching status: ${error.message}</p>`;
  }
}

// Wire up the refresh button and fetch on page load
document.getElementById('refresh-btn').addEventListener('click', fetchStatus);

fetchStatus();

What does this script do?

  • fetchStatus() calls your backend's /api/status endpoint and injects the returned JSON data into the page.
  • The event listener on the button lets users manually refresh the data.
  • fetchStatus() is also called immediately on page load so data appears without clicking.
  • Save script.js.
  • Run ls public again. You should now see index.html, script.js, and style.css.

Missing a file?

Make sure each file is inside the correct folder. server.js and package.json go in the root deploy-app folder. index.html, style.css, and script.js go inside public/.

Check the VS Code sidebar. The folder structure should be deploy-app > public > (3 files) with server.js and package.json at the top level.

help me find where my files went wrong

Create the Dockerfile

A Dockerfile is a recipe that tells Docker how to build your app into a portable image. It defines the base operating system, copies your code in, installs dependencies, and specifies how to start the app.

  • Create a file called Dockerfile (no file extension) in the root of deploy-app and paste the following:
# Use a lightweight Node.js base image
FROM node:24-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package.json first for dependency caching
COPY package.json ./

# Install production dependencies
RUN npm install --production

# Copy the application code
COPY server.js ./
COPY public ./public

# Document which port the app uses
EXPOSE 3000

# Start the server when the container runs
CMD ["node", "server.js"]

What does each Dockerfile instruction do?

  • FROM node:24-alpine starts from a minimal Linux image with Node.js pre-installed. Alpine is only ~55 MB compared to ~390 MB for the full image.
  • WORKDIR /app sets the directory where all following commands run inside the container.
  • COPY package.json is copied first so Docker can cache the npm install step. If your code changes but dependencies do not, Docker skips reinstalling.
  • RUN npm install --production installs Express inside the container. You do not need Node.js on your own machine.
  • EXPOSE 3000 documents which port the app uses. It does not publish the port. You do that at run time.
  • CMD tells Docker what command to run when the container starts.
  • Save Dockerfile.

No file extension on Dockerfile

The file must be named exactly Dockerfile with a capital D and no extension like .txt. If VS Code added an extension, rename it in the sidebar.

Build and run the Docker container

Your Dockerfile is your recipe. Now you will build it into an image (a snapshot of the app and all its dependencies) and run that image as a live container.

  • Build your Docker image by running this command:
docker build -t deploy-app .

What does this command do?

  • docker build reads your Dockerfile and executes each instruction to create an image.
  • -t deploy-app names (tags) the image deploy-app so you can reference it later.
  • . tells Docker to look for the Dockerfile in the current directory.

You will see Docker download the node:24-alpine base image (first time only), install your dependencies, and copy your files. The final line shows the image was built successfully.

Build failed?

If you see an error about the Dockerfile not being found, make sure your terminal is inside the deploy-app folder. Run ls and confirm you see Dockerfile in the output.

If you see a connection error, make sure Docker Desktop is running. Check your system tray or taskbar for the Docker whale icon.

help me fix my Docker build error

Before you run the container, what do you expect to see when you visit http://localhost:3000 afterwards?

  • Start a container from your image by running:
docker run -p 3000:3000 deploy-app

What does -p 3000:3000 mean?

The -p flag publishes a container port to your machine. The format is host:container. This maps port 3000 on your machine to port 3000 inside the container, so your browser can reach the app.

Your terminal will show Server running on port 3000 and stay attached to the container (this is expected).

  • Open your browser and visit http://localhost:3000.

You should see a dark-themed page titled "Deployed with Docker + AWS" with a status box showing live data from your API: a status of running, a timestamp, and the message Hello from a Docker container deployed on AWS!.

Page not loading?

Check that your terminal still shows Server running on port 3000. If the container exited, look for error messages above that line. A common cause is a typo in server.js.

If you see "port already in use", another process is using port 3000. Stop it or change the host port: docker run -p 3001:3000 deploy-app and visit http://localhost:3001 instead.

help me debug my Docker container

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files and can see the status data refreshing in your browser when you click the Refresh Status button.

ⓧ I'd like to double check the full code

Scroll back through this substep and confirm each file is present and matches. Your project should have these files in order:

  • server.js in the root folder (the Express server with the /api/status route).
  • package.json in the root folder (dependencies including Express 5.2.1).
  • public/index.html (the HTML page with the status box and refresh button).
  • public/style.css (all three sections: reset/body/container, components, button).
  • public/script.js (the fetchStatus function and event listener).
  • Dockerfile in the root folder (FROM, WORKDIR, COPY, RUN, EXPOSE, CMD).

Your app is running in a Docker container on your machine. Next up, you will push this container image to a cloud registry so AWS can access it and deploy it live on the internet.

Push Your Image to AWS ECR

Your Docker image is built and running perfectly on your machine. But AWS ECS (the service that will run your container in the cloud) cannot pull an image from your laptop.

You need to upload your image to a cloud-based container registry where AWS services can access it. This is the same pattern used in production: images are built locally, pushed to a registry, then pulled by deployment services.

In this step, get ready to:
  • Create a private container registry on AWS ECR.
  • Authenticate Docker to push images to your registry.
  • Tag and push your local image to the cloud.
Create an ECR repository

AWS Elastic Container Registry (ECR) is a private Docker registry hosted on AWS. You need a repository in ECR to store your container image where ECS can pull it during deployment.

  • If your container from Step 2 is still running, press Ctrl+C in its terminal to stop it.
  • Create an ECR repository by running this command:
aws ecr create-repository --repository-name deploy-app --region us-east-1

What does this command do?

  • aws ecr create-repository creates a new private image repository in your AWS account.
  • --repository-name deploy-app names the repository to match your Docker image.
  • --region us-east-1 creates it in the US East region.

You should see JSON output that includes a repositoryUri field. It looks like 123456789012.dkr.ecr.us-east-1.amazonaws.com/deploy-app.

  • Note the 12-digit number at the start of your repositoryUri. This is your AWS account ID. You will need it for the next commands.

Seeing a RepositoryAlreadyExistsException?

This means you already have a repository named deploy-app in your account. That is fine. You can continue to the next step and push your image to the existing repository.

If you see a credentials or access error, verify that aws configure has your Access Key and region set correctly.

help me fix an ECR create-repository error

Authenticate Docker to ECR

Cloud registries are private by default. Before Docker can push images to ECR, you need to authenticate with a temporary token that lasts 12 hours.

  • Authenticate Docker to your ECR registry by running this command:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com

What does this command do?

  • aws ecr get-login-password generates a temporary authentication token from AWS.
  • The | pipe passes that token directly to the docker login command.
  • --password-stdin reads the password from the pipe instead of prompting you to type it.
  • The URL at the end is your private ECR registry endpoint.

You should see Login Succeeded in your terminal.

Not seeing Login Succeeded?

Make sure you replaced the account ID placeholder with the 12-digit number from your repositoryUri output in the previous step.

Confirm Docker Desktop is running. If it is not, start it and try the command again.

help me fix my ECR Docker login

Tag and push your image to ECR

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

  • Tag your local image for ECR by running this command:
docker tag deploy-app:latest [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/deploy-app:latest
  • Push the tagged image to ECR by running this command:
docker push [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/deploy-app:latest

What do these commands do?

  • docker tag creates a new name (alias) for your existing deploy-app:latest image. The new name includes the full ECR URI so Docker knows where to send it.
  • docker push uploads all the image layers to ECR. You will see progress bars as each layer uploads.

You will see each layer being pushed with progress output. Once complete, your image is stored in the cloud.

Before you verify, what do you expect the output of a describe-images command to show? Think about what metadata ECR stores about your image.

  • Verify your image is in ECR by running this command:
aws ecr describe-images --repository-name deploy-app --region us-east-1

You should see JSON output containing an imageDetails array. Inside it, look for "imageTags": ["latest"] confirming your image is stored with the latest tag.

Not seeing your image in the output?

If the imageDetails array is empty, the push may not have completed. Re-run the docker push command and check for errors in its output.

If you see an authentication error, your login token may have expired. Run the aws ecr get-login-password pipe command again to get a fresh token, then retry the push.

help me verify my image was pushed to ECR

Your container image is now stored in the cloud where any AWS service can pull it. Next up, you will deploy it live to the internet with a single ECS command.

Deploy Live with ECS Express Mode

Your container image is sitting in ECR, but nothing is running it yet. AWS needs something to pull that image, spin up a container, and make it accessible to the world.

ECS Express Mode handles all of this with a single CLI command. It automatically provisions a Fargate task, an Application Load Balancer, HTTPS, auto-scaling, and a public URL. One command replaces what would otherwise be dozens of manual configuration steps.

In this step, get ready to:
  • Create the IAM roles that ECS Express Mode requires.
  • Deploy your container with a single CLI command.
  • Access your app live on the internet with HTTPS.
Create the IAM roles for ECS Express Mode

ECS Express Mode needs two IAM roles to operate. One lets ECS pull your container image from ECR. The other lets ECS manage infrastructure like load balancers and security groups on your behalf.

  • In VS Code, create a new file called trust-policy-tasks.json in your deploy-app folder.
  • Paste in the following trust policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

What does this trust policy do?

A trust policy tells AWS which service is allowed to "assume" (use) this role. This one says the ecs-tasks.amazonaws.com service can use the role, which lets ECS pull your container image from ECR when launching tasks.

  • Create another file called trust-policy-ecs.json in the same folder.
  • Paste in this trust policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Why two different trust policies?

The first policy trusts ecs-tasks.amazonaws.com because it handles task-level operations like pulling images. The second trusts ecs.amazonaws.com because it manages infrastructure resources like load balancers and scaling policies.

  • Save both files.
  • Confirm the files exist by running:
ls *.json

You should see package.json, trust-policy-ecs.json, and trust-policy-tasks.json listed.

  • Create the first IAM role (ecsTaskExecutionRole) by running this command:
aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document file://trust-policy-tasks.json

The command outputs JSON containing your new role's ARN. You will need this ARN shortly.

  • Attach the required policy to ecsTaskExecutionRole by running:
aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

This command returns no output on success.

What is ecsTaskExecutionRole?

This role gives ECS permission to perform task-level actions on your behalf: pulling container images from ECR, writing logs to CloudWatch, and fetching secrets. The AmazonECSTaskExecutionRolePolicy managed policy bundles all these permissions together.

  • Create the second IAM role (ecsInfrastructureRoleForExpressServices) by running:
aws iam create-role --role-name ecsInfrastructureRoleForExpressServices --assume-role-policy-document file://trust-policy-ecs.json
  • Attach its required policy by running:
aws iam attach-role-policy --role-name ecsInfrastructureRoleForExpressServices --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRoleforExpressGatewayServices

Again, no output means success.

What is ecsInfrastructureRoleForExpressServices?

This role lets ECS manage AWS infrastructure on your behalf. When Express Mode provisions a load balancer, configures security groups, and sets up auto-scaling, it uses this role's permissions to create and modify those resources.

🙋‍♀️ Seeing an EntityAlreadyExists error?

This means the role already exists in your AWS account (perhaps from a previous project). You can skip the create-role command and just run the attach-role-policy command to ensure the correct policy is attached.

Help me verify my existing IAM roles have the correct trust policy and permissions for ECS Express Mode.

Deploy your container to ECS Express Mode

Both roles are ready. Now you can deploy your app with a single command that tells ECS exactly what image to run, which port your app listens on, and which roles to use.

  • Deploy your app by running this command:
aws ecs create-express-gateway-service --primary-container image=[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/deploy-app:latest,containerPort=3000 --execution-role-arn arn:aws:iam::[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:role/ecsTaskExecutionRole --infrastructure-role-arn arn:aws:iam::[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:role/ecsInfrastructureRoleForExpressServices --monitor-resources

What does this command do?

  • --primary-container tells ECS which image to run and which port your app listens on (3000 instead of the default 80).
  • --execution-role-arn points to the role that lets ECS pull your image from ECR.
  • --infrastructure-role-arn points to the role that lets ECS create load balancers and scaling policies.
  • --monitor-resources makes the CLI continuously print provisioning status so you can watch the deployment happen in real time.

The terminal will display resource provisioning updates as ECS creates your infrastructure. This takes a few minutes. Wait for the status to show ACTIVE.

Don't close your terminal

The --monitor-resources flag keeps the terminal busy while infrastructure is provisioned. Deployment typically takes 2-5 minutes. When it finishes, the command prints JSON with your service details including the service name and status.

🙋‍♀️ Seeing an access denied error?

Make sure your AWS CLI credentials have permission to create ECS services. Verify your credentials are still valid by running aws sts get-caller-identity. If your account ID appears, credentials are fine and the issue is IAM permissions on your user account.

Help me troubleshoot access denied errors for ECS Express Mode deployment.

Access your live app

Once the deployment completes and shows ACTIVE, your app is live on the internet. ECS Express Mode generated a unique URL for your service.

Before you open it, what do you expect to see? Will it look the same as it did locally on http://localhost:3000?

  • Find the service URL in the terminal output. The format is:
https://<service-name>.ecs.us-east-1.on.aws/
  • Copy your service URL from the terminal output and paste it into your browser.

You should see your app running live on the internet. The same dark background, green heading, and status box you saw locally now appears at a public HTTPS URL.

  • Click the Refresh Status button to confirm the backend API is responding.

The API responds with the current timestamp, proving your backend is running in the cloud.

What did ECS Express Mode provision for you?

  • A Fargate task running your container with 1 vCPU and 2 GB memory.
  • An internet-facing Application Load Balancer distributing traffic across availability zones.
  • Automatic HTTPS with a managed certificate.
  • Auto-scaling based on CPU utilization.

💡 Why ECS Express Mode over EC2?

Deploying to an EC2 instance would require you to SSH in, install Node.js, configure Nginx as a reverse proxy, manage SSL certificates, and handle scaling manually. ECS Express Mode teaches the modern container deployment pattern (registry to orchestrator to auto-scaling) without requiring Linux server administration.

🙋‍♀️ App not loading at the URL?

Give it 1-2 minutes after the status shows ACTIVE. The load balancer health check needs time to confirm your container is responding. If the page still doesn't load after a few minutes, verify your container port is set to 3000 in the deploy command (not the default 80).

Help me debug why my ECS Express Mode service URL is not loading.

Your app is live on the internet with automatic HTTPS, load balancing, and auto-scaling. Next up, tackle the Secret Mission to learn how real-world container update workflows work.

Secret mission

Update and Redeploy Your App

Your app is live, but real-world deployment is never a one-time event. Professional teams push updates to production multiple times a day. Can you modify your app, rebuild the container, push it to ECR, and trigger a redeployment so your live URL shows the new version?

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 compute and an Application Load Balancer, which incur charges while running.

Cost warning

Your ECS Express Mode service costs approximately $0.06/hour (~$1.50/day) while running. This includes Fargate compute and the Application Load Balancer.

If you are done with the project, delete the service to stop all charges.

Resources you used:

  • ECS Express Mode service (Fargate compute + Application Load Balancer).
  • ECR repository (deploy-app).
  • IAM roles (ecsTaskExecutionRole and ecsInfrastructureRoleForExpressServices).
  • Local project folder (deploy-app on your Desktop).
  • Local Docker image (deploy-app:latest).

Keep everything running

No action needed. Choose this if you are still actively building on this project or want to keep your app live.

  • Your app stays live at its public URL.
  • Charges continue at approximately $0.06/hour (~$1.50/day) for Fargate compute and the ALB.
  • Your ECR repository holds your image at negligible storage cost.
  • IAM roles remain in your account at no cost.

Pause - I'll come back to this later

Delete the running ECS service to stop compute charges, but keep your image and roles so you can redeploy quickly later.

  • Delete the ECS Express Mode service to stop all compute and ALB charges by running this command:
aws ecs delete-express-gateway-service --service-arn [[SERVICE_ARN="your ECS Express Mode service ARN"]]
  • Wait for the deletion to complete. The ALB is automatically removed when the service is deleted.
  • Your ECR repository and Docker image remain intact for redeployment later.
  • Your IAM roles remain in your account at no cost.
  • Your local project files stay on your Desktop.

Delete - I don't want to use this again

Remove all project resources from AWS and your local machine. This is recommended if you are done learning and want a clean slate.

Start by deleting the ECS Express Mode service. This stops all tasks and removes the ALB automatically.

  • Delete the ECS service by running:
aws ecs delete-express-gateway-service --service-arn [[SERVICE_ARN="your ECS Express Mode service ARN"]]
  • Wait for the command to finish. The service enters a DRAINING state while tasks complete, then is permanently removed along with the ALB.
  • Delete the ECR repository and the image stored in it by running:
aws ecr delete-repository --repository-name deploy-app --force
  • Delete the IAM roles. You must detach the managed policies before deleting each role. Run these commands to remove ecsTaskExecutionRole:
aws iam detach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam delete-role --role-name ecsTaskExecutionRole
  • Run these commands to remove ecsInfrastructureRoleForExpressServices:
aws iam detach-role-policy --role-name ecsInfrastructureRoleForExpressServices --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRoleforExpressGatewayServices
aws iam delete-role --role-name ecsInfrastructureRoleForExpressServices
  • Remove the local Docker image by running:
docker rmi deploy-app:latest
  • Delete the local project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf deploy-app
  • On Windows, run:
cd ~\Desktop
Remove-Item -Recurse -Force deploy-app

That's a Wrap!

That's a Wrap!

Nice work! You just took a web app from your local machine to a live, publicly accessible URL on AWS. The pipeline you built works for any app in any framework, because Docker makes your deployment framework-agnostic.

You've learned how to:

  • Built a full-stack web app and packaged it in a Docker container, guaranteeing it runs identically on any machine.
  • Pushed the container image to AWS Elastic Container Registry (ECR), making it available for cloud services to pull and run.
  • Deployed the app live on the internet using ECS Express Mode with automatic HTTPS, load balancing, and auto-scaling, all from a single CLI command.
  • Secret Mission: Updated and redeployed the app using the real-world container update workflow that professional teams use daily.

Ready to quiz yourself?