A Pragmatic CI/CD Setup for Small Product Teams

Skip the over-engineered pipelines. Here is the minimal CI/CD stack that has served our client teams well for years.

Most 5-engineer teams do not need a dedicated platform engineer or a custom pipeline DSL. They need a setup that is boring, reliable, and ships code safely to production every day. Here is the stack we set up by default on new client engagements.

The shape of the pipeline

  1. Push to a branch → CI runs lint, typecheck, unit tests.
  2. Open a PR → add integration tests, build Docker image, run security scans.
  3. Merge to main → deploy to staging automatically.
  4. Tag a release → deploy to production with manual approval.

That's it. Four stages, each doing one thing well.

Tools we use

  • GitHub Actions — native to the source code, generous free tier, easy secrets management.
  • Docker + multi-stage builds — smaller images, better caching, consistent runtime.
  • GHCR or ECR — for container registry. GHCR is free and good enough for most.
  • Kubernetes (EKS/GKE) or Fly.io/Railway — depending on scale. Do not start with K8s unless you need it.
  • Terraform — for infra. Even if it is 200 lines, Terraform beats click-ops.
Boring tools compound. Flashy tools decay.

Speed matters — keep CI under 5 minutes

A slow CI kills developer velocity. Our targets:

  • Unit tests: under 2 minutes
  • Full CI on PR: under 5 minutes
  • Deploy to staging: under 3 minutes

To get there: cache node_modules/pip deps, run tests in parallel shards, and do not rebuild what has not changed.

Safety rails

  • Branch protection: require status checks and one review.
  • Auto-revert: if staging smoke tests fail, the pipeline rolls back.
  • Feature flags (via LaunchDarkly, Unleash, or your own table) so risky features can ship dark.
  • Database migrations: always additive first, backfill, then drop later.

Observability from day one

Ship logs to a log aggregator (we like Grafana Loki or Datadog), metrics to Prometheus, and error tracking to Sentry. Wire up alerting on the three things that actually matter: error rate, latency, and deployment failures. Add more alerts only after you have ignored a notification twice.

What we skip on purpose

  • Complex canary deployments (staging + feature flags cover 90% of the value).
  • Multi-cloud abstractions (pick one cloud and commit).
  • A dedicated CI tool separate from the source host (GitHub Actions is plenty).

A concrete GitHub Actions workflow

Theory is cheap, so here is roughly what the YAML looks like for a typical Node service. The whole thing lives in .github/workflows/ci.yml and stays under a hundred lines.

  • Trigger on push and pull_request so every branch and PR is checked.
  • A test job that checks out the code, runs actions/setup-node with a cached ~/.npm, then runs npm ci && npm run lint && npm test.
  • A build job that depends on test, builds the Docker image with docker/build-push-action, and pushes it to GHCR tagged with the commit SHA.
  • A deploy job gated by GitHub Environments so production requires a human click, while staging deploys on every merge to main.

If your team is on GitLab instead, the same four stages map almost one-to-one onto a .gitlab-ci.yml with stages: [test, build, deploy] and rules: blocks for branch filtering. The principles travel; only the syntax changes. The point is that any engineer on the team can open one file and understand the entire path from commit to production.

Testing strategy that respects the clock

CI is only as trustworthy as the tests it runs, but a small team cannot afford a 40-minute test suite. We lean on a deliberate pyramid:

  1. Many unit tests — fast, isolated, run on every push. These catch the bulk of regressions.
  2. A focused set of integration tests — hit a real database and real HTTP, but only run on PRs. Spin up Postgres and Redis as service containers so they mirror production.
  3. A thin layer of end-to-end smoke tests — Playwright or Cypress against staging after deploy. Test the three or four flows that, if broken, would have a customer calling you.

Flaky tests are the silent killer of CI culture. The moment a test fails randomly, engineers start ignoring red builds, and the whole safety net unravels. Quarantine flaky tests aggressively and fix or delete them within the week.

