Deploy a Dockerized Python App to AWS ECS Using GitHub Actions

Deploy a Dockerized Python App to AWS ECS Using GitHub Actions

Deploying applications to the cloud reliably can be challenging due to inconsistent environments, scaling requirements, and manual deployment steps that can cause errors or downtime.

Containerization with Docker standardizes application environments, allowing them to run identically across development and production, while managed AWS services like AWS ECS provide scalable, fault-tolerant hosting for containers. Automation pipelines such as GitHub Actions handle builds and deployments, reducing manual effort and keeping production aligned with code changes.

This guide walks through deploying a Dockerized Python Flask app to AWS ECS using GitHub Actions, creating an end-to-end deployment pipeline that deploys code changes automatically.

Prerequisites

Before proceeding with the steps, ensure you have the following:

  • An AWS account with an IAM user having programmatic access (AWS Access Key ID & Secret Access Key) and permissions for ECS, ECR, and IAM.
  • A GitHub account with a repository for the project.
  • Local setup: Python 3.x, Docker, and Git installed.
  • Basic familiarity with Python, Docker, and Git.

Steps To Deploy a Dockerized Python App to AWS ECS Using GitHub Actions

Step 1: Create a Python Flask App

  • First, let’s create a project directory on the local machine where the Python app will be created.
mkdir <name-for-python-app> && cd <name-for-python-app>
Creating a Project Directory
Creating a Project Directory
  • Inside the project directory (example: Python-app-for-ECS), create a file named app.py with the script below.
Creating the Python App
Creating the Python App
from flask import Flask, request, jsonify, render_template_string

app = Flask(__name__)

HTML = """
<h2>What is the capital of France?</h2>
<input id="a"><button onclick="s()">Submit</button>
<p id="r"></p>
<script>
function s() {
  fetch('/answer', {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({a: document.getElementById('a').value})
  })
  .then(r => r.json())
  .then(d => document.getElementById('r').innerText = d.m)
}
</script>
"""

@app.route('/')
def home():
    return render_template_string(HTML)

@app.route('/answer', methods=['POST'])
def answer():
    a = request.get_json().get('a','').strip().lower()
    if a == 'paris':
        return jsonify(m='Correct!')
    return jsonify(m=' Wrong! The correct answer is Paris.')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

[It creates a lightweight Flask app that delivers an interactive browser-based quiz. Users submit answers through API calls, and the backend responds with JSON messages indicating whether answers are correct or incorrect.]

  • Create a requirements.txt file in the project directory.
Successful Creation of App File and Requirements File
Successful Creation of App File and Requirements File

Step 2: Dockerize The Python App

With the Python app ready, the next step is to package it into a Docker container. This allows the app to run consistently across different environments, from local development to cloud deployment on AWS ECS.

  • Create the Dockerfile inside the project directory where the app.py and requirements.txt files are located with the following code:
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"]
Creating a Docker File of the Python App
Creating a Docker File of the Python App

Refer to the Containerizing app on Docker documentation to learn more.

(Optional) Test Docker Image Locally

Before deploying the Docker image to ECS, test it in the local environment to ensure it works as expected.

  • Build the Docker image and run the container on the local machine with the following command:
docker build -t <app-name-for-local-test> .
docker run -p 5000:5000 <app-name-for-local-test>

Note: Replace the placeholders with the desired name.

Building and Running Docker Image Locally
Building and Running a Docker Image Locally
  • Verify local testing by opening the web browser and visiting http://localhost:5000.
Local Testing of Flask App Working
Local Testing of Flask App Working

Step 3: Push Code to GitHub

To prepare for deployment using GitHub Actions, the local project needs to be pushed to a GitHub repository. Run the following commands in sequence from the project’s root directory:

git init
git add .
git commit -m "Commiting local code to GitHub"
git branch -M main
git remote add origin https://github.com/<username>/<repository-name>.git
git push -u origin main

Note: Replace <username> and <repository-name> placeholders in the repository URL with your actual GitHub username and repository name.

Pushing Local Project To GitHub Repository
Pushing Local Project To GitHub Repository

Authentication Tip: During the git push process, GitHub requires a Personal Access Token (PAT) instead of a password for HTTPS pushes, as seen in the figure above. A PAT can be created in GitHub account settings under Developer settings > Personal access tokens.

Project Folder Successfully Pushed To GitHub
Project Folder Successfully Pushed To GitHub

Step 4: Prepare AWS ECS Environment

