The CI/CD setup a three-person startup team actually needs on AWS — the reference GitHub Actions pipeline, how many environments, where secrets go, how to roll back in under five minutes, and what it costs to run.
A three-person team needs four things from CI/CD, and nothing else: every merge to main runs the tests, a passing build deploys itself to staging without anyone touching a terminal, a human approves the release to production, and a bad release can be undone in minutes. That is roughly two days of setup on AWS with GitHub Actions, and it costs most small teams under ₹3,000 a month to run. Everything beyond those four things — self-hosted runners, GitOps controllers, blue/green traffic shifting, a service mesh — is a solution to a problem you do not have yet. This guide is the setup we build for small teams, with the config.
Key takeaways: Start with one pipeline that tests, builds once, and deploys the same artifact to staging and then production behind a manual approval. Use two environments, not four. Authenticate GitHub to AWS with OIDC rather than stored access keys. Make rollback a redeploy of the previous image, and rehearse it before you need it. Cost and timing figures below are INFOCRUD planning ranges reviewed in August 2026, not a quote.
What a small team actually needs from CI/CD
The purpose of a pipeline at this size is not speed. It is removing the human from the parts of a release where humans are unreliable — running the tests, remembering the build steps, deploying the exact thing that was tested. A small team ships less often than a large one, which makes each release riskier, and manual deploys are where that risk lives.
Five capabilities cover it:
- Every merge is tested. Lint and tests run on the pull request, and a failing check blocks the merge. This is the half of CI/CD that pays for itself first
- The artifact is built once. One container image, tagged with the commit SHA, promoted unchanged from staging to production. Rebuilding per environment means you never actually tested what you shipped
- Staging deploys automatically. No approval, no ceremony — merged code is running somewhere real within about ten minutes
- Production needs one click. A named person approves, and the approval is recorded against the commit
- Rollback is one command. Not a hotfix, not a revert-and-rebuild — a redeploy of the last known-good image
What is genuinely overkill before roughly ten engineers or meaningful compliance pressure:
- Self-hosted runners. They save money only at high build volume, and they add a machine you now have to patch and secure
- Kubernetes, if you are not already running it. ECS Fargate or App Runner deploys the same container with a fraction of the operational surface
- Blue/green or canary deployments. A rolling update with health checks handles almost everything a small team ships
- Four environments. Dev, QA, UAT, and production is an enterprise shape that mainly produces four ways for configuration to drift apart
The reference setup: GitHub Actions to AWS
The shape below is the one we hand to small client teams, and the same release discipline we hold ourselves to on OffyBox, the multi-tenant B2B platform we build and operate: GitHub Actions for the pipeline, ECR for images, ECS Fargate for the runtime, and no long-lived AWS credentials anywhere in GitHub.
It runs as three jobs — test, deploy to staging, deploy to production — where each one only starts if the previous one passed.
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test
staging:
needs: test
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deploy
aws-region: ap-south-1
- uses: aws-actions/amazon-ecr-login@v2
id: ecr
- name: Build and push image
id: build
run: |
IMAGE=${{ steps.ecr.outputs.registry }}/app:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
echo "image=$IMAGE" >> $GITHUB_OUTPUT
- uses: aws-actions/amazon-ecs-render-task-definition@v1
id: taskdef
with:
task-definition: infra/ecs/staging.json
container-name: app
image: ${{ steps.build.outputs.image }}
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.taskdef.outputs.task-definition }}
cluster: staging
service: app
wait-for-service-stability: trueThe production job is the same deploy steps against the production cluster, pointed at the image the staging job already built rather than a fresh docker build. That single detail is what makes the promotion honest: the bytes running in production are the bytes that passed staging.
Three lines in that file do more work than they look like they do.
permissions: id-token: write enables OIDC. GitHub asks AWS for a short-lived token scoped to a role you control, so there is no AWS_ACCESS_KEY_ID stored in the repository to leak, rotate, or forget about. Setting it up is one IAM identity provider and one role with a trust policy restricted to your repository — and it is the single highest-value hour in this entire setup.
environment: production is a GitHub environment, and it is where you attach required reviewers. That is the approval gate; you do not need a separate release tool for it. wait-for-service-stability: true then means the job fails if the new tasks never become healthy — otherwise a broken release reports green while ECS quietly rolls it back behind you.
Environments: how many, and why staging is not optional
Two deployed environments. Staging and production. Local development is the third, and it does not need to live on AWS.
| Environment | Deployed by | Data | What it is for |
|---|---|---|---|
| Local | The developer | Seeded fixtures | Writing and running the code |
| Staging | Automatically, on every merge to main | Anonymised or synthetic, never a production copy | Proving the deploy path and catching integration failures |
| Production | Manual approval on a promoted image | Real | Users |
Founders under cost pressure regularly ask whether staging can be dropped. It is the wrong saving. Staging is not there to be a testing environment — it is there to prove that the deployment mechanism works before you use it on customers. Without it, every production deploy is also the first time that release path has been exercised, and you are debugging two things at once at the worst possible moment. It is also cheaper than it feels: staging runs on the smallest sizes that work and switches off outside working hours on a schedule, which removes roughly two-thirds of its runtime cost — the same non-production scheduling covered in our AWS cost optimization guide.
The rule that keeps two environments from drifting: staging and production differ only in scale and data, never in configuration shape. Same task definition structure, same secret names, same infrastructure code, different values.
Secrets: the mistake almost everyone makes
The mistake is not putting secrets in Git. Most teams know not to do that. The mistake is baking secrets into the container image — passing them as --build-arg, or copying a .env file in during the build.
It fails quietly because the app works. But the values are now in the image layers — in ECR, in every developer's local Docker cache, and in any registry the image is ever copied to. Rotating them means rebuilding and redeploying every image that contains them, and you cannot audit who read them.
Secrets belong in one of these places, injected at runtime:
- AWS Secrets Manager for anything that rotates or is genuinely sensitive — database credentials, payment keys, signing secrets. It charges about $0.40 per secret per month plus a small per-call fee, and it supports automatic rotation for RDS
- SSM Parameter Store for the rest — feature flags, endpoints, non-sensitive configuration. Standard parameters are free, and
SecureStringparameters are encrypted with KMS - GitHub Actions secrets only for what the pipeline itself needs, which after you adopt OIDC is close to nothing
In ECS you reference them in the task definition under secrets rather than environment, and ECS injects the value at task start. The container never sees the secret at build time, rotation is a value change plus a restart, and every read is logged in CloudTrail. One matching rule worth adopting on day one: no human has standing production database credentials. Access goes through a role that can be granted and revoked, and the pipeline is the normal way changes reach production.
Rollback in under five minutes
Every team says it can roll back. Most have never tried it under pressure, which is when they discover their rollback is "revert the commit and wait for a full build" — twenty minutes of a broken release while the pipeline runs the tests again.
Because the pipeline builds one immutable image per commit and keeps it in ECR, rollback is a redeploy of the previous task definition revision. No build, no tests, no branch surgery:
# list recent revisions
aws ecs list-task-definitions --family-prefix app --sort DESC --max-items 5
# redeploy the previous known-good revision
aws ecs update-service \
--cluster production \
--service app \
--task-definition app:41 \
--force-new-deploymentThat is typically 2–4 minutes to healthy tasks on a small Fargate service. Three things make it dependable:
- Enable the ECS deployment circuit breaker with rollback, so a release whose tasks never pass health checks reverts on its own without anyone being paged
- Keep database migrations backward-compatible for one release. This is the real constraint — code rolls back in minutes, a dropped column does not. Add columns before you use them, remove them a release later
- Rehearse it quarterly, on a Tuesday morning, in production. A rollback nobody has performed is a hypothesis
What it costs to run
For a small team on a private repository, this is the monthly picture:
| Line item | Typical cost | Notes |
|---|---|---|
| GitHub Actions | ₹0 to ~₹1,000 | Free plan includes 2,000 minutes/month for private repos; Team includes 3,000. Beyond that, standard Linux runners bill about $0.008/minute |
| ECR storage | ~₹100–300 | $0.10 per GB-month. A lifecycle policy keeping the last 20 images stops this growing |
| Staging compute | ~₹1,500–3,000 | Smallest workable Fargate sizing, scheduled off outside working hours |
| Secrets Manager | ~₹200–500 | About $0.40 per secret per month; Parameter Store standard parameters are free |
| Setup effort | 2–4 working days | OIDC role, pipeline, two environments, rollback rehearsal |
Cost and effort figures are INFOCRUD planning ranges reviewed in August 2026 for a single small service; your build minutes and image sizes will move the compute lines most. The comparison worth making is not against zero — it is against the hours currently spent on manual deploys, plus the cost of one bad release that took an afternoon to unpick.
How this rolls out
- Phase 01Day 1 — CI only. Tests and lint run on every pull request, and a failing check blocks the merge. Nothing deploys yet. This alone catches most of what manual review misses, and it is safe to ship before anyone agrees on the deployment design.
- Phase 02Days 2–3 — staging, automatically. Create the OIDC role, build one image per commit to ECR, deploy to a staging cluster on every merge to
main. Move secrets out of the image and into Secrets Manager or Parameter Store while the stakes are still low. - Phase 03Day 4 — production, on approval. Add the production environment with required reviewers, promote the staging image rather than rebuilding, turn on the deployment circuit breaker, and rehearse a rollback before the first real release goes through it.
The order matters. Each phase is useful on its own, so a team that stops after phase two still ends up meaningfully better off than one that spent a month designing the perfect pipeline and shipped none of it.
Mistakes that make a pipeline worse than no pipeline
- Rebuilding the image per environment. Now staging and production are different artifacts, and "it worked in staging" stops meaning anything
- Long-lived AWS keys in GitHub secrets. They never get rotated, they are readable by anyone with repository admin, and OIDC removes the need for them entirely
- A test suite everyone ignores. One flaky test that fails a quarter of the time teaches the team to re-run until green, which is worse than having no gate at all — fix it or delete it the week it starts
- Deploying on a schedule instead of on merge. Batching a week of changes into one Friday release is exactly the pattern CI/CD exists to remove
- No migration discipline. The most common reason a rollback fails is a database change the previous version cannot read
Frequently asked questions
Do we need CI/CD if we deploy once a week?
Yes, and arguably more than a team deploying daily. Infrequent releases are large, and large releases are where manual deploy steps get skipped or misremembered. Start with the CI half — tests on every pull request — which is valuable regardless of how often you ship.
GitHub Actions, GitLab CI, or Jenkins?
Use whatever is attached to where your code already lives. For teams on GitHub, Actions needs no separate service to host, secure, or upgrade. Jenkins is a server you now operate; that is a reasonable trade at scale and a poor one at three people.
ECS, App Runner, or Kubernetes?
ECS Fargate is the default we recommend for small teams — containers without managing nodes, and it integrates directly with the deploy actions above. App Runner is simpler still for a single stateless web service. Choose Kubernetes when you have a specific requirement it answers, not as a starting point.
Can we add this to an application that is already live?
Usually yes, and the sequence is the same. The one prerequisite is a repeatable build — if the current deployment involves steps that only exist on someone's laptop, capturing those honestly is the first task, and teams often find that exercise more valuable than the pipeline itself.
Who maintains it after it is built?
It needs an owner, but not a full-time one. Realistically this is a few hours a month — dependency updates on the actions, image lifecycle policies, and reviewing what the pipeline is telling you about flaky tests. That ongoing ownership is one of the things a managed DevOps engagement is normally scoped to cover.
The bottom line
CI/CD for a small team is not a platform project. It is one pipeline file, one IAM role, two environments, and a rollback you have actually practised. Build the artifact once, promote it unchanged, keep secrets out of the image, and make undoing a release cheaper than debugging it live. Two to four days of work, under ₹3,000 a month to run, and it removes the class of failure where the deploy itself is the incident.
If your releases still involve someone SSH-ing into a server, or you have a pipeline nobody trusts enough to use on a Friday, that is worth an outside look. You can explore Managed DevOps to see how release, cloud, and observability work fits together, or book a free 30-minute infrastructure review and we will tell you what your current path to production is actually costing you.



