DH
4 min read

CI/CD Pipeline for a Dockerized Full-Stack App: GitHub Actions to ECS

Production-ready GitHub Actions workflow that builds Docker images, pushes to ECR, and deploys to ECS Fargate—eliminating manual deploys and 11pm hotfixes.

CI/CD Pipeline for a Dockerized Full-Stack App: GitHub Actions to ECS

If you've manually SSH'd into a server for an 11pm hotfix, you already know why automating deployment matters. A solid CI/CD pipeline isn't optional—it's how you stop being your own bottleneck.

This guide walks through a production-ready GitHub Actions pipeline that builds Docker images, pushes them to Amazon ECR, and deploys to ECS Fargate.

What We're Deploying

A typical full-stack setup:

  • Frontend: Next.js (containerized)
  • Backend: FastAPI or Django (containerized)
  • Database: PostgreSQL on RDS (uncontainerized—don't run stateful databases in ECS)
  • Orchestration: ECS Fargate with two separate services

Both containers live in the same ECR namespace, tagged by service and git SHA.

Prerequisites

  • An ECS cluster and task definitions (frontend, backend) already created
  • Two ECR repositories: myapp/frontend, myapp/backend
  • An IAM OIDC role with ECR push and ECS deploy permissions
  • GitHub repository with secrets configured

Repository Structure

.github/workflows/deploy.yml
frontend/Dockerfile
backend/Dockerfile

The GitHub Actions Workflow

name: Build and Deploy to ECS

on:
push:
branches: [main]

env:
AWS_REGION: us-east-1
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}

jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read

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

- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Build and push frontend
env:
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/myapp/frontend:$IMAGE_TAG ./frontend
docker push $ECR_REGISTRY/myapp/frontend:$IMAGE_TAG

- name: Build and push backend
env:
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/myapp/backend:$IMAGE_TAG ./backend
docker push $ECR_REGISTRY/myapp/backend:$IMAGE_TAG

- name: Deploy frontend to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: frontend-task-def.json
service: frontend-service
cluster: myapp-cluster
wait-for-service-stability: true

- name: Deploy backend to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: backend-task-def.json
service: backend-service
cluster: myapp-cluster
wait-for-service-stability: true

OIDC Instead of Static Keys

We use role-to-assume rather than long-lived IAM keys. Static credentials rotated poorly (or not at all) are a genuine liability. OIDC lets GitHub assume a short-lived token—no credentials sitting in secrets.

Setup takes ~15 minutes: configure an OIDC identity provider in IAM and create a role trusting token.actions.githubusercontent.com. Worth it.

Image Tagging with Git SHA

Using github.sha as your tag gives traceability. Every deployment maps to an exact commit. When production breaks, you know what code is running and can rollback instantly.

Never use :latest on production images. It breaks your rollback path and makes debugging harder.

Task Definition File

The amazon-ecs-deploy-task-definition action needs a local JSON file. Generate it once:

aws ecs describe-task-definition \
--task-definition frontend-task-def \
--query taskDefinition > frontend-task-def.json

Commit this file. The action updates the image URI and registers a new revision automatically.

Adding a Test Gate

A deployment pipeline without tests is just automated breakage. Add this job before build-and-deploy:

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run backend tests
run: |
cd backend
pip install -r requirements.txt
pytest

Make build-and-deploy depend on it:

build-and-deploy:
needs: test

Failed tests now block deployment. That's the point.

Environment-Specific Deployments

For staging vs. production, use branch or tag conditions. A common pattern: main deploys to staging; tags deploy to production:

on:
push:
branches: [main] # staging
tags: ['v*'] # production

Use conditionals on deploy steps to target the right cluster based on github.ref.

What You Get

This pattern handles what actually matters in production: auditability via SHA tagging, security via OIDC, and confidence via a mandatory test gate. It's simple enough to reason about at 2am when something breaks.

Natural next steps: Docker layer caching for faster builds, Slack notifications, and AWS Secrets Manager instead of environment variables in task definitions.

Get the basics working first. Optimize when you have evidence of what's slow.

Damian Hodgkiss

Damian Hodgkiss

Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.

Creating Freedom

Join me on the journey from engineer to solopreneur. Learn how to build profitable SaaS products while keeping your technical edge.

    Proven strategies

    Learn the counterintuitive ways to find and validate SaaS ideas

    Technical insights

    From choosing tech stacks to building your MVP efficiently

    Founder mindset

    Transform from engineer to entrepreneur with practical steps