To run the Dockerized Python app on AWS ECS, begin by configuring the core infrastructure components: an ECR repository to store the container image, an ECS cluster to host the application, and task and service definitions to manage its execution.

4.1 Create an ECR Repository

  • Navigate to Elastic Container Registry (ECR) in AWS Console and click “Create repository.”
  • Under General settings, enter the repository name (example: ecr-repo-for-app).
Creating an ECR Repository for the Python App
Creating an ECR Repository for the Python App
  • Leave other settings as default and click “Create.”

4.2 Create an ECS Cluster

  • Navigate to the Amazon ECS (Elastic Container Service) console and click on Clusters in the sidebar, then click “Create Cluster.”
  • In the Cluster configuration, enter a cluster name (for example, flask-ecs-cluster).
  • Under Infrastructure, select AWS Fargate (serverless).
Creating an ECS Cluster For The App
Creating an ECS Cluster For The App
  • Leave other settings at their default values, including optional features such as CloudWatch Container Insights and encryption.
  • Click Create.

4.3 Create a Task Definition

  • Navigate to the ECS console, select Task Definitions from the left sidebar, and click “Create new task definition.”
  • Enter the task definition name (example, my-app-task-definition).
  • Choose AWS Fargate as the launch type.
  • Choose an appropriate “Task size” (CPU and Memory settings) based on the application’s expected workload. (This guide uses 0.25 vCPU and 0.5 GB of memory to support a lightweight Flask app.)
Configuring Task Definition Name, Launch Type, and Resource Settings
Configuring Task Definition Name, Launch Type, and Resource Settings
  • For the Task execution role, select the pre-defined ecsTaskExecutionRole (if available) or create a new IAM role with the permissions to pull images from ECR and send logs to CloudWatch.
Attaching the Task Execution Role
Attaching the Task Execution Role

Under Container details, provide:
 Container name: <any-name> (eg: flask-container)
 Image URI: Enter a valid image URI (e.g., nginx if your app image isn’t ready). The GitHub Actions workflow will update it with the actual Docker image URI after pushing to ECR.
– Container port: Set to 5000 (the default port used by Flask apps running inside the Docker container)

Adding Container Details for Task Definition
Adding Container Details for Task Definition
  • Click Create to save the task definition.

4.4 Create an ECS Service

  • In the AWS Console, go to ECS > Clusters, select the existing cluster created earlier (e.g., flask-ecs-cluster).
  • Click the Services tab, then click Create.
Navigating to the ‘Services’ tab to create an ECS Service
Navigating to the ‘Services’ tab to create an ECS Service
  • Configure the Service details:
    – Task definition family: Select the task definition created earlier
    – Service name: Enter a unique name (eg, my-app-ecs-service)
ECS Service Details Setup
ECS Service Details Setup
  • Under Compute configuration (advanced), select “Capacity provider strategy” and configure the following:
     Capacity provider strategy: Use custom (Advanced)
     Capacity provider: FARGATE
    – Base: 0
    – Weight: 1
    – Platform version: LATEST
Compute Configuration for ECS service
Compute Configuration for ECS service
  • In the Deployment configuration section, select:
    – Scheduling strategy: Select Replica
    – Desired tasks: Enter the number of tasks to run (e.g., 2).
Deployment Settings for ECS Service
Deployment Settings for ECS Service
  • Under Networking, select:
    – VPC: Select the default VPC (or your custom VPC if preferred for better control).
    – Subnets: Choose at least one public subnet within the selected VPC.
    – Security group: Create or select a security group that allows inbound traffic on the port 5000.
    – Auto-assign public IP: Enable to allow your ECS tasks to have public IP addresses for internet access.
Networking Setup For ECS Service
Networking Setup For ECS Service
  • Leave the remaining optional sections at their default settings.
  • Click “Create” to launch the service.

Step 5: Configure AWS Credentials as GitHub Secrets

GitHub Actions requires AWS credentials for authentication, plus environment details to deploy to ECS.

In the GitHub repository:

  • Navigate to Settings > Secrets and variables > Actions. This opens the secrets management section within the same tab.
  • Click New repository secret to add a new secret.
Navigating to Set AWS Secrets
Navigating to Set AWS Secrets
  • Then, add the following secrets (one at a time):
     AWS_ACCESS_KEY_ID: <your-access-key-id>
    – AWS_SECRET_ACCESS_KEY: <your-secret-access-Key>
    – AWS_REGION: <desired-aws-region> (e.g. us-east-1)
    – AWS_ACCOUNT_ID: <your-12-digit-aws-account-id>
