Once upon a time, deploying apps used to be slow, manual and repetitive process. However, modern software is built and shipped rapidly through automation. This is called CI/CD (Continuous Integration / Continuous Deployment).
Previously, I covered about how to set-up a CI/CD Pipeline on Github. But if you’re using GitLab, you have a powerful CI/CD system built right into your repository. In-fact, one of the distinguishing features for GitLab is its native support for CI/CD workflows.
In this guide, I’ll walk you through setting up your very first GitLab CI/CD pipeline. We’ll break down what it actually is, why you need it, and then create a working example with a simple Node/React-style app. Along the way, I’ll share some practical developer insights that I’ve learned the hard way.
What is GitLab CI/CD (and Why Should You Care)?
At its core, GitLab CI/CD is GitLab’s built-in automation system. You define instructions in a special YAML file (.gitlab-ci.yml
), and GitLab takes care of running them every time you push code.
Think of it like giving GitLab a recipe:
- Step 1 → Build the app
- Step 2 → Run the tests
- Step 3 → Deploy somewhere
Instead of doing these things manually, GitLab CI/CD does them automatically, on every commit, merge request, or release tag.
Why is this useful?
- You catch bugs early with automated tests.
- You ensure every commit is deployable.
- You remove the “it works on my machine” problem.
- You free yourself from repetitive manual tasks.
If you’re building anything beyond a hobby project, CI/CD is the secret sauce to moving fast without breaking things.
Get Started with GitLab CI/CD
CI/CD is a continuous method of software development, where you continuously build, test, deploy, and monitor iterative code changes. This iterative process helps reduce the chance that you develop new code based on buggy or failed previous versions. GitLab CI/CD can catch bugs early in the development cycle and help ensure that the code deployed to production complies with your established standards.
This process is part of a larger workflow:
GitLab DevSecOps lifecycle with stages for Plan, Create, Verify, Secure, Release, and Monitor.
Step 1: Create a .gitlab-ci.yml
file
To use GitLab CI/CD, you start with a .gitlab-ci.yml
file at the root of your project. This file specifies the stages, jobs, and scripts to be executed during your CI/CD pipeline. In this file, you define variables, dependencies between jobs, and specify when and how each job should be executed.
Step 2: Find or create runners
Runners are the agents that run your jobs. These can run on physical machines or virtual instances. In your .gitlab-ci.yml
file, you can specify a container image. The runner loads the image, clones your project, and runs the job either locally or in the container.
- On GitLab.com, runners on Linux, Windows, and macOS are already available.
- On self-managed GitLab, you’ll need to register your own runner.
Step 3: Define your pipelines
A pipeline is what you’re defining in .gitlab-ci.yml
. It’s made up of jobs and stages:
- Stages define the order of execution (build, test, deploy).
- Jobs specify the tasks in each stage (compile, test, etc.).
Pipelines can be triggered by commits, merges, or schedules.
Step 4: Use CI/CD variables
GitLab CI/CD variables are key-value pairs used to store configuration settings or sensitive info (like API keys). Variables can be defined in the file, in project settings, or generated dynamically.
Types:
- Custom variables → user-defined.
- Predefined variables → set automatically by GitLab.
Variables can be protected or masked for security.
Step 5: Use CI/CD components
A CI/CD component is a reusable pipeline configuration unit. They help reduce duplication, improve maintainability, and promote consistency across projects. GitLab even provides templates for common tasks.
Create and Run Your First GitLab CI/CD Pipeline
Prerequisites
Before starting, make sure you have:
- A project in GitLab. (If you don’t, create a free public project at gitlab.com)
- Maintainer or Owner role for that project.
Steps
1. Ensure you have runners available
- On GitLab.com, runners are provided by default.
- To check: go to Settings > CI/CD > Runners. At least one runner should show as active.
- If none are available, install GitLab Runner locally and register it with your project.
2. Create a .gitlab-ci.yml
file
At the root of your repository, add a .gitlab-ci.yml
file to define your pipeline.
For example:
stages: # Pipeline stages
- build
- test
- deploy
build-job: # Job 1: Build
stage: build
image: node:18
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
test-job: # Job 2: Test
stage: test
image: node:18
script:
- npm install
- npm test
lint-job: # Job 3: Lint (runs in parallel with test-job)
stage: test
image: node:18
script:
- npm run lint
deploy-job: # Job 4: Deploy
stage: deploy
image: node:18
script:
- echo "Deploying to production server..."
only:
- main
This defines four jobs across three stages: build → test → deploy.
-
build-job
installs dependencies and builds your app. -
test-job
runs your tests. -
lint-job
checks your code quality (runs alongside tests). -
deploy-job
deploys to production, but only when changes are pushed to themain
branch.
3. Commit and run
Commit the .gitlab-ci.yml
file to your repository. GitLab automatically starts the pipeline.
4. View your pipeline and jobs
- Go to Build > Pipelines to see the pipeline progress.
- Click on a pipeline ID to view a graph of jobs and dependencies.
- Select a job name (e.g.,
deploy-prod
) to inspect logs and job details.
Done! - you’ve just created and run your first GitLab CI/CD pipeline! From here, you can expand the .gitlab-ci.yml
with more advanced jobs, variables, and integrations.
The Building Blocks of GitLab CI/CD
Before jumping into the setup, let’s clarify the three key concepts you’ll see in every .gitlab-ci.yml
file:
1. Pipelines
A pipeline is the full CI/CD workflow. It’s triggered by a commit or merge, and it runs through a series of stages/jobs.
2. Stages
A stage is a phase in your pipeline (build, test, deploy). Stages run sequentially: the test
stage won’t run until build
has succeeded.
3. Jobs
A job is a single task inside a stage. It’s just a set of shell commands. For example, a test job might run npm test
.
So the hierarchy looks like this:
Setting Up Your First GitLab CI/CD Pipeline
We’ll build a simple pipeline for a Node/React app. Assume your project has this structure:
Step 1: Create a .gitlab-ci.yml
File
At the root of your repo, create a file named .gitlab-ci.yml
. This file tells GitLab what to do when you push code.
Step 2: Define Your Stages
Step 3: Add Your Jobs
Step 4: Push and Watch the Magic
Head to Build → Pipelines in GitLab to see your pipeline running.
Developer Insights: Common Mistakes and Pro Tips
Even experienced developers slip up when setting up GitLab CI/CD. Here are a few common mistakes I’ve seen (and made myself), plus how to avoid them:
1. Forgetting to cache dependencies
If every job re-installs dependencies from scratch, pipelines get painfully slow. For example, in a Node.js project, npm install
can take minutes each time.
Fix: Use GitLab’s cache
keyword to speed things up:
cache:
paths:
- node_modules/
2. Not pinning Docker images
Using image: node:latest
seems convenient, but it can break your build if a new Node release introduces changes.
Fix: Pin to a specific version like node:18
to ensure stability.
3. Misusing stages and jobs
Beginners sometimes put all commands in a single job. That works… until you need parallelization or conditional deployments.
Fix: Break pipelines into clear stages (build
, test
, deploy
) with multiple jobs. This makes debugging and scaling way easier.
4. Ignoring YAML anchors
If you repeat the same image
, cache
, or before_script
blocks in multiple jobs, your .gitlab-ci.yml
gets messy.
Fix: Use YAML anchors to DRY up your file:
.default-job: &default-job
image: node:18
cache:
paths:
- node_modules/
build-job:
<<: *default-job
script:
- npm run build
5. Not setting branch rules
Without only
or except
, you might accidentally deploy from feature branches.
Fix: Protect your deploy job:
only:
- main
That way, the section not only lists mistakes but also explains why they hurt and how to fix them with code snippets.
Pro Tips (Quick Wins)
-
Cache smartly → Save time by caching
node_modules/
orvendor/
. -
Pin versions → Use
node:18
instead ofnode:latest
for stability. -
Use YAML anchors → Keep your
.gitlab-ci.yml
clean and DRY. -
Protect deploys → Run deployment jobs only on
main
(or release branches). - Keep pipelines fast → Fail fast with lint/tests before hitting deploy.
A Real-World Example: Node/React App
Let’s make this practical. Imagine you’ve got a simple React frontend with a Node/Express backend in the same repo. You want your pipeline to:
- Install dependencies
- Run tests
- Build the React app
- Deploy the backend + frontend
Here’s how your .gitlab-ci.yml
might look:
stages:
- install
- test
- build
- deploy
default:
image: node:18
cache:
paths:
- node_modules/
install:
stage: install
script:
- npm install
artifacts:
paths:
- node_modules/
test:
stage: test
script:
- npm test
build_frontend:
stage: build
script:
- cd frontend
- npm install
- npm run build
artifacts:
paths:
- frontend/build
build_backend:
stage: build
script:
- cd backend
- npm install
- npm run build
artifacts:
paths:
- backend/dist
deploy:
stage: deploy
script:
- echo "Deploying app..."
- rsync -avz frontend/build/ user@server:/var/www/frontend
- rsync -avz backend/dist/ user@server:/var/www/backend
only:
- main
What’s Happening Here
- Stages: Install → Test → Build → Deploy (a standard flow for Node + React apps).
-
Cache: Keeps
node_modules/
between jobs so installs are faster. -
Artifacts: React build output (
frontend/build
) and backend dist files (backend/dist
) are passed to the deploy job. -
Deploy: Uses
rsync
as a simple way to push files to a server — in real life, you’d use SSH, Docker, or Kubernetes. - only: main: Ensures deployment only runs from your main branch, avoiding those “oops, I deployed a feature branch” moments.
Wrapping Up
GitLab CI/CD may seem intimidating at first, but once you understand pipelines, stages, and jobs, it clicks. With just a .gitlab-ci.yml
file, you can:
- Build your app automatically.
- Run tests on every commit.
- Deploy safely and consistently.
The real power comes when you start refining your pipelines: caching, artifacts, YAML anchors, and custom runners.
If you’re working with Node or React, the examples above should give you a solid starting point. From here, you can expand into more advanced flows (linting, end-to-end tests, staging/production deployments, Docker builds, etc.).
One More Thing
While CI/CD pipelines are perfect for automating your builds and tests, our team at DeepDocs is working hard to bring CI/CD automation to keep your documentation updated.
You see, updating docs is still a manual process, and it is often forgotten as it is seen as a boring or low value task. DeepDocs takes care of this keeping your docs automatically updated with your commits, and with no manual effort. it completely free to try on GitHub.
Top comments (4)
Really clear breakdown, thanks for the simple YAML example! Helped me finally get my first pipeline running.
Glad it was helpful
This guide was incredibly helpful as I'm transitioning into DevOps roles. The step-by-step walkthrough of setting up a GitLab CI/CD pipeline using .gitlab-ci.yml was clear and practical. I especially appreciated the insights on configuring runners and automating builds, tests, and deployments. These are essential skills for modern software development, and this post has given me the confidence to implement CI/CD in my own projects. Thank you for sharing such a comprehensive and accessible resource!
I'm glad you found it helpful. All the best in your new adventure