Building better software, one stack at a time.

Tutorials

How to Set Up CI/CD for a Node.js App with GitHub Actions

How to Set Up CI/CD for a Node.js App with GitHub Actions

A manual deploy is a quiet sort of technical debt. You SSH in, run the tests you half-remember, pull the latest commit, restart the process, and hope the lockfile on the server matches the one in your editor. It works for a while. Then it's Friday afternoon, someone else's branch is live, and nobody can say which commit is running.

GitHub Actions replaces that ritual with something duller and better: every push runs your tests, and every merge to your main branch ships a build. No new servers, no third-party account, and no YAML wizardry beyond what you actually need. Here's a practical path from manual deploys to a pipeline you can trust.

What the pipeline should do

Resist building everything at once. A useful first pipeline has three stages, in order:

  1. Test — install dependencies and run the test suite on every push and pull request.
  2. Build — compile or bundle the app, or simply confirm it starts without crashing.
  3. Deploy — ship to staging or production, but only from your main branch, and only after the first two pass.

That separation matters. Tests should be fast and always run; deploys should be deliberate and gated. Folding them into one script is how teams end up with a green tick next to a broken release.

Before you write any YAML

Ten minutes of preparation saves an afternoon of red crosses.

  • Commit your lockfile. npm ci refuses to run without one, and that is a feature rather than a nuisance.
  • Pin a Node version, either with an .nvmrc file or an engines field in package.json, so CI and your laptop agree.
  • Check that test, build and start exist as scripts in package.json. Your workflow will call those names, not raw commands.
  • Decide where secrets live. Deployment keys belong in repository or environment secrets, never in the workflow file.

One more check: does your test suite exit cleanly? If it hangs on your machine, it will hang on a runner, and you will spend an hour blaming GitHub.

Step one: tests on every push

Workflows live in .github/workflows/. Create a file there — call it ci.yml — with a name, a trigger block, and one job.

The trigger is the easy part: on: push for every branch, plus pull_request so contributors get feedback before anyone reviews the diff.

The job needs runs-on: ubuntu-latest and a short list of steps. First, check out the code with actions/checkout. Then set up Node with actions/setup-node, passing your version and setting the cache option to npm — that alone trims minutes off most builds by reusing downloaded packages between runs. Install with npm ci, then run npm test.

Pin the actions to a major version (@v4 rather than a floating tag) so a breaking change upstream doesn't wake you on a Tuesday morning.

Testing more than one Node version

If you maintain a library, or your team is midway through an upgrade, a matrix is worth the four extra lines. List two versions — say 20 and 22 — and GitHub runs the job once per version, in parallel. When the older one fails, you know precisely which release dropped support for the API you're using, instead of guessing from a production error later.

Step two: add build and deploy jobs

Once tests have been green reliably for a week, add the rest. Use needs so the deploy job only runs when the test job succeeds.

Write a deploy job with needs: test and a condition limiting it to your main branch. If you deploy over SSH, add the private key to repository secrets, write it to a file with the ssh-agent action, then use rsync to push the built output to the server. After shipping, run whatever restart command your setup uses and check the app responds — a deploy that doesn't verify itself is only half a deploy.

Attach the job to a GitHub environment, for example "production". That gives you a deployment history, and you can require manual approval before the job runs. For most teams, a human clicking a button once a week is a small price for not discovering a bad migration at midnight.

Two details make deploys calmer. Add a concurrency group so two pushes in quick succession can't deploy on top of each other; GitHub will queue or cancel the older run. And make the deploy idempotent — running it twice should leave you in the same state, not halfway through a broken one.

The parts people forget

  • Secrets and forks. Secrets aren't passed to workflows triggered by pull requests from forks. That's deliberate, so design your tests to run without production credentials.
  • Branch protection. In repository settings, require the test job as a status check before merging. A pipeline nobody gates on is decoration.
  • Environment variables. Tests that pass locally with a .env file will fail in CI. Set non-secret values in the workflow, and keep a small .env.example for new starters.
  • Case sensitivity. macOS and Windows treat File.js and file.js as the same file. Linux runners do not.
  • Time limits. Set a sensible timeout on each job so a hung test doesn't occupy a runner for the default maximum.

When it goes wrong

Most first failures are dull ones. npm ci fails when the lockfile is out of sync with package.json — run npm install locally, commit the updated lockfile, push again. A test that passes locally but fails in CI usually means a missing environment variable, or a file path that only exists on your machine. A workflow that never triggers is usually an indentation error in the YAML, or a branch filter that doesn't match. Read the log from the top; the real error is rarely the last line.

Keep it boring

The goal isn't an impressive pipeline. It's one you stop thinking about. Tests on every push, a deploy behind a button, secrets in the right place, and a running version you can trace back to a commit. Start with the test job this afternoon, watch it go green a few times, then add the deploy step once you trust it. Within a month you'll have forgotten what the manual version felt like, which is exactly the point.

Photo: jamesmarkosborne / Pixabay

Related Posts

A Beginner's Guide to UK GDPR for Web Developers: Cookies, Consent and Data Requests

Web Development

A Beginner's Guide to UK GDPR for Web Developers: Cookies, Consent and Data Requests

Practical UK GDPR guidance for small websites: how cookie consent really works, what to record, and how to answer...

How to Prepare Your E-commerce Site for the UK Christmas Shopping Rush

Web Development

How to Prepare Your E-commerce Site for the UK Christmas Shopping Rush

A practical guide to load testing, caching, payment limits, stock messaging and calm launch freezes so your shop...