AWS Credentials as Secrets in GitHub
AWS Credentials as Secrets in GitHub

Step 6: Create GitHub Actions Workflow

The workflow builds and pushes the Docker image to ECR, updates the ECS task definition with the new image, and deploys the updated service automatically on code changes.

  • On the local machine, inside the project root directory (example: Python-app-for-ECS), create a workflow directory and a .yml file.
mkdir -p .github/workflows
nano .github/workflows/deploy.yml
Creating a Workflow Directory and Deployment File
Creating a Workflow Directory and Deployment File
  • Paste the following content into the .yml file:
name: Deploy to ECS via GitHub Actions

on:
  push:
    branches: [main]

env:
  AWS_REGION: ${{ secrets.AWS_REGION }}
  ECR_REPO_NAME: <your-ecr-repo-name>
  ECS_CLUSTER_NAME: <your-ecs-cluster-name>
  ECS_SERVICE_NAME: <your-ecs-service-name>
  TASK_DEFINITION_NAME: <your-task-def-name>

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Install jq
        run: sudo apt-get update && sudo apt-get install -y jq

      - name: Log in to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, tag, and push Docker image to ECR
        run: |
          IMAGE_URI=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPO_NAME }}:latest
          echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_ENV
          docker build -t $IMAGE_URI .
          docker push $IMAGE_URI

      - name: Update ECS Task Definition with new image
        run: |
          aws ecs describe-task-definition \
            --task-definition ${{ env.TASK_DEFINITION_NAME }} \
            --query "taskDefinition | {
              family: family,
              taskRoleArn: taskRoleArn,
              executionRoleArn: executionRoleArn,
              networkMode: networkMode,
              containerDefinitions: containerDefinitions,
              requiresCompatibilities: requiresCompatibilities,
              cpu: cpu,
              memory: memory
            }" > taskdef.json

          cat taskdef.json | \
            jq 'del(.taskRoleArn | select(. == null)) | del(.executionRoleArn | select(. == null))' | \
            jq --arg IMAGE "$IMAGE_URI" '.containerDefinitions[0].image = $IMAGE' > new-taskdef.json

          aws ecs register-task-definition --cli-input-json file://new-taskdef.json

      - name: Deploy updated task to ECS service
        run: |
          NEW_REVISION=$(aws ecs describe-task-definition \
            --task-definition ${{ env.TASK_DEFINITION_NAME }} \
            --query "taskDefinition.revision" \
            --output text)

          aws ecs update-service \
            --cluster ${{ env.ECS_CLUSTER_NAME }} \
            --service ${{ env.ECS_SERVICE_NAME }} \
            --task-definition ${{ env.TASK_DEFINITION_NAME }}:$NEW_REVISION \
            --force-new-deployment

Note: Replace the followings <placeholders> with the actual values:

<your-ecr-repo-name>ECR repository name (eg, ecr-repo-for-app)
<your-task-def-name>ECS task definition name (eg, my-app-task-definition)
<your-ecs-cluster-name>ECS cluster name (eg, flask-ecs-cluster)
<your-ecs-service-name>ECS service name (eg, my-app-ecs-service)

Push Workflow to GitHub

Commit and push the new workflow file to trigger the deployment:

git add .github
git commit -m "Add GitHub Actions workflow for ECS deployment"
git push origin main

Step 7: Verify Deployment

  • In GitHub, go to the Actions tab, check the workflow status; it should complete without errors.
Successful ECS Deployment With GitHub Actions
Successful ECS Deployment With GitHub Actions

Step 8: Test Deployed Flask App

  • In the AWS ECS Console, navigate to Clusters → <your-ecs-cluster> → Services → <your-ecs-service-name>.
Desired Amount of Running Tasks in the 'Tasks' tab
Desired Amount of Running Tasks in the ‘Tasks’ tab
  • In the Tasks tab, click on the running task, then scroll down to the Network section to find the Public IP address.
Viewing the 'Network' Section Of a Running Task For Public IP
Viewing the ‘Network’ Section Of a Running Task For Public IP
  • Open the browser and visit: http://<public-ip>:5000.
Successful Flask App Deployment Via AWS ECS
Successful Flask App Deployment Via AWS ECS

Conclusion

Effective cloud deployments require consistency, scalability, and minimal manual intervention. This guide illustrates how integrating containerization, managed orchestration, and automation can help achieve these objectives.

The deployment process outlined using Docker, AWS ECS, and GitHub Actions ensures environments remain synchronized, updates are applied automatically, and applications are delivered efficiently in production.