Production-grade full-stack application containerised with Docker and deployed to AWS ECS Fargate. Infrastructure provisioned with Terraform, automated deployments via GitHub Actions CI/CD pipeline, and served securely over HTTPS with a custom domain.
- AWS (ECR, ALB, VPC, IAM, ECS) – used to provision and run the core infrastructure and services
- Terraform – provisions and manages AWS infrastructure as code
- Docker – containerises the app to ensure consistent environments across development and production
- GitHub Actions (CI/CD) – automates build, test, and deployment pipelines
- CloudWatch – handles logging and monitoring for the deployed services
- Cloudflare – manages DNS and adds an extra layer of security and performance (CDN, SSL)
- User accesses the application via custom domain
- DNS (Cloudflare) resolves to AWS Application Load Balancer
- Application Load Balancer terminates HTTPS using ACM certificate
- Traffic is routed to ECS service target group
- ECS Fargate task serves the application
- Application communicates with Supabase for database and authentication
Key Highlights
- CI/CD pipeline: commit → production in <10 minutes
- Secure architecture: private subnets, ALB-only access, HTTPS enforced
- Infrastructure as Code using modular Terraform
- Optimised Docker image (↓18% size reduction)
- Zero manual deployment steps — fully automated
| Environment | URL |
|---|---|
| Production | https://debtmates.ibrahimdevops.co.uk |
Demo walkthrough:
- Reduced Docker image size from 219MB to 179MB using a multi-stage build (18% reduction)
- End-to-end deployment from commit to live ECS service completes in under 10 minutes via CI/CD
- ECS tasks deployed in private subnets with no public IPs — only reachable via the ALB
- All traffic encrypted in transit via ACM-issued SSL certificate
- Least-privilege IAM roles scoped specifically to ECS task execution, following AWS security best practices
- Reduced manual deployment steps from ~15+ AWS Console actions to a single
git pushvia CI/CD automation
All infrastructure is provisioned with Terraform and organised into reusable modules. The following AWS resources are created:
| Resource | Description |
|---|---|
| VPC | Custom VPC with public and private subnets across eu-west-2a and eu-west-2b |
| NAT Gateway | Allows ECS tasks in private subnets to make outbound internet requests |
| Security Groups | ALB SG (80/443 public), ECS SG (port 3000 from ALB only) |
| ECR | Container registry for the Docker image |
| ECS Cluster | Fargate cluster running the application as a single container service |
| ALB | Internet-facing load balancer with HTTP → HTTPS redirect |
| ACM | SSL certificate for the custom domain (DNS validated) |
| IAM | Least-privilege task execution role for ECS |
| CloudWatch | Log group /ecs/app for container logs |
| S3 + DynamoDB | Remote Terraform state storage and state locking |
- ECS tasks run in private subnets with no public IPs
- Only ALB security group can access ECS service
- HTTPS enforced using ACM certificate
- IAM roles to follow least privilege principles
- Secrets stored in GitHub Action secrets
- Terraform state secured with S3 + DynamoDB locking
- Deployed across 2 Availability Zones
- ECS service can be scaled horizontally by increasing task count
- ALB distributes traffic across tasks
- Stateless container design allows easy scaling
Screenshots of AWS resources created with Terraform here
- Fargate chosen for simplicity over EC2
- NAT Gateway is the main cost driver
- Single service and minimal compute used to keep costs low
Builds the Docker image and pushes it to Amazon ECR.
Bootstraps the Terraform backend (S3 + DynamoDB) and applies the necessary IAM policies before running terraform fmt (formatting), terraform validate (validation) and tflint (linting). Then executes terraform init, terraform plan and terraform apply to provision or update infrastructure. Remote state is stored in S3 with DynamoDB state locking.
Only runs if the Terraform Deploy workflow completes with a success conclusion — skipped entirely on failure or cancellation. Hits the /health endpoint on both domains in parallel using a matrix strategy, with up to 5 retries and a 10 second timeout per request. Validates that the JSON response contains "status": "ok", failing the pipeline if it doesn't.

| Secret | Description |
|---|---|
AWS_ACCESS_KEY_ID |
IAM user access key |
AWS_SECRET_ACCESS_KEY |
IAM user secret key |
AWS_REGION |
e.g. eu-west-2 |
ECR_REGISTRY |
ECR registry URI |
ECR_REPOSITORY |
ECR repository name |
ECS_CLUSTER |
ECS cluster name |
ECS_SERVICE |
ECS service name |
VITE_SUPABASE_URL |
Supabase project URL |
VITE_SUPABASE_ANON_KEY |
Supabase anon key |
├── app/ # Full-stack application
│ ├── Dockerfile
│ ├── docker-compose.yaml
│ └── .dockerignore
├── infra/ # Terraform infrastructure
│ ├── modules/
│ │ ├── vpc/
│ │ ├── acm/
│ │ ├── ecr/
│ │ ├── ecs/
│ │ ├── alb/
│ │ └── iam/
│ ├── bootstrap/ # S3 + DynamoDB remote state backend
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── backend.tf
│ └── terraform.tfvars
├── .github/
│ └── workflows/
│ ├── docker-build-push-ecr.yaml # Build and push image to ECR
│ ├── terraform-deploy.yaml # Terraform provisioning
│ └── post-deploy-check.yaml # Post-deploy health check
└── README.md
git clone https://github.com/Mullah98/DebtMates.git
npm installCreate a .env file with your Supabase credentials:
VITE_SUPABASE_URL=your_supabase_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_keynpm run dev
# → http://localhost:5173npm run build
node server.js
# → http://localhost:3000docker compose up --build
# → http://localhost:3000For full setup instructions see the app README.
- Fargate over EC2 - eliminates server management overhead at the cost of higher compute pricing. Right choice for a project of this scale.
- Two Availability Zones - adds resilience but increases cost, particularly with NAT Gateways provisioned per AZ.
- NAT Gateway with Elastic IP - keeps ECS tasks private with no public IPs while allowing outbound traffic to external services.
- Remote state with S3 + DynamoDB - storing Terraform state remotely rather than locally ensures the state is never lost and prevents concurrent runs from corrupting it via DynamoDB state locking.
-
Docker & Environment Variables — Vite bundles env vars at build time, not runtime. Resolved by injecting Supabase credentials during the Docker build via Docker Compose.
-
ECS Health Checks Timing Out — ECS tasks were marked unhealthy due to a security group misconfiguration. The task SG only allowed TCP 80, but the app listens on port 3000. Fixed by restricting inbound TCP 3000 to the ALB security group only.
-
ACM Certificate Validation with Cloudflare — Both
ibrahimdevops.co.ukand*.ibrahimdevops.co.ukshare the same ACM validation record, causing duplicate CNAME errors in Cloudflare. Resolved by using the...ellipsis operator in the Terraformfor_eachloop to deduplicate validation records. -
Terraform State Lock Contention — Concurrent pipeline runs caused DynamoDB lock conflicts. Fixed by adding a
concurrencyblock to the Terraform workflow. -
Plan/Apply Consistency — Persisted the Terraform plan as a
tfplanartifact to guarantee the apply step executes exactly what was reviewed in the plan. -
IAM Permissions — Iteratively scoped permissions for the ECS task execution role and CI/CD OIDC role as Terraform surfaced missing permissions across modules.
- Add ECS autoscaling policies based on CPU/Memory
- Move secrets to AWS Secrets Manager for runtime injection
- Replace NAT Gateway with VPC endpoints to reduce costs
- Implement blue/green deployments for zero downtime releases




