Deploy a Containerized App to ECS

Build and deploy a containerized Flask app to AWS ECS Fargate with ECR and ALB.

Introduction

30 Second Summary

When you use an app on your phone, that app is running inside a container somewhere in the cloud. A whole system of tools keeps it alive, balanced across servers, and monitored around the clock.

In this project, you will build and deploy a containerized web application to AWS using the same infrastructure that powers production workloads. You will set up a local development workflow with hot-reload, push your container image to a private registry, and launch it as a highly available service behind a load balancer.

What You'll Build

A fully deployed containerized application on AWS ECS Fargate, developed locally with Docker Compose and accessible to the world through an Application Load Balancer.

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

  • A Docker Compose local development environment with hot-reload and environment variable separation for fast iteration.
  • A production-optimized Docker image pushed to a private Amazon ECR repository with vulnerability scanning enabled.
  • A running ECS Fargate service with container health checks, fronted by an ALB and monitored through CloudWatch Logs.
  • Secret Mission: Add a CloudWatch alarm that alerts you when your service has zero healthy targets.

Are there any prerequisites?

You will need an AWS account, Docker Desktop installed on your machine, the AWS CLI installed, and VS Code. Step 1 walks you through configuring everything from scratch.

Set Up the Local Development Environment

Before deploying containers to the cloud, professional teams build and test locally first. A local development environment lets you iterate on your application code in seconds instead of waiting for cloud deployments.

In this step, you'll configure your AWS CLI credentials, write a Flask application with a multi-stage Dockerfile, and set up Docker Compose for hot-reload local development.

In this step, get ready to:
  • Configure AWS CLI with your credentials and verify access.
  • Create a Flask application with a multi-stage Dockerfile.
  • Test the local development workflow with Docker Compose hot-reload.
Configure AWS CLI credentials and create the project structure

The AWS CLI needs your credentials to interact with your AWS account on your behalf. You'll also create the project folder that holds all your application and configuration files.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Configure the AWS CLI by running this command:
aws configure

What does this command do?

This launches an interactive prompt that asks for four values. Enter each one when prompted:

  • AWS Access Key ID and AWS Secret Access Key authenticate your CLI session with your AWS account.
  • Default region name: enter us-east-1.
  • Default output format: enter json.

Finding your Access Key ID and Secret Access Key

If you do not already have an access key, sign into the AWS Console, navigate to IAM, select your user, go to the Security credentials tab, and click Create access key. Copy the values before closing the dialog.

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

You should see a JSON response with your AWS account ID, user ARN, and user ID.

Seeing an error about credentials?

Double check that you entered the Access Key ID and Secret Access Key correctly. Run aws configure again to re-enter them.

If you see a permissions error, make sure your IAM user has the AdministratorAccess policy attached or sufficient permissions for ECS, ECR, and EC2.

help me troubleshoot my AWS CLI credentials

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ecs-demo-app
cd ecs-demo-app
  • 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.
  • Navigate to your Desktop, select the ecs-demo-app folder, and click Open.
Create the Flask application with a multi-stage Dockerfile

Your application is a lightweight Flask web server with a health check endpoint. The multi-stage Dockerfile separates local development tooling from a minimal production image.

  • Create a new file in VS Code by clicking File then New File.
  • Save it immediately as requirements.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Add the following content to requirements.txt:
flask==3.1.3
  • Create another new file and save it as app.py.
  • Add the following code to app.py:
import os
from flask import Flask, jsonify

# Create the Flask application instance
app = Flask(__name__)


# Main route - returns app status and environment info
@app.route("/")
def home():
    environment = os.environ.get("ENVIRONMENT", "development")
    return jsonify({
        "message": "Hello from ECS Fargate!",
        "environment": environment,
        "status": "running"
    })


# Health check endpoint - used by ECS and the load balancer
@app.route("/health")
def health():
    return jsonify({"status": "healthy"}), 200


# Run the app with debug mode in development
if __name__ == "__main__":
    debug_mode = os.environ.get("ENVIRONMENT", "development") == "development"
    app.run(host="0.0.0.0", port=5000, debug=debug_mode)