Rollbacks: the feature nobody plans until 2 a.m.

Every deploy strategy is really a rollback strategy in disguise. Because we tag images with the commit SHA and never mutate a tag, rolling back is just redeploying the previous SHA — no rebuild, no scramble. On Kubernetes that is a one-liner: kubectl rollout undo deployment/api. On Fly.io it is fly deploy --image <previous-sha>.

The best time to test your rollback is on a calm Tuesday afternoon, not during an incident. Practice it once a quarter so it is muscle memory, not a panic.

Pair that with the auto-revert on failed staging smoke tests mentioned above, and most bad deploys never reach a customer at all. The combination of immutable image tags, additive-first migrations, and feature flags means a rollback almost never requires touching the database — which is exactly the property you want when you are tired and the dashboard is red.

Secrets, security, and supply chain

Small does not mean careless. A few cheap habits keep you out of trouble:

  • Store secrets in GitHub Actions encrypted secrets or your cloud's secret manager — never in the repo, never in plaintext env files committed by accident.
  • Pin third-party Actions to a commit SHA rather than a floating @v3 tag, so a compromised release cannot silently run in your pipeline.
  • Run npm audit, pip-audit, or Dependabot, plus a container scan like Trivy, as part of the PR build.
  • Give the deploy job the narrowest cloud IAM role it can do its job with — scoped to one cluster or one app, nothing more.

This is the same DevOps discipline we bring to every engagement under our cloud and DevOps services, and increasingly we let AI tooling draft the first version of these pipelines so engineers can spend their review time on the parts that actually carry risk.

When to add complexity — and when not to

The setup above carries a team comfortably from launch to a few million requests a day. You should resist adding moving parts until a real, measured pain forces the issue. Good triggers to graduate:

  • Deploys are blocking each other → add a proper queue or merge train.
  • Staging no longer predicts production behaviour → invest in canary or blue-green.
  • One repo is becoming a bottleneck for many teams → consider a monorepo with affected-only builds.

Until then, every extra abstraction is a tax your five engineers pay in confusion. The goal is not the most sophisticated pipeline; it is the one your team understands completely.

This setup takes about a week to stand up on a new project and saves months of pain down the road. Keep it boring. If you would like a hand standing it up or auditing the one you have, get in touch — we do fixed-scope CI/CD engagements for exactly this.

Frequently Asked Questions

Do small teams really need CI/CD?

Yes, and arguably more than large ones. A small team has no spare capacity to absorb a broken release or a manual deploy that goes wrong at midnight. A simple pipeline that runs tests on every push and deploys safely pays for itself within the first prevented incident. You do not need a platform team to get there — a single well-written GitHub Actions or GitLab CI file is enough to start.

GitHub Actions or GitLab CI — which should I choose?

Use whichever lives next to your source code. If your repositories are on GitHub, use GitHub Actions; if they are on GitLab, use GitLab CI. Both are mature, both have a generous free tier, and both express the same four-stage pipeline of test, build, deploy to staging, and deploy to production. The integration with your existing host matters far more than any feature difference between the two tools.

How fast should my CI pipeline be?

Aim for unit tests under two minutes, a full pull-request check under five minutes, and a staging deploy under three minutes. Anything slower starts eroding developer velocity because people stop waiting for the build and start context-switching. You hit these targets by caching dependencies, running tests in parallel shards, and never rebuilding what has not changed.

What is the safest way to roll back a bad deploy?

Tag every container image with its commit SHA and never mutate a tag, so rolling back is simply redeploying the previous SHA with no rebuild required. On Kubernetes that is kubectl rollout undo, and on Fly.io it is a deploy pointed at the previous image. Combine this with additive-first database migrations and feature flags so a rollback almost never has to touch the database, and practise it on a calm day so it is muscle memory before you ever need it in an incident.

← Back to blog

Need A CI/CD Setup For Your Team?

We help small teams set up production-grade CI/CD in days, not months. Fixed-scope engagements available.

Get In Touch