Introduction
Continuous Integration catches bugs at merge time instead of production time. The cost of a failing test in CI is a few minutes of a developer's attention. The cost of the same bug in production is much higher. GitLab CI is built into GitLab, requires no separate service to run, and configures entirely through a YAML file in your repository.
What is Continuous Integration?
Continuous Integration is the practice of merging code changes into a shared branch frequently, with automated builds and tests running on every merge. The goal is to detect integration problems early, when they are cheap to fix, rather than during a release cycle when multiple changes have accumulated.
The practical benefits: bugs are caught close to when they are introduced, so context is fresh; test failures block merges before bad code reaches the main branch; and teams build confidence that the main branch is always in a deployable state.
GitLab CI
GitLab CI is GitLab's built-in CI/CD service. It reads a .gitlab-ci.yml file from the root of your repository and runs the jobs defined there on every push. You do not need a separate Jenkins server or external CI service.
Reasons to use it: the pipeline configuration lives next to your code and changes with it, GitLab's shared runners handle the compute for most projects, and pipeline status is visible directly in merge requests.
Setting Up GitLab CI for a Node.js Project
Prerequisites
- A GitLab account.
- A Node.js project in a GitLab repository.
- Basic YAML familiarity.
Step 1: Create a .gitlab-ci.yml File
In the root of your project, create .gitlab-ci.yml:
image: node:14
stages:
- test
test_job:
stage: test
script:
- npm install
- npm test
The image key specifies the Docker image the job runs in. The stages key defines the pipeline stages and their order. Each job specifies which stage it belongs to and what commands to run.
Step 2: Configure GitLab Runner (Optional)
GitLab's shared runners handle most projects without any additional setup. If you need jobs to run on your own infrastructure, you can install and register a self-hosted GitLab Runner. For most Node.js projects, the shared runners are sufficient.
Step 3: Commit and Push
git add .gitlab-ci.yml
git commit -m "Add GitLab CI configuration"
git push origin main
GitLab detects the file and starts the pipeline immediately.
Step 4: Monitor the Pipeline
Navigate to CI/CD > Pipelines in your GitLab project. You can view job logs, download artifacts, and see pipeline status. Failed jobs show the exact command that failed and its output.
Advanced Configuration
Caching Dependencies
Caching node_modules avoids reinstalling packages on every pipeline run:
cache:
paths:
- node_modules/
test_job:
stage: test
script:
- npm install
- npm test
Multiple Environments
Deploy to staging automatically on every merge to main:
stages:
- test
- deploy
deploy_staging:
stage: deploy
script:
- npm run build
- scp -r ./build user@staging-server:/var/www/app
only:
- main
Parallel Testing
Split a slow test suite across multiple runners:
test_job:
stage: test
script:
- npm install
- npm test
parallel: 4
Environment Variables
Store secrets in GitLab project settings under Settings > CI/CD > Variables, then reference them in your pipeline:
script:
- npm run deploy -- --api-key=$API_KEY
Never put secrets directly in .gitlab-ci.yml. They would be visible in repository history and to anyone with read access.
Best Practices for GitLab CI with Node.js
Pin the Node.js version to avoid environment drift between pipeline runs and local development:
image: node:14.17.0
Add a lint step so formatting and static analysis issues fail the pipeline before tests run:
script:
- npm run lint
- npm test
Restrict expensive jobs to relevant branches. Running the full test suite on every feature branch commit is usually not necessary:
only:
- merge_requests
- main
Declare build artifacts so they can be downloaded from the pipeline UI or passed between stages:
artifacts:
paths:
- build/
Troubleshooting Common Issues
If the pipeline fails on dependency installation, check that all packages are declared in package.json and that there are no version conflicts. The job log shows the exact error.
For permission errors running scripts, add a chmod step:
script:
- chmod +x ./deploy.sh
- ./deploy.sh
For missing environment variables, verify they are set in GitLab's CI/CD variable settings and that the variable name in the YAML matches exactly.
Integrating Docker
For containerized deployments, use Docker-in-Docker to build and push images in CI:
image: docker:stable
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
build_docker_image:
stage: build
script:
- docker build -t my-node-app .
- docker push my-node-app
only:
- main
The docker:dind service runs a Docker daemon inside the pipeline container, which the build script connects to.
Conclusion
A GitLab CI pipeline for a Node.js project takes under an hour to set up and immediately improves the feedback loop for every merge. Lint failures and test failures surface in minutes rather than during code review or after deployment. Once the basics are working, add caching to speed up runs, restrict jobs to the branches where they add value, and extend the pipeline with deployment stages as the project matures.
Additional Resources
Glossary
- Continuous Integration (CI): A practice where developers integrate code changes frequently with automated builds and tests.
- GitLab Runner: The application that executes CI jobs defined in
.gitlab-ci.yml. - Pipeline: A collection of stages and jobs executed in a defined order on each push.