Containerize a Python API with ECS
Build a Flask API, containerize it with Docker, and deploy it live on AWS.
Introduction
30 Second Summary
Every app starts on a developer's laptop. But for anyone else to use it, that app needs to run reliably on a server in the cloud, working exactly the same way it did locally.
In this project, you will build a Python Flask API, package it into a Docker container, and deploy it live to AWS using ECS Express Mode.
What You'll Build
A REST API that serves book recommendations, containerized with Docker and deployed to a live HTTPS endpoint on AWS.
By the end of this project, you'll have:
- A working Python Flask API with multiple endpoints running locally.
- A production-ready Dockerfile and a container image stored in Amazon ECR (AWS's container registry).
- A live, internet-accessible API deployed on Amazon ECS with a unique HTTPS URL anyone can visit.
- Secret Mission: Add a health check endpoint and configure auto-scaling so your API handles traffic spikes automatically.
Are there any prerequisites?
You will need an AWS account, Docker Desktop installed with the WSL 2 backend, WSL on Windows 11, and Cursor as your code editor. You should also be comfortable with basic Python and terminal commands.
Install and Configure the AWS CLI
To deploy your containerized app to AWS later in this project, you need a way to talk to AWS services from your terminal. The AWS CLI (Command Line Interface) is that bridge.
In this step, you'll install the AWS CLI, connect it to your AWS account with your credentials, and verify the connection works.
In this step, get ready to:
- Install AWS CLI v2 on your machine.
- Configure your credentials so the CLI can access your AWS account.
- Verify the connection by confirming your account identity.
Install AWS CLI v2
The AWS CLI lets you create, manage, and delete AWS resources directly from a terminal. You'll use it throughout this project to push Docker images and deploy your application.
- Press the Windows key to open your search bar.
- Type Ubuntu and press Enter to open your WSL terminal.
- Check whether the AWS CLI is already installed by running this command:
aws --version
✔️ I see aws-cli/2.x.x
Your output looks something like:
aws-cli/2.27.41 Python/3.11.6 Windows/10 exe/AMD64 prompt/off
AWS CLI v2 is already installed and ready to go. Skip ahead to the next substep.
ⓧ I see aws-cli/1.x.x
You have an older version of the AWS CLI installed. You need version 2 for this project.
- Download the AWS CLI v2 MSI installer from this link:
- https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file and follow the installer prompts.
- Close your WSL terminal completely and reopen it.
- Run aws --version again. You should now see aws-cli/2.x.x in the output.
Why does this work?
The Windows MSI installer places the AWS CLI on your system PATH. WSL shares this PATH with Windows, so the aws command becomes available inside your Ubuntu terminal automatically.
ⓧ Command not found
The AWS CLI is not installed yet. You'll install it now using the official MSI installer for Windows.
- Download the AWS CLI v2 MSI installer from this link:
- https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file and follow the installer prompts. Accept the defaults and click through each screen.
- Close your WSL terminal completely and reopen it (this refreshes the PATH so the new command is found).
- Run aws --version again. You should now see aws-cli/2.x.x in the output.
Why does this work?
The Windows MSI installer places the AWS CLI on your system PATH. WSL shares this PATH with Windows, so the aws command becomes available inside your Ubuntu terminal automatically.
Still not seeing a version number?
Make sure you fully closed and reopened your WSL terminal after running the installer. The PATH update only takes effect in new terminal sessions.
If the command still isn't found, try running /mnt/c/Program\ Files/Amazon/AWSCLIV2/aws.exe --version to confirm the installer placed the files correctly.
help me troubleshoot why aws --version is not working in my WSL terminal after installing the AWS CLI MSI on Windows
Configure the AWS CLI with your credentials
The AWS CLI needs credentials to prove who you are when making requests to AWS. You'll provide an Access Key ID and Secret Access Key from your AWS account.
Where do I get my access keys?
Access keys are created in the AWS Management Console. Go to the IAM console at https://console.aws.amazon.com/iam/ and navigate to Users.
Select your user, open the Security credentials tab, and choose Create access key. Select Other as the use case, then click Next and Create access key.
Copy both the Access Key ID and Secret Access Key immediately. The secret key is shown only once. Store them somewhere safe.
- In your WSL terminal, run the configuration command:
aws configure
- The command will prompt you for four values. Enter each one and press Enter after each:
- AWS Access Key ID: paste your Access Key ID.
- AWS Secret Access Key: paste your Secret Access Key.
- Default region name: type us-east-1.
- Default output format: type json.
What does aws configure do?
This command stores your credentials in a local file at ~/.aws/credentials so the AWS CLI can authenticate your requests automatically. The region and output format are saved in ~/.aws/config.
Every AWS CLI command you run from now on will use these settings by default.
Not sure which region to use?
Use us-east-1 as shown above. This is the most widely supported AWS region and is used throughout this project.
help me understand AWS regions and which one to pick for my project
Verify the configuration works
Now that your credentials are saved, you can confirm the AWS CLI can actually reach your account.
- Test the connection by running this command:
aws sts get-caller-identity
What does this command do?
The aws sts get-caller-identity command asks AWS "who am I?" and returns details about the credentials you configured. If the credentials are valid, you'll get a response. If they're wrong, you'll get an error.
You should see JSON output that looks similar to this:
{
"UserId": "AIDAEXAMPLE123456",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/your-username"
}
The Account field shows your 12-digit AWS account ID. The Arn field shows the full identity path of the user whose credentials you configured.
Seeing an error instead of JSON?
If you see a message about invalid credentials or expired tokens, run aws configure again and double-check that you pasted the Access Key ID and Secret Access Key correctly (no extra spaces or missing characters).
If you see a network-related error, check your internet connection and try again.
help me fix the error I'm seeing when running aws sts get-caller-identity
Your AWS CLI is installed, configured, and verified. Next up, you'll build the Python Flask API that you'll eventually containerize and deploy to the cloud.
Build the Python Flask API
Your AWS CLI is configured and ready to talk to the cloud. Now it's time to build the application you'll eventually deploy there.
In this step, you'll create a simple REST API using Flask, a lightweight Python web framework. The API serves book recommendations as JSON data, giving you a realistic app to containerize and deploy in later steps.
In this step, get ready to:
- Create a project directory and Flask application with multiple API endpoints.
- Install dependencies and test the API locally.
Create the project directory and Flask app
Every project needs a home. You'll create a dedicated folder in WSL's Linux filesystem, which gives Docker the best performance when reading your files later.
- In your WSL terminal, move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir flask-books-api
cd flask-books-api
- Confirm the folder exists by running:
ls ~/Desktop
You should see flask-books-api in the output.
Don't see the folder?
If ~/Desktop didn't exist, WSL may have placed you elsewhere. Run mkdir -p ~/Desktop first, then retry the commands above.
help me create my project directory in WSL
Now you need to open this folder in Cursor so you can create your application files.
- Press the Windows key to open your search bar.
- Type Cursor and press Enter to open it.
- In Cursor, click File in the top menu bar.
- Click Open Folder.
- In the path bar at the top, type \\wsl$\Ubuntu\home\ followed by your WSL username, then \Desktop\flask-books-api.
- Click Select Folder.
Find your WSL username
Not sure what your WSL username is? Run whoami in your WSL terminal. The output is your username.
- Create a new file called app.py by clicking the new file icon in Cursor's Explorer sidebar.
Start by adding the imports, app instance, and book data to app.py:
- Set up the Flask app and sample data by pasting this code into app.py:
from flask import Flask, jsonify
# Create the Flask application instance
app = Flask(__name__)
# Sample book data that our API will serve
books = [
{"id": 1, "title": "The Pragmatic Programmer", "author": "David Thomas & Andrew Hunt", "year": 2019},
{"id": 2, "title": "Clean Code", "author": "Robert C. Martin", "year": 2008},
{"id": 3, "title": "Designing Data-Intensive Applications", "author": "Martin Kleppmann", "year": 2017},
]
What does this code do?
- from flask import Flask, jsonify imports the Flask framework and a helper that converts Python data into JSON responses.
- app = Flask(__name__) creates your web application instance.
- books is a list of dictionaries acting as a simple in-memory database. Each book has an id, title, author, and year.
- Add the API route handlers below the books list by pasting this code at the bottom of app.py:
# Root endpoint - shows available API routes
@app.route("/")
def home():
return jsonify({"message": "Welcome to the Books API", "endpoints": ["/books", "/books/<id>"]})
# Returns all books as JSON
@app.route("/books")
def get_books():
return jsonify(books)
# Returns a single book by its ID, or 404 if not found
@app.route("/books/<int:book_id>")
def get_book(book_id):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Book not found"}), 404
return jsonify(book)
# Run the app on all interfaces, port 5000
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
- @app.route("/") defines the root endpoint. It returns a welcome message and lists the available API paths.
- @app.route("/books") returns the full list of books as JSON.
- @app.route("/books/<int:book_id>") looks up a single book by its ID. If the book doesn't exist, it returns a 404 error.
- app.run(host="0.0.0.0", port=5000) starts the server on port 5000 and listens on all network interfaces. This is important for Docker later, since containers need the app to listen beyond just localhost.
- Save app.py by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and move on to the next section.
ⓧ I'd like to double check the full code
Here's what your complete app.py should look like:
from flask import Flask, jsonify
app = Flask(__name__)
books = [
{"id": 1, "title": "The Pragmatic Programmer", "author": "David Thomas & Andrew Hunt", "year": 2019},
{"id": 2, "title": "Clean Code", "author": "Robert C. Martin", "year": 2008},
{"id": 3, "title": "Designing Data-Intensive Applications", "author": "Martin Kleppmann", "year": 2017},
]
@app.route("/")
def home():
return jsonify({"message": "Welcome to the Books API", "endpoints": ["/books", "/books/<id>"]})
@app.route("/books")
def get_books():
return jsonify(books)
@app.route("/books/<int:book_id>")
def get_book(book_id):
book = next((b for b in books if b["id"] == book_id), None)
if book is None:
return jsonify({"error": "Book not found"}), 404
return jsonify(book)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Create the requirements file and test locally
Python projects use a requirements.txt file to list their dependencies. This file tells anyone (or any container) exactly which packages to install.
- Create a new file called requirements.txt in Cursor's Explorer sidebar.
- Add this single line to pin Flask to a specific version:
flask==3.1.3
Why pin the version?
Pinning flask==3.1.3 ensures everyone running your project gets the exact same version. Without a pin, a future pip install could pull a newer version that breaks your code.
- Save requirements.txt by pressing Ctrl+S.
- Switch back to your WSL terminal and install the dependencies by running:
pip install -r requirements.txt
You should see Flask and its sub-dependencies downloading and installing.
Seeing a permission error or pip not found?
If pip is not found, try pip3 instead. If you see a permission denied error, add --user to the end of the command.
help me install Flask in WSL
Now run the Flask app to make sure everything works before you containerize it.
- Start the Flask development server by running:
python app.py
You should see output showing the server is running on http://0.0.0.0:5000.
- Open a second WSL terminal tab or window.
- Test the /books endpoint by running:
curl http://localhost:5000/books
You should see your three books returned as a JSON array.
What should I see?
The response is a JSON array containing three book objects, each with id, title, author, and year fields. If you see this data, your API is working correctly.
- Return to the first terminal where Flask is running.
- Stop the server by pressing Ctrl+C.
Curl returns connection refused?
Make sure the Flask server is still running in your first terminal before testing with curl. If you see python: command not found, try python3 app.py instead.
help me run my Flask app locally in WSL
Your API is running and tested. Next up, you'll wrap this application in a Docker container so it runs the same way on any machine.
Containerize the Application with Docker
Your Flask API works on your machine, but that is only half the battle. To deploy it to the cloud, you need to package it into a container that runs identically everywhere.
A Dockerfile is a recipe that tells Docker exactly how to build your application into a portable image. In this step, you will write that recipe, build the image, and prove it runs locally before pushing it to the cloud.
In this step, get ready to:
- Write a Dockerfile that packages your Flask API.
- Build the Docker image and run it locally.
- Stop the container and confirm the image is ready for deployment.
Write the Dockerfile
A Dockerfile lists instructions in order: start from a base image, copy your code in, install dependencies, and define how to run the app.
- Create a new file called Dockerfile in your project directory by running this command:
touch Dockerfile
- Open Dockerfile in Cursor and paste the following code:
# Use a lightweight Python base image
FROM python:3.12-slim
# Set the working directory inside the container
WORKDIR /app
# Copy requirements first (for Docker layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY app.py .
# Tell Docker which port the app listens on
EXPOSE 5000
# Define the command to run the app
CMD ["python", "app.py"]
What does this Dockerfile do?
Each instruction creates a layer in the final image:
- FROM python:3.12-slim starts from an official Python image that is small and production-friendly.
- WORKDIR /app creates a directory inside the container and switches to it.
- COPY requirements.txt . and RUN pip install install your Python dependencies. Copying requirements first means Docker can cache this layer and skip reinstalling if only your app code changes.
- COPY app.py . adds your application source code.
- EXPOSE 5000 documents the port your app listens on.
- CMD ["python", "app.py"] tells Docker what command to run when the container starts.
- Save Dockerfile.
- Confirm the file exists by running:
ls
You should see Dockerfile, app.py, and requirements.txt listed.
Don't see the Dockerfile?
Make sure you created it in the same directory as app.py. Run pwd to check your current directory, then retry the touch Dockerfile command from there.
help me figure out why my Dockerfile isn't appearing in my project directory
Build and run the Docker image locally
Now you will turn the Dockerfile into a runnable image, then launch a container from it to prove it works.
- Build the Docker image by running this command:
docker build -t flask-books-api .
What does this command do?
- docker build reads the Dockerfile in the current directory (.) and executes each instruction.
- -t flask-books-api tags the resulting image with the name flask-books-api so you can reference it later.
The build takes a minute or two the first time because Docker downloads the python:3.12-slim base image.
Seeing a permission error or daemon not running?
Make sure Docker Desktop is running on your machine. You can check by looking for the Docker whale icon in your system tray. If you see a message about the Docker daemon not being available, open Docker Desktop and wait for it to fully start.
help me fix a Docker daemon connection error in WSL
- Run a container from the image by running this command:
docker run -d -p 5000:5000 flask-books-api
What does this command do?
- -d runs the container in detached mode (in the background) so your terminal stays free.
- -p 5000:5000 maps port 5000 on your machine to port 5000 inside the container.
- flask-books-api is the image name you tagged in the previous step.
Docker prints a long container ID string. This confirms your container is running in the background.
- Test the containerized API by running:
curl http://localhost:5000/books
You should see the JSON list of books returned, just like when you ran the app directly with Python.
Getting connection refused?
Give the container a few seconds to start up, then try the curl command again. If it still fails, run docker ps to check if the container is actually running. If it exited immediately, run docker logs $(docker ps -lq) to see the error output.
help me debug why my Docker container won't stay running
Stop the container and verify the image
You have confirmed the container works. Now stop it so port 5000 is free for later use.
- Find and stop the running container by running these commands:
docker ps
docker stop $(docker ps -q --filter ancestor=flask-books-api)
What does this do?
docker ps lists all running containers so you can see yours. The second command finds the container ID of your flask-books-api container and passes it to docker stop to shut it down gracefully.
- Confirm your image is ready by running:
docker images
You should see flask-books-api listed with a latest tag and a size around 150-200 MB.
Don't see flask-books-api in the list?
Go back and re-run the docker build -t flask-books-api . command. Check for any error messages during the build. The most common cause is a typo in the Dockerfile or running the build from the wrong directory.
help me figure out why my Docker image is missing
✔️ Awesome, I've got everything!
Great. Double check you have saved your Dockerfile and confirmed the image appears in docker images.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile you should have:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
Your application is now a portable container image ready to run anywhere Docker is available. Next up, you will push this image to Amazon ECR so AWS can access it for deployment.
Push the Image to Amazon ECR
Your Docker image is built, tested, and ready to go. The next challenge is getting it somewhere that AWS deployment services can pull from.
Amazon ECR (Elastic Container Registry) is AWS's managed container registry. It stores your Docker images so that services like ECS can pull and run them.
In this step, get ready to:
- Create an ECR repository in AWS.
- Authenticate Docker to your ECR registry.
- Tag and push your local image to ECR.
Create an ECR repository
Before you can push an image, you need a repository in ECR to store it. Think of it like creating a folder in the cloud specifically for your container image.
- Create a new ECR repository by running this command in your WSL terminal:
aws ecr create-repository --repository-name flask-books-api --region us-east-1
What does this command do?
- aws ecr create-repository tells AWS to create a new private container registry.
- --repository-name flask-books-api names your repository so you can reference it later.
- --region us-east-1 creates the repository in the US East region, matching your CLI configuration.
- You should see JSON output containing a repositoryUri field. Copy the value of repositoryUri and save it somewhere (a text file or sticky note). You will need it for the next commands.
What is the repositoryUri?
The repositoryUri follows the format <aws_account_id>.dkr.ecr.us-east-1.amazonaws.com/flask-books-api.
It is the full address where your image will live in the cloud. Every ECR command from here on uses this URI.
Seeing RepositoryAlreadyExistsException?
This means a repository named flask-books-api already exists in your account. That is fine. You can continue to the next substep using the existing repository.
help me fix my ECR repository creation error
Authenticate Docker to ECR
ECR is a private registry. Docker needs a temporary authentication token before it can push images there. The token lasts 12 hours.
- Log Docker into 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?
- aws ecr get-login-password retrieves a temporary authentication token from AWS.
- The | (pipe) sends that token directly to the next command.
- docker login --username AWS --password-stdin uses the token to authenticate Docker to your private ECR registry.
Find your AWS account ID
Your 12-digit account ID appeared in the repositoryUri output from the previous command. It is the number before .dkr.ecr.
You can also retrieve it by running aws sts get-caller-identity and looking at the Account field.
- You should see Login Succeeded in the terminal output.
Login failed or connection refused?
- Make sure Docker Desktop is running. The Docker daemon must be active for the login command to work.
- Double check that you replaced the account ID placeholder with your actual 12-digit AWS account number.
- If you see a credentials store error, try running the command again. Temporary network issues can cause this.
help me troubleshoot my Docker ECR login failure
Tag and push the image to ECR
Docker needs your image to be tagged with the full ECR repository URI before it can push to the right destination. Tagging does not create a new image. It adds an alias that points to the same image data.
- Tag your local image with the ECR repository URI by running this command:
docker tag flask-books-api:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/flask-books-api:latest
What does this command do?
- docker tag creates an alias for an existing image.
- flask-books-api:latest is your local image name and tag.
- The second argument is the full ECR destination address. Docker uses this URI to know which registry and repository to push to.
- 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/flask-books-api:latest
This will take a minute or two as Docker uploads each layer of your image. You should see multiple lines showing layers being pushed, followed by a final digest line.
What should I see?
The output shows each image layer being uploaded with a Pushed status. At the end, you will see a line starting with latest: digest: sha256: followed by a long hash. This confirms your image is stored in ECR.
Push failed or denied?
- If you see an authorization error, your login token may have expired. Re-run the authentication command from the previous substep.
- If you see a "repository does not exist" error, double check that the repository name in your tag matches flask-books-api exactly.
- If the push hangs, check your internet connection. Large layers can take a few minutes on slower connections.
help me fix my docker push to ECR
Your container image is now stored in the cloud. Next, you will deploy it as a live, internet-accessible API using ECS Express Mode.
Deploy to AWS with ECS Express Mode
Your container image is now stored in Amazon ECR, ready to be deployed.
In this step, you will take that image and deploy it to the internet using ECS Express Mode. Express Mode handles all the heavy lifting of cloud networking, load balancing, and auto-scaling with a single command.
In this step, get ready to:
- Create the two IAM roles required for ECS Express Mode.
- Deploy your containerized API to the cloud with one command.
- Access your live API via a public HTTPS URL.
Create the required IAM roles
ECS Express Mode needs two IAM roles to operate. One lets ECS pull your container image and write logs. The other lets ECS provision infrastructure like load balancers and security groups on your behalf.
- Create the first role (ecsTaskExecutionRole) by running this command in your WSL terminal:
aws iam create-role --role-name ecsTaskExecutionRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
What does this command do?
This creates an IAM role that ECS tasks can "assume" (act as). The trust policy specifies that ecs-tasks.amazonaws.com is allowed to use this role.
Think of it like giving ECS a badge that grants access to specific AWS services on your behalf.
- Attach the managed policy to this role by running:
aws iam attach-role-policy --role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
What does this policy grant?
AmazonECSTaskExecutionRolePolicy gives ECS permission to pull container images from ECR and write logs to CloudWatch. Without this, ECS cannot download your image to run it.
- Create the second role (ecsInfrastructureRoleForExpressServices) by running:
aws iam create-role --role-name ecsInfrastructureRoleForExpressServices \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAccessInfrastructureForECSExpressServices",
"Effect": "Allow",
"Principal": {
"Service": "ecs.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
Why a separate infrastructure role?
This role lets the ECS service itself (not the task) create and manage AWS infrastructure. Express Mode automatically provisions an Application Load Balancer, security groups, and auto-scaling policies. It needs this role to do that work.
- Attach the infrastructure policy to this role by running:
aws iam attach-role-policy --role-name ecsInfrastructureRoleForExpressServices \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRoleforExpressGatewayServices
- Verify both roles exist by running:
aws iam get-role --role-name ecsTaskExecutionRole --query 'Role.RoleName'
aws iam get-role --role-name ecsInfrastructureRoleForExpressServices --query 'Role.RoleName'
You should see both role names returned in the output.
Seeing an "EntityAlreadyExists" error?
This means the role already exists in your AWS account. That is fine. You can skip the create-role command for that role and just run the attach-role-policy command to make sure the correct policy is attached.
help me fix my IAM role creation error for ECS Express Mode
Deploy the application with ECS Express Mode
With both IAM roles in place, you can now deploy your containerized API to the cloud. ECS Express Mode provisions everything you need with a single command.
- Deploy your application by running this command, replacing the account ID placeholder with your own AWS account ID:
aws ecs create-express-gateway-service \
--service-name flask-books-api \
--primary-container '{"image":"[[AWS_ACCOUNT_ID=\"your 12-digit AWS account ID\"]].dkr.ecr.us-east-1.amazonaws.com/flask-books-api:latest","containerPort":5000}' \
--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?
This single command does a lot of work behind the scenes:
- It pulls your container image from ECR.
- It creates a Fargate task to run your container (1 vCPU, 2 GB memory by default).
- It provisions an internet-facing Application Load Balancer with automatic SSL/TLS.
- It sets up auto-scaling based on CPU utilization.
- The containerPort of 5000 tells the load balancer to route traffic to port 5000 inside your container, where Flask is listening.
The --monitor-resources flag keeps the terminal open so you can watch the deployment progress in real time.
Finding your AWS account ID
If you do not remember your 12-digit account ID, run aws sts get-caller-identity --query 'Account' --output text to retrieve it.
The deployment takes a few minutes. You will see status updates in your terminal as resources are provisioned. Wait until the status shows the service is active.
Deployment taking longer than 5 minutes?
Express Mode provisions multiple resources (load balancer, security groups, scaling policies). First-time deployments can take 3-7 minutes. If it seems stuck, do not cancel. The --monitor-resources flag will surface any errors automatically.
If you see a permissions error, double-check that both IAM roles have the correct policies attached by running aws iam list-attached-role-policies --role-name ecsTaskExecutionRole.
help me debug my ECS Express Mode deployment
Access the live API
Once the deployment finishes, your API is live on the internet. ECS Express Mode gives your service a unique HTTPS URL.
- Your service URL follows this format:
https://flask-books-api.ecs.us-east-1.on.aws
What is this URL?
ECS Express Mode automatically assigns a unique HTTPS URL to your service in the format https://<service-name>.ecs.<region>.on.aws. Since you named your service flask-books-api and deployed to us-east-1, your URL is the one shown above. SSL/TLS is handled automatically.
- Test your live API by running:
curl https://flask-books-api.ecs.us-east-1.on.aws/books
You should see the same JSON response with your three book recommendations that you saw when testing locally.
- Test the single-book endpoint by running:
curl https://flask-books-api.ecs.us-east-1.on.aws/books/1
You should see the details for "The Pragmatic Programmer" returned as JSON.
Share your URL with anyone
This URL is publicly accessible. Anyone with the link can call your API from their browser or terminal. Try pasting it directly into your web browser to see the JSON response rendered on screen.
Getting a 503 or timeout error?
The load balancer may still be waiting for your container to pass its health check. Wait 1-2 minutes after deployment completes, then try again.
If the error persists, make sure your app.py uses host="0.0.0.0" (not 127.0.0.1). Flask must listen on all interfaces for the load balancer to reach it inside the container.
help me fix my ECS Express Mode 503 error
Your API is now live on the internet. Anyone in the world can access it. In the next section, you will tackle a secret mission to make your API more production-ready with health checks and auto-scaling.
Secret mission
Add Health Check and Auto-Scaling
Your API is live, but production APIs need to prove they're healthy and handle traffic spikes gracefully. In this secret mission, you'll add a health check endpoint and configure auto-scaling so your service can automatically recover from failures and grow under load.
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. Your ECS Express Mode service incurs Fargate compute and ALB charges while running, so deleting is recommended if you are done exploring.
Cost warning
Leaving your ECS Express Mode service running costs approximately $1-2 per day for the default Fargate configuration (1 vCPU, 2 GB memory) plus ALB hourly charges.
If you are not actively using the API, delete the service to stop all charges.
Resources you used:
- ECS Express Mode service (includes Fargate tasks, Application Load Balancer, auto-scaling policies, security groups).
- ECR repository (flask-books-api).
- IAM role: ecsTaskExecutionRole.
- IAM role: ecsInfrastructureRoleForExpressServices.
- Local project files (app.py, requirements.txt, Dockerfile).
- Local Docker image (flask-books-api).
Keep everything running
No action needed. Choose this if you are still actively building or testing your API.
- Your API remains accessible at its public URL.
- Fargate and ALB charges continue while the service is active.
- The local Docker image and project files remain on your machine.
Pause - I'll come back to this later
Delete the ECS service to stop all cloud charges, but keep your local files and Docker image so you can redeploy later.
- Delete the ECS Express Mode service by running the following command in your WSL terminal (replace the service ARN with yours):
aws ecs delete-express-gateway-service --service-arn [[ECS_SERVICE_ARN="your ECS service ARN from the deploy step"]]
- Keep your local project files, Docker image, ECR repository, and IAM roles intact for a quick redeploy later.
When you are ready to redeploy, you can push a new image to ECR and run aws ecs create-express-gateway-service again with the same roles.
Delete - I don't want to use this again
Remove all project resources and start fresh. Run each command below in your WSL terminal.
- Delete the ECS Express Mode service (this removes the ALB, security groups, auto-scaling, and Fargate tasks automatically):
aws ecs delete-express-gateway-service --service-arn [[ECS_SERVICE_ARN="your ECS service ARN from the deploy step"]]
- Delete the ECR repository and all images inside it:
aws ecr delete-repository --repository-name flask-books-api --region us-east-1 --force
- Delete the ecsTaskExecutionRole IAM role (detach its policy first):
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
- Delete the ecsInfrastructureRoleForExpressServices IAM role (detach its policy first):
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:
docker rmi flask-books-api
- Delete the local project folder:
rm -rf ~/Desktop/flask-books-api
That's a Wrap!
That's a Wrap!
You just deployed a containerized application to the cloud. You took a Python API from your local machine all the way to a live, internet-accessible endpoint on AWS.
Here's what you accomplished:
- Built a Python Flask REST API from scratch and containerized it with a production-ready Dockerfile.
- Pushed a container image to Amazon ECR, AWS's managed container registry.
- Deployed a live, publicly accessible API using ECS Express Mode with a single command.
- Added a /health endpoint and configured auto-scaling for production readiness.
Ready to quiz yourself?