What does this code do?

  • The / route returns a JSON greeting that includes the current environment name. This lets you confirm whether you're running in development or production.
  • The /health route returns a simple healthy status. ECS uses this endpoint to check if your container is alive.
  • The debug_mode variable enables Flask's auto-reload in development but disables it in production for stability.
  • Save app.py.
  • Create another new file and save it as Dockerfile (no file extension).
  • Add the following content to your Dockerfile:
# Stage 1: Builder - used for local development (includes shell and tools)
FROM python:3.11-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

# Stage 2: Production - minimal image for deployment
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

Why two stages?

The builder stage is what Docker Compose targets during local development. It includes a full shell and debugging tools.

The second stage is the production image. When you run docker build without specifying a target, Docker builds the final stage by default. This keeps your deployed image minimal and secure.

  • Save Dockerfile.
  • Create one more new file and save it as .dockerignore.
  • Add the following content to .dockerignore:
__pycache__
*.pyc
.env
.git

What does .dockerignore do?

This file tells Docker which files to exclude from the build context. Keeping .env out prevents your local environment variables from leaking into the image. Excluding __pycache__ and .git reduces the image size and build time.

  • Save .dockerignore.
  • Confirm all your files exist by opening the integrated terminal in VS Code with Ctrl+` and running:
ls

You should see app.py, requirements.txt, Dockerfile, and .dockerignore in the output.

Missing a file?

Make sure you saved each file inside the ecs-demo-app folder. If a file ended up in the wrong location, drag it into the correct folder using the VS Code Explorer sidebar.

If .dockerignore does not appear in ls output, try ls -a to show hidden files (files starting with a dot).

help me find my missing project files

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

import os
from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/")
def home():
    environment = os.environ.get("ENVIRONMENT", "development")
    return jsonify({
        "message": "Hello from ECS Fargate!",
        "environment": environment,
        "status": "running"
    })


@app.route("/health")
def health():
    return jsonify({"status": "healthy"}), 200


if __name__ == "__main__":
    debug_mode = os.environ.get("ENVIRONMENT", "development") == "development"
    app.run(host="0.0.0.0", port=5000, debug=debug_mode)
flask==3.1.3
# Stage 1: Builder - used for local development (includes shell and tools)
FROM python:3.11-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

# Stage 2: Production - minimal image for deployment
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]
__pycache__
*.pyc
.env
.git
Create Docker Compose for local development and test

Docker Compose orchestrates your local development environment. The develop.watch feature automatically syncs file changes into the running container and restarts it, giving you hot-reload without manual rebuilds.

  • Create a new file and save it as compose.yaml.
  • Add the following content to compose.yaml:
services:
  app:
    build:
      context: .
      # Use the builder stage for local development
      target: builder
    ports:
      - "5000:5000"
    # Read environment variables from the .env file
    env_file:
      - .env
    # Watch for file changes and sync them into the container
    develop:
      watch:
        - action: sync+restart
          path: .
          target: /app
          ignore:
            - __pycache__/
  • Create one more file and save it as .env.
  • Add the following content to .env:
ENVIRONMENT=development
APP_PORT=5000

What does this configuration do?

  • The target: builder line tells Compose to build only the first stage of your Dockerfile, which includes development tools.
  • The sync+restart action watches your project directory, copies changed files into the container at /app, and restarts the process so Flask picks up the changes.
  • The .env file keeps environment-specific values separate from your code. Locally you use development. In ECS, you'll set production directly in the task definition.
  • Save both compose.yaml and .env.

Why Docker Compose for local development?

Using Compose locally establishes the same container-based workflow you'll use in production. Your app runs in an identical environment whether it's on your laptop or on ECS Fargate.

This pattern also scales. In larger projects, you can add databases, caches, and message queues to the same Compose file and run the full stack with a single command.

  • Start the local development environment by running this command in the VS Code integrated terminal:
docker compose up --watch

What should I see?

Docker builds the image from your Dockerfile's builder stage, installs Flask, and starts the container. After a moment you should see output indicating the Flask server is running on port 5000.

The --watch flag activates Compose Watch so file changes are automatically synced and the container restarts.

  • Open your browser and navigate to http://localhost:5000.

You should see a JSON response containing "message": "Hello from ECS Fargate!" and "environment": "development".

  • Test hot-reload by opening app.py in VS Code and changing the message string "Hello from ECS Fargate!" to something else (for example, "Hello from local dev!").
  • Save app.py.
  • Refresh your browser at http://localhost:5000.

You should see your updated message in the JSON response. Compose Watch detected the file change, synced it into the container, and restarted the process automatically.

  • Change the message back to "Hello from ECS Fargate!" and save app.py so your code matches the original.

App not responding at localhost:5000?

Make sure Docker Desktop is running. If you see a connection refused error, check the terminal for build errors. A common cause is a typo in requirements.txt or compose.yaml.

If the container starts but hot-reload does not work, confirm your Docker Desktop version includes Compose v2.22.0 or later. Update Docker Desktop if needed.

help me debug my Docker Compose setup

  • Stop the running containers by pressing Ctrl+C in the terminal, then run:
docker compose down

This stops and removes the containers so they don't keep running in the background.

✔️ Awesome, I've got everything!

Great. Make sure all files are saved and docker compose down completed successfully before moving on.

ⓧ I'd like to double check the full code

services:
  app:
    build:
      context: .
      target: builder
    ports:
      - "5000:5000"
    env_file:
      - .env
    develop:
      watch:
        - action: sync+restart
          path: .
          target: /app
          ignore:
            - __pycache__/
ENVIRONMENT=development
APP_PORT=5000

Your local development environment is working. Next up, you'll build the production image and push it to Amazon ECR so it's ready for deployment.

Build the Production Image and Push to ECR

In the previous step, you set up a local development environment with Docker Compose and confirmed your Flask app runs correctly.

Now it is time to package your app for the cloud. You will build a production-optimized Docker image, create a private repository on Amazon ECR (Elastic Container Registry), and push your image there so ECS can pull it during deployment.

In this step, get ready to:
  • Build and test the production Docker image locally.
  • Create a private ECR repository with vulnerability scanning enabled.
  • Authenticate Docker to ECR, then tag and push your image.
Build the production image and test it

Your multi-stage Dockerfile has two stages. Docker Compose targets the builder stage for local development, but when you run docker build without a --target flag, Docker builds the final (last) stage by default. That final stage is your production image.

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

What does this command do?

The -t ecs-demo-app flag tags the resulting image with the name ecs-demo-app. The . sets the build context to your current directory.

Because you are not specifying --target builder, Docker processes both stages and produces the final stage (Stage 2: Production) as the output image.

  • You should see Docker downloading the python:3.11-slim base image, installing dependencies, and finishing with a success message that includes the image ID.
  • Test the production image locally by running:
docker run -p 5000:5000 -e ENVIRONMENT=production ecs-demo-app

What does this command do?

The -p 5000:5000 flag maps port 5000 on your machine to port 5000 inside the container. The -e ENVIRONMENT=production flag sets the environment variable directly (instead of reading from .env).

This simulates how ECS will run your container in the cloud, with the ENVIRONMENT variable set to production.

  • Open http://localhost:5000 in your browser.
  • You should see a JSON response with "environment": "production" and "message": "Hello from ECS Fargate!".
  • Return to your terminal and press Ctrl+C to stop the container.

Not seeing the JSON response?

  • If you see "port is already allocated", make sure you ran docker compose down at the end of Step 1. Only one process can use port 5000 at a time.
  • If the build failed with a "no such file or directory" error, confirm you are running the command from inside the ecs-demo-app folder where your Dockerfile lives.
  • Still stuck? Help me debug my Docker build or run error.
Create an ECR repository with image scanning

Your production image works locally. Next, you need somewhere in AWS to store it so ECS can pull it during deployment.

  • Create a private ECR repository with vulnerability scanning enabled by running:
aws ecr create-repository --repository-name ecs-demo-app --image-scanning-configuration scanOnPush=true --region us-east-1

What does this command do?

  • --repository-name ecs-demo-app names your repository. This is where your Docker images live in ECR.
  • --image-scanning-configuration scanOnPush=true tells ECR to automatically scan every image you push for known security vulnerabilities.
  • --region us-east-1 creates the repository in the same region your ECS cluster will use.
  • In the JSON output, find the repositoryUri field. It looks like 123456789012.dkr.ecr.us-east-1.amazonaws.com/ecs-demo-app.
  • Copy this value and save it somewhere (a text file or sticky note). You will use it in the next commands.

What is a repositoryUri?

The repositoryUri is the full address Docker uses to push and pull images from your private ECR repository. The first part is your AWS account ID followed by the ECR registry domain, and the last part is the repository name.

Seeing a RepositoryAlreadyExistsException?

This means a repository named ecs-demo-app already exists in your account. You can safely continue. Retrieve the URI by running:

aws ecr describe-repositories --repository-names ecs-demo-app --region us-east-1

Help me resolve my ECR repository creation error.

Authenticate, tag, and push the image to ECR

ECR is a private registry. Before you can push images, Docker needs to authenticate with your AWS credentials.

  • Log Docker in 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="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com

What does this command do?

This is two commands joined by a pipe (|). The first command (aws ecr get-login-password) fetches a temporary authentication token from AWS. The pipe feeds that token into docker login, which authenticates Docker to your private ECR registry.

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

  • You should see Login Succeeded in the output.
  • Tag your local image with the ECR repository URI by running:
docker tag ecs-demo-app:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/ecs-demo-app:latest

Why tag the image?

Docker needs to know where to push the image. Tagging attaches your ECR repository URI to the local image, so docker push knows the destination. The :latest suffix is the image tag that identifies this version.

  • Push the tagged image to ECR by running:
docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/ecs-demo-app:latest
  • You should see Docker uploading layers to ECR. This may take a minute depending on your internet speed.
  • Once the push completes, open the ECR Console in your browser at https://console.aws.amazon.com/ecr.
  • Click on the ecs-demo-app repository.
  • You should see your image listed with the latest tag.
  • Click the image tag, then check the Vulnerabilities tab to review the scan findings. Since you enabled scanOnPush, ECR automatically scanned your image for known CVEs.

Push failed with "no basic auth credentials"?

  • This means your Docker login expired or did not succeed. Re-run the aws ecr get-login-password command above and confirm you see Login Succeeded.
  • Double-check that your account ID in the tag command matches the one in the repositoryUri from the create-repository output.
  • Help me debug my ECR push error.

Your production image is now stored securely in ECR with vulnerability scanning confirmed. Next up, you will create the ECS cluster and task definition that tells AWS how to run your container.

Create the ECS Cluster and Task Definition

Your Docker image is now safely stored in Amazon ECR with vulnerability scanning enabled. The next step is telling AWS how to actually run that image as a live container.

ECS (Elastic Container Service) is AWS's container orchestration platform. You will create a cluster to group your resources, then register a task definition that describes exactly how your container should run. The task definition includes a health check so ECS can automatically restart unhealthy containers.

In this step, get ready to:
  • Create an ECS cluster using AWS Fargate infrastructure.
  • Register a task definition with a container health check, environment variables, and CloudWatch logging.
Create an ECS cluster

An ECS cluster is a logical grouping of tasks and services. It defines what infrastructure is available to run your containers.

  • Open the ECS Console by navigating to https://console.aws.amazon.com/ecs/v2 in your browser.
  • In the left navigation panel, click Clusters.
  • Click Create cluster.
  • Enter demo-cluster in the Cluster name field.
  • Leave the infrastructure setting as AWS Fargate (serverless). This is selected by default.
  • Click Create.

What is AWS Fargate?

Fargate is a serverless compute engine for containers. Instead of managing EC2 instances yourself, Fargate allocates exactly the right amount of compute for each task. You only pay for the CPU and memory your containers use.

Wait a few seconds for the cluster to finish creating. You should see a green banner confirming the cluster was created successfully.

Cluster creation failed?

Check that your AWS region is set to us-east-1 in the top-right corner of the console. If you see a permissions error, confirm your IAM user has the necessary ECS permissions.

Help me troubleshoot my ECS cluster creation error

Create a task definition with container health check

A task definition is like a blueprint for your container. It tells ECS which image to use, how much CPU and memory to allocate, which ports to expose, and how to check the container's health.

  • In the left navigation panel, click Task definitions.
  • Click Create new task definition.

You will see a form with multiple sections. Start with the top-level task settings.

  • Enter demo-app-task in the Task definition family field.
  • Under Infrastructure requirements, confirm that AWS Fargate is selected as the launch type.
  • Set CPU to .5 vCPU.
  • Set Memory to 1 GB.
  • For Task execution role, select ecsTaskExecutionRole. If it does not appear in the dropdown, select Create new role and ECS will create it automatically.

What is the ecsTaskExecutionRole?

This IAM role gives ECS permission to pull your container image from ECR and write logs to CloudWatch. Without it, your tasks cannot start. The ECS Console creates this role automatically on first use if it does not exist.

Now configure the container that runs inside this task.

  • Scroll down to the Container - 1 section.
  • Enter demo-app in the Name field.
  • In the Image URI field, paste your ECR repository URI with the :latest tag. It looks like: your-account-id.dkr.ecr.us-east-1.amazonaws.com/ecs-demo-app:latest.
  • Under Port mappings, enter 5000 for Container port and select TCP as the protocol.

Next, add an environment variable so the container knows it is running in production.

  • Expand the Environment variables section.
  • Click Add environment variable.
  • Enter ENVIRONMENT as the Key.
  • Enter production as the Value.

Now configure the health check so ECS knows when your container is healthy.

  • Expand the HealthCheck section (under the container configuration).
  • In the Command field, enter:
CMD-SHELL, curl -f http://localhost:5000/health || exit 1
  • Set Interval to 30.
  • Set Timeout to 5.
  • Set Retries to 3.
  • Set Start period to 10.

What does this health check do?

Every 30 seconds, ECS runs curl inside the container to hit the /health endpoint you built in app.py. If the endpoint returns a success response, the container is marked HEALTHY.

If it fails 3 times in a row, ECS marks the container as UNHEALTHY and automatically stops and replaces it. The 10-second start period gives the container time to boot up before health checks begin counting failures.

Finally, configure logging so container output streams to CloudWatch.

  • Scroll down to the Log collection section.
  • Confirm that Use log collection is turned on. The awslogs log driver is selected by default.
  • Change the awslogs-group value to /ecs/demo-app-task.
  • Confirm awslogs-region is set to us-east-1.
  • Set awslogs-stream-prefix to ecs.

Why do we need a stream prefix?

The stream prefix groups log streams by service in CloudWatch. Without it, logs from different containers would be harder to find and filter. The ECS Console also requires a stream prefix to display logs in its built-in log viewer.

  • Scroll to the bottom of the page and click Create.

You should see a success banner confirming your task definition was created. The page shows demo-app-task:1 indicating revision 1.

  • Click on the task definition name to view its details.
  • Expand the container definition for demo-app and confirm you can see the health check command listed.

What about the CloudWatch Log Group?

The ECS Console automatically creates the CloudWatch Log Group /ecs/demo-app-task when the task definition is registered. The ecsTaskExecutionRole has the necessary permissions to create log groups and write log events.

Task definition creation failed?

Double-check that your ECR image URI is correct. It must match the exact format: <account-id>.dkr.ecr.us-east-1.amazonaws.com/ecs-demo-app:latest. A typo in the URI is the most common cause of task definition errors.

If you see a role error, confirm that ecsTaskExecutionRole is selected (not a custom role without the required permissions).

Help me fix my ECS task definition creation error

Your cluster is ready and your task definition describes exactly how to run the container. Next up, you will create an Application Load Balancer to distribute traffic across your running containers.

Create the Application Load Balancer

Your ECS cluster and task definition are ready. The cluster knows what to run, but it doesn't yet know how users will reach your containers.

In this step, you'll create an Application Load Balancer (ALB) that sits between the internet and your ECS tasks. The ALB distributes incoming HTTP requests across your running containers and uses health checks to route traffic only to healthy ones.

In this step, get ready to:
  • Create an Application Load Balancer with a target group that knows how to reach your containers.
  • Create security groups that lock down network access so only the ALB can talk to your ECS tasks.
Create an ALB and target group

An ALB needs two things: a listener that accepts incoming traffic, and a target group that tells it where to forward that traffic. You'll create both together.

  • In the AWS Console, type EC2 in the top search bar and select **EC2** from the results.
  • In the left sidebar, scroll down and click Load Balancers under the Load Balancing section.
  • Click Create load balancer.
  • Under Application Load Balancer, click Create.

Now configure the basic settings for your load balancer.

  • Enter demo-alb in the Load balancer name field.
  • Leave Scheme set to Internet-facing.
  • Leave IP address type set to IPv4.

Next, configure the network mapping.

  • Under Network mapping, select your default VPC.
  • Select at least 2 Availability Zone subnets by checking the boxes next to them.

Why at least 2 Availability Zones?

An ALB requires subnets in at least 2 different Availability Zones for high availability. If one zone has an outage, the ALB can still route traffic to containers running in the other zone.

Now create a security group that controls who can reach your load balancer.

  • Under Security groups, remove the default security group by clicking the X next to it.
  • Click Create a new security group. This opens a new tab.
  • Enter demo-alb-sg for the Security group name.
  • Enter Allow HTTP from internet for the Description field.
  • Under Inbound rules, click Add rule.
  • Set Type to HTTP (this automatically sets port to 80).
  • Set Source to Anywhere-IPv4 (0.0.0.0/0).
  • Click Create security group.
  • Switch back to the load balancer creation tab in your browser.
  • Click the refresh icon next to the Security groups dropdown, then select demo-alb-sg.

What is a security group?

A security group acts as a virtual firewall that controls inbound and outbound traffic for AWS resources. The demo-alb-sg group you just created allows anyone on the internet to send HTTP traffic on port 80 to your load balancer.

Now configure the listener and create a target group.

  • Under Listeners and routing, confirm the listener is set to HTTP on port 80.
  • Click Create target group. This opens a new tab.

In the new target group tab, configure where the ALB will forward traffic.

  • Select IP addresses as the target type.
  • Enter demo-app-tg for the Target group name.
  • Set Protocol to HTTP and Port to 5000.
  • Under Health checks, set the Health check path to /health.
  • Click Next.
  • Skip the Register targets step (ECS will register targets automatically later). Click Create target group.

Why IP target type instead of instance?

ECS Fargate tasks use the awsvpc network mode, which gives each task its own private IP address. The ALB routes traffic directly to these IP addresses, so the target group must use IP target type rather than instance target type.

Now finish creating the ALB.

  • Switch back to the load balancer creation tab.
  • Click the refresh icon next to the target group dropdown, then select demo-app-tg.
  • Scroll down and click Create load balancer.

Wait for the ALB status to change from Provisioning to Active. This usually takes 2-3 minutes.

ALB stuck in Provisioning?

Make sure you selected at least 2 subnets in different Availability Zones. If you only selected 1, the ALB cannot provision. Delete it and start again with 2 or more AZ subnets selected.

If it has been more than 5 minutes, refresh the page and check the status again.

Why is my ALB not becoming Active?

Create a security group for the ECS tasks

Right now, anyone who knows a container's IP address could bypass the load balancer and talk to it directly. You need a second security group that locks down your containers so only the ALB can reach them.

  • In the EC2 Console left sidebar, click Security Groups under the Network & Security section.
  • Click Create security group.
  • Enter demo-ecs-sg for the Security group name.
  • Enter Allow traffic from ALB only for the Description field.
  • Select your default VPC.
  • Under Inbound rules, click Add rule.
  • Set Type to Custom TCP.
  • Set Port range to 5000.
  • For Source, select Custom, then start typing demo-alb-sg and select the ALB security group from the dropdown.
  • Click Create security group.

Why restrict traffic to the ALB security group?

By setting the source to demo-alb-sg, only traffic coming from the ALB can reach your containers on port 5000. This is a security best practice. Direct access to containers is blocked, and all traffic must flow through the load balancer.

Confirm the ALB is ready by returning to the Load Balancers page in the EC2 Console. Your demo-alb should show a status of Active.

Don't see your security group in the source dropdown?

Make sure you selected the same VPC for demo-ecs-sg as the one your ALB uses. Security groups are VPC-specific, so they only appear in dropdowns when the VPCs match.

Why can't I find my ALB security group when creating an inbound rule?

Your networking layer is locked in. You have a load balancer ready to accept traffic from the internet and a security group that ensures only the ALB can reach your containers. Next, you'll tie everything together by deploying an ECS service that places your containers behind this load balancer.

Deploy the ECS Service Behind the ALB

Your infrastructure is in place. You have a cluster, a task definition, a load balancer, and security groups all configured and ready.

Now it's time to bring everything together by creating an ECS service. The service launches your container tasks, registers them with the ALB target group, and keeps the desired number of tasks running at all times.

In this step, get ready to:
  • Create an ECS service connected to your Application Load Balancer.
  • Verify your app is accessible via the ALB public DNS name with container health checks passing.
Create the ECS service with load balancer integration

An ECS service maintains a desired number of task instances and integrates with a load balancer to distribute traffic across them. You will create a service that runs two tasks behind your ALB.

The Create service page appears. Configure the following sections:

  • Under Deployment configuration, for Family, select demo-app-task.
  • For Revision, choose the latest revision (1).
  • For Service name, enter demo-app-service.
  • For Desired tasks, enter 2.

Why two tasks?

Running two tasks means your app stays available even if one task restarts or fails a health check. The ALB distributes traffic between both tasks automatically.

Now configure the networking section:

  • Expand the Networking section.
  • For VPC, select your default VPC.
  • For Subnets, select at least two subnets (matching the ones your ALB uses).
  • For Security group, remove any default security group and select demo-ecs-sg.
  • For Public IP, turn it on (set to Turned on).

Why enable public IP for Fargate tasks?

Fargate tasks need a public IP to pull your container image from ECR when running in a public subnet. Without it, the task fails to start because it cannot reach the ECR endpoint.

Finally, configure the load balancer integration:

  • Expand the Load balancing section.
  • For Load balancer type, select Application Load Balancer.
  • Choose Use an existing load balancer.
  • For Load balancer, select demo-alb.
  • For Choose container to load balance, select demo-app 5000:5000.
  • Choose Use an existing target group.
  • For Target group, select demo-app-tg.
  • Review your settings, then click Create.

What happens when you create the service?

ECS launches two Fargate tasks using your task definition. Each task gets its own elastic network interface (ENI) and private IP address.

ECS automatically registers those IP addresses as targets in your demo-app-tg target group. The ALB starts routing traffic to them once they pass the health check.

Service stuck in provisioning?

If your tasks fail to start, check that demo-ecs-sg allows inbound TCP on port 5000 from demo-alb-sg. Also confirm that the Public IP option is turned on so the tasks can pull the image from ECR.

help me debug why my ECS tasks are failing to start

Verify the deployment

Your tasks need 2-3 minutes to start, pass health checks, and register with the load balancer. Let's confirm everything is working.

  • Wait 2-3 minutes for the tasks to reach the RUNNING state.
  • In the ECS Console, click your demo-app-service service.
  • Click the Tasks tab. You should see 2 tasks with status RUNNING.
  • Click one of the tasks to view its details.
  • Under Health status, confirm the container shows HEALTHY.

Now grab the ALB DNS name to access your app from the internet:

  • Open the EC2 Console and navigate to Load Balancers in the left sidebar.
  • Select demo-alb.
  • Copy the DNS name value (it looks like demo-alb-123456789.us-east-1.elb.amazonaws.com).
  • Paste the DNS name into your browser's address bar and press Enter.

You should see a JSON response like this:

{
  "environment": "production",
  "message": "Hello from ECS Fargate!",
  "status": "running"
}

What should I see?

The JSON response confirms your Flask app is running in production mode on ECS Fargate. The ALB received your browser's HTTP request on port 80 and forwarded it to one of your containers on port 5000.

Finally, verify that your container logs are streaming to CloudWatch:

  • Open the CloudWatch Console.
  • In the left sidebar, click Logs, then Log groups.
  • Click the /ecs/demo-app-task log group.
  • You should see log streams (one per task) with entries showing Flask startup messages and incoming request logs.

Not seeing a response from the ALB DNS?

Check that the target group demo-app-tg shows healthy targets. In the EC2 Console, go to Target Groups, select demo-app-tg, and click the Targets tab. If targets show unhealthy, confirm your container's /health endpoint is returning a 200 status code and that demo-ecs-sg allows traffic from demo-alb-sg on port 5000.

help me troubleshoot why my ALB is not routing traffic to my ECS tasks

Your containerized app is now live on the internet, fronted by a load balancer that distributes traffic across multiple healthy containers. Next up, the Secret Mission challenges you to add monitoring that alerts you if your service goes down.

Secret mission

Add a CloudWatch Alarm for Zero Healthy Targets

Your service is running smoothly, but what happens when all your containers go down? In this secret mission, you will create a CloudWatch alarm that watches your target group's healthy host count. When it drops to zero, an email notification fires so you know immediately.

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 ECS Fargate tasks and an Application Load Balancer, both of which incur hourly charges while active.

Cost warning

Your ECS Fargate tasks cost approximately $0.01/hour per task (2 tasks running). The Application Load Balancer costs approximately $0.0225/hour. Combined, that is roughly $1/day. If you are not continuing to the next project immediately, pause or delete your resources to avoid unexpected charges.

Resources you used:

  • ECS Cluster (demo-cluster) with 2 running Fargate tasks.
  • ECS Service (demo-app-service) and Task Definition (demo-app-task).
  • Application Load Balancer (demo-alb) and Target Group (demo-app-tg).
  • Security Groups (demo-alb-sg and demo-ecs-sg).
  • ECR Repository (ecs-demo-app).
  • CloudWatch Log Group (/ecs/demo-app-task).
  • SNS Topic (ecs-alerts) and CloudWatch Alarm (if you completed the Secret Mission).
  • Local project folder (ecs-demo-app) on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue with the next project in this series.

  • Your ECS tasks will continue running behind the ALB.
  • You will continue to be charged approximately $1/day for the ALB and Fargate tasks combined.
  • Your app remains accessible at the ALB DNS name.

Pause - I'll come back to this later

Shut down running tasks to stop Fargate charges while keeping your infrastructure in place. The ALB will continue to charge approximately $0.54/day while paused.

  • Open the ECS Console at https://console.aws.amazon.com/ecs/v2.
  • Select demo-cluster, then go to the Services tab.
  • Select demo-app-service and click Update.
  • Change Desired tasks to 0 and click Update.
  • To resume later, update the desired count back to 2.

Delete - I don't want to use this again

Remove all project resources and stop all charges. Follow these steps in order.

Delete the ECS service and cluster:

  • Open the ECS Console at https://console.aws.amazon.com/ecs/v2.
  • Select demo-cluster, then go to the Services tab.
  • Select demo-app-service, then click Delete.
  • Check Force delete service, type delete in the confirmation prompt, and click Delete.
  • Wait for the service to finish deleting.
  • Click Delete Cluster, type delete demo-cluster in the confirmation box, and click Delete.

Delete the Application Load Balancer and Target Group:

  • Open the EC2 Console at https://console.aws.amazon.com/ec2/.
  • In the navigation pane, click Load Balancers.
  • Select demo-alb, then choose Actions, Delete load balancer.
  • Type confirm and click Delete.
  • In the navigation pane, click Target Groups.
  • Select demo-app-tg, then choose Actions, Delete.
  • Confirm deletion.

Delete the Security Groups:

  • In the EC2 Console navigation pane, click Security Groups.
  • Select demo-ecs-sg, then choose Actions, Delete security groups. Confirm.
  • Select demo-alb-sg, then choose Actions, Delete security groups. Confirm.

Delete the ECR repository:

  • Run this command in your terminal:
aws ecr delete-repository --repository-name ecs-demo-app --force --region us-east-1

Delete the CloudWatch Log Group:

  • Run this command in your terminal:
aws logs delete-log-group --log-group-name /ecs/demo-app-task

Delete Secret Mission resources (if completed):

  • In the CloudWatch Console, navigate to Alarms, select your HealthyHostCount alarm, and click Delete.
  • In the SNS Console, navigate to Topics, select ecs-alerts, and click Delete. Confirm by typing delete me when prompted.

Delete local project files:

  • Run this command to remove the project folder from your Desktop:
cd ~/Desktop
rm -rf ecs-demo-app

Nice Work!

Nice Work!

Nice work! You just deployed a fully containerized web application to AWS ECS Fargate, complete with a local development workflow, a private container registry, and a load-balanced production service.

You've learned how to:

  • Build a local development environment with Docker Compose, hot-reload via Compose Watch, and a multi-stage Dockerfile separating dev and production targets.
  • Push a production-optimized Docker image to a private Amazon ECR repository with vulnerability scanning enabled on push.
  • Deploy an ECS Fargate service behind an Application Load Balancer, with container-level health checks and logs streaming to CloudWatch.
  • Secret Mission: Created a CloudWatch alarm monitoring healthy target count with SNS email notifications.

Ready to quiz yourself?