What we’re building
This guide walks through building a production-ready deployment pipeline for a typical web application. By the end, you’ll have:
- Automated testing on every pull request
- Docker containerisation with multi-stage builds
- Infrastructure defined in Terraform
- Automated deployment to AWS on merge to main
- Monitoring and alerting for production issues
The stack: GitHub Actions for CI/CD, Docker for containerisation, Terraform for infrastructure, and AWS (ECS Fargate) for hosting. These are the tools we use at ELASYN for most client projects because they’re battle-tested, well-documented, and cost-effective.
Week 1: Foundation
Day 1–2: Containerisation
Start with a Dockerfile. Multi-stage builds keep your production image small by separating the build environment from the runtime:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["yarn", "start"]
Test locally: docker build -t myapp . && docker run -p 3000:3000 myapp. If it works in the container, it’ll work in production.
Day 3–4: CI pipeline
Create .github/workflows/ci.yml:
name: CI
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: yarn install --frozen-lockfile
- run: yarn lint
- run: yarn type-check
- run: yarn test
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- run: docker build -t myapp .
Every pull request now runs linting, type checking, tests, and verifies the Docker build succeeds. No broken code merges to main.
Day 5: Infrastructure
Define your AWS infrastructure in Terraform. The core resources for an ECS Fargate deployment:
- VPC with public and private subnets across two availability zones
- Application Load Balancer with HTTPS termination
- ECS Cluster and Service running your Docker containers
- ECR Repository for storing container images
- RDS PostgreSQL in private subnets
- CloudWatch Log Group for container logs
Each resource is defined in code, version-controlled, and reproducible. Spinning up a new environment is terraform apply -var="environment=staging".
Week 2: Deployment and monitoring
Day 6–7: Deployment pipeline
Create .github/workflows/deploy.yml for automated production deployment:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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: ap-southeast-2
- uses: aws-actions/amazon-ecr-login@v2
- run: |
docker build -t myapp .
docker tag myapp:latest $ECR_REGISTRY/myapp:$GITHUB_SHA
docker push $ECR_REGISTRY/myapp:$GITHUB_SHA
- run: |
aws ecs update-service \
--cluster production \
--service myapp \
--force-new-deployment
Merge to main → build Docker image → push to ECR → deploy to ECS. The entire process takes 4–6 minutes.
Day 8–9: Zero-downtime deployments
ECS rolling deployments ensure zero downtime:
- New task starts with the updated image
- Load balancer health checks verify the new task is healthy
- Traffic shifts to the new task
- Old task drains connections and stops
Configure the deployment in your ECS service definition:
- Minimum healthy percent: 100% (old tasks stay running until new ones are healthy)
- Maximum percent: 200% (allows running both old and new simultaneously)
- Health check grace period: 60 seconds
Day 10: Monitoring and alerting
A deployment pipeline without monitoring is like driving without a dashboard. Set up:
CloudWatch Alarms:
- CPU utilisation > 80% for 5 minutes
- Memory utilisation > 85% for 5 minutes
- 5xx error rate > 1% for 3 minutes
- Response time P95 > 2 seconds for 5 minutes
Structured logging. Every log entry includes: timestamp, request ID, user ID, action, duration, and outcome. When something breaks, you can trace the exact request through the entire system.
Health check endpoint. A /health endpoint that verifies the application can connect to its database and essential services. The load balancer uses this to route traffic only to healthy instances.
What this costs
For a typical web application serving 10,000–50,000 requests per day:
- ECS Fargate (2 tasks, 0.5 vCPU, 1GB RAM): ~$50/month
- ALB: ~$25/month
- RDS (db.t4g.small): ~$30/month
- ECR: ~$5/month
- CloudWatch: ~$10/month
- GitHub Actions: Free for public repos, ~$15/month for private
Total: approximately $135/month for a production environment with auto-scaling, zero-downtime deployments, and monitoring. Compare this to the cost of manual deployments and the risk of downtime.
Common mistakes to avoid
Skipping staging. Deploy to a staging environment first, always. Use the same pipeline, the same Docker image, and the same infrastructure configuration. The only differences should be environment variables and resource sizes.
Hardcoding configuration. Database URLs, API keys, and feature flags belong in environment variables, not in your code or Docker image. Use AWS Parameter Store or Secrets Manager for sensitive values.
Ignoring the database. Your deployment pipeline deploys application code, but database migrations need their own strategy. Run migrations as a separate step before the application deployment, with rollback scripts prepared.
No rollback plan. If the new deployment breaks, you need to be back to the previous version in under 2 minutes. Keep the previous Docker image tagged and ready. ECS makes rollback a single command: revert the task definition to the previous revision.
The pipeline described here has been running in production for multiple ELASYN clients. It handles everything from a marketing site with 1,000 daily visitors to a SaaS platform processing 100,000 API calls per day. The infrastructure scales; the pipeline doesn’t change.