Article URL Copied to Clipboard!
Home Blog Cloud & DevOps CI/CD for Small Teams: A Simple Pipeline...
Cloud & DevOps

CI/CD for Small Teams: A Simple Pipeline That Stops Broken Releases

In a lot of small teams, the deployment process is a person. One developer knows which commands to run on the server, in which order, and what to do when...

codesalen
codesalen
September 30th 2026 • 7 mins, 1376 words
CI/CD for Small Teams: A Simple Pipeline That Stops Broken Releases

In a lot of small teams, the deployment process is a person. One developer knows which commands to run on the server, in which order, and what to do when npm install hangs. It works until that person is on leave, or tired on a Friday evening, and a release goes out without the database migration it needed.

A CI/CD pipeline replaces that person's memory with a written, repeatable process. It does not need to be elaborate. For a team of two to ten developers, a pipeline with four stages catches the usual causes of broken releases, and it can typically be set up in a day or two.

What a CI/CD pipeline is actually for

Two jobs, nothing more:

  • Continuous integration (CI) stops broken code from reaching your main branch. Every pull request is installed, checked and tested automatically, the same way every time.
  • Continuous delivery (CD) makes releasing boring. Code that passed CI is deployed by a script, first to staging and then to production, without anyone typing commands on a server.

The test of a good pipeline is simple: could a developer who joined last week ship a small fix safely, without asking anyone which steps to follow? If the answer is no, the process still lives in someone's head.

The four stages a small team needs

  1. Check every pull request. Install dependencies from the lockfile, run the linter or static analysis, run the tests, and build the app. If any step fails, the pull request cannot be merged.
  2. Make those checks mandatory. A check that people can ignore will be ignored on the busiest day. Branch protection turns it into a rule.
  3. Deploy main to staging automatically. Every merge lands on a staging server within minutes, so the team and the client can see it working before customers do.
  4. Deploy to production deliberately. Production is a separate, explicit step, such as a button or a version tag, using the same script as staging, followed by a health check and a clear way to roll back.

That is the whole design. Everything else is refinement.

A working example with GitHub Actions

Here is a minimal workflow for a Node.js web app, saved as .github/workflows/ci.yml. The same shape works for a Laravel app with composer install and php artisan test in place of the npm steps.

name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    concurrency:
      group: test-${{ github.ref }}
      cancel-in-progress: true
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build

  deploy-staging:
    needs: test
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-staging
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v7
      - run: ./scripts/deploy.sh staging
        env:
          DEPLOY_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}

What each part is doing:

  • npm ci installs exactly what is in the lockfile, so CI tests the same dependency versions production will run.
  • cache: npm keeps downloaded packages between runs, which usually shortens every build.
  • timeout-minutes stops a hung test from quietly using up your build minutes.
  • The test job's concurrency block cancels an older run when someone pushes again to the same branch, because only the latest commit matters.
  • The deploy job has its own concurrency group with cancel-in-progress: false. A deployment stopped halfway is worse than one that finishes, so deploys queue instead of cancelling each other.
  • Secrets such as the SSH key come from the repository's encrypted secrets, never from a file in the repository.

The deploy script itself should be dull and idempotent: build or pull the new version, run migrations, switch traffic to it, and check a health URL. If the health check fails, the script exits with an error and the pipeline shows red, which is exactly what you want to see.

Make the checks mandatory

In GitHub, a branch protection rule on main with "Require status checks to pass before merging" means a pull request cannot be merged until the test job succeeds. GitHub's documentation on protected branches also describes a strict mode, on by default, that requires the branch to be up to date with main before merging. It produces more builds, but it catches the case where two pull requests pass separately and break together.

Check your plan first. Protected branches are available on public repositories with GitHub Free, and on private repositories with GitHub Pro, Team or Enterprise. If your code is private and on the free plan, this is one of the few places where paying for a plan is directly worth it.

Production deploys: approval without ceremony

You want a human decision before production, but not a meeting. GitHub offers environments with required reviewers, where a deployment job waits until one of up to six listed users or teams approves it. The catch, per GitHub's deployments and environments reference: on the Free, Pro and Team plans, required reviewers only work on public repositories, and environment secrets in private repositories need Pro, Team or Enterprise.

For a private repository on a smaller plan, a simpler pattern works well:

  • Put the production deploy in its own workflow, triggered by workflow_dispatch (a "Run workflow" button) or by pushing a version tag such as v1.4.0.
  • Remember that anyone with write access to the repository can run a manual workflow, so keep write access to the people who should be able to deploy.
  • Use a separate production SSH key or cloud credential with only the permissions the deploy needs.
  • Keep the previous release on the server, so rolling back is one command rather than a rebuild.

If you deploy containers, the same idea applies: CI builds and tags an image once, and staging and production run that exact image. That is one of the main reasons to adopt Docker in a small team, but a plain script on a single server is fine too.

What a CI/CD pipeline costs to run

For most small teams, the answer is very little. According to GitHub's Actions billing documentation, Actions is free for public repositories on standard GitHub-hosted runners, and the GitHub Free plan includes 2,000 minutes a month for private repositories (3,000 on Pro and Team). The minutes reset each billing cycle.

A quick estimate for a hypothetical team of four developers:

  • 40 pull requests a month, about 4 pushes each, 6 minutes per test run: 40 × 4 × 6 = 960 minutes
  • 40 merges to main, 8 minutes each for test plus staging deploy: 320 minutes
  • Total: roughly 1,300 Linux minutes, inside the free allowance

The number to watch is macOS. GitHub lists Linux runners at $0.006 per minute and macOS runners at $0.062, so a 15-minute iOS build costs about the same as 155 Linux minutes. For mobile apps, build iOS on merges to main and on release tags, not on every push. The same pipeline can then upload builds to TestFlight and the Google Play testing tracks automatically, which pays off in the weeks covered by our app launch checklist. Prices change, so check GitHub's current rates before you budget.

What to leave out for now

Small teams often copy pipelines designed for companies with a platform team. You probably do not need, yet:

  • Kubernetes, unless you already run several services that need independent scaling.
  • A test matrix across five runtime versions when production runs exactly one.
  • A coverage target such as 90%. Tests on the paths that lose money or data matter more than a percentage.
  • Several long-lived environments. One staging environment that mirrors production is enough for most teams.

And if your whole product is a brochure site on managed hosting that already deploys from Git, you may not need a custom pipeline at all. A linter on pull requests and the host's built-in deploy can be the right answer.

A good first week: add the pull request checks, turn on branch protection, write the deploy script and point it at staging. Production can follow once staging deploys have been dull for a couple of weeks. If you want a second pair of eyes, our cloud and DevOps engineers can review your current release process and set up a pipeline like this one with GitHub Actions. If you would rather start with a question, describe your current setup and we will tell you whether it needs changing at all.

Author

codesalen

Senior Software Architect & Solutions Lead

Passionate about enterprise web systems, high-scale cloud infrastructure, and modern AI engineering. Leading technical innovations at Codesalen.

💬 Comments (0)

Be the first to share your thoughts on this article!

Leave a Comment

Share this article:
Need Help? Ask CodeSalen AI Online ×

CodeSalen AI

Active • Solutions Advisor
Call Solutions Lead

Search CodeSalen