Tutorial
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.
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.