Back to Blog
Tutorial11 min read2026-07-06

How to Deploy with GitLab CI/CD

GitLab CI/CD is a powerful, YAML-driven pipeline engine. This tutorial walks through building a real deploy pipeline, stages, jobs, caching, environments, secrets, and connecting it to a deployment target.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

How GitLab CI/CD is structured

GitLab CI/CD is configured by a single file at the root of your repo: .gitlab-ci.yml. When you push, GitLab reads it and runs a pipeline made of stages, each containing jobs. Jobs in the same stage run in parallel; stages run in sequence. The mental model:

Pipeline
├── stage: test     (jobs run in parallel)
├── stage: build    (runs after test passes)
└── stage: deploy   (runs after build)

If any job in a stage fails, later stages don't run, your gate against shipping broken code.

A first pipeline

Here's a minimal but realistic pipeline for a Node app:

stages:
  - test
  - build
  - deploy

variables:
  NODE_ENV: "test"

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

Key pieces:

  • image sets the Docker image the job runs in.
  • cache persists node_modules between runs keyed on the lockfile, so unchanged dependencies aren't reinstalled every time.
  • artifacts pass build output (dist/) to later stages and let you download it.

Caching vs. artifacts

These two get confused constantly:

CacheArtifacts
PurposeSpeed up jobs (deps)Pass results between stages
LifetimeBest-effort, may be evictedStored, downloadable, expiring
Examplenode_modules/dist/, test reports

Rule of thumb: cache things you can regenerate (dependencies), artifact things you need downstream (build output, reports).

Secrets and variables

Never hardcode credentials in .gitlab-ci.yml. Use GitLab's CI/CD variables (Settings → CI/CD → Variables), which are injected as environment variables into jobs. Mark sensitive ones as masked (hidden in logs) and protected (only available on protected branches).

deploy:
  stage: deploy
  image: alpine:3
  script:
    - echo "Deploying with token..."
    - ./deploy.sh   # reads $DEPLOY_TOKEN from the environment
  # $DEPLOY_TOKEN is defined as a masked, protected CI/CD variable

Environments and deployment gating

GitLab's environment keyword tracks where you deploy and unlocks features like deployment history and manual approval gates. A common pattern: auto-deploy to staging, but require a manual click for production.

deploy_staging:
  stage: deploy
  script: ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy_production:
  stage: deploy
  script: ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual            # requires a human to click "play"

The when: manual on production means GitLab won't ship to prod until someone explicitly approves, a simple, effective safety gate.

The rules keyword: controlling when jobs run

Modern GitLab pipelines use rules to decide whether a job runs. Common patterns:

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'          # only on main
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'  # on MRs
  - changes:
      - src/**/*                                # only if src changed

rules replaced the older only/except syntax and is far more flexible. Use it to avoid running expensive deploy jobs on every feature branch.

Deploying to PandaStack from GitLab

PandaStack's native model is git-push deploys, connect a repo and pushes build and deploy automatically. If your team's source of truth is GitLab, you have two clean options:

Option 1, let PandaStack watch the repo. Connect your GitLab repo to PandaStack so pushes trigger builds and deploys directly. Your .gitlab-ci.yml then focuses on what GitLab does best, running tests and linting, while PandaStack handles the build-and-deploy. This keeps responsibilities clean: GitLab validates, PandaStack ships.

Option 2, trigger PandaStack from a GitLab job. If you want GitLab to own the deploy step (e.g., to gate prod behind when: manual), have a deploy job call PandaStack, for example by POSTing to a deploy hook after tests pass:

deploy_production:
  stage: deploy
  image: alpine:3
  script:
    - apk add --no-cache curl
    - curl -X POST "$PANDASTACK_DEPLOY_HOOK"   # stored as a masked CI/CD variable
  environment:
    name: production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

Either way, PandaStack does the heavy lifting on the deploy side: builds run in rootless BuildKit in ephemeral Kubernetes Job pods, images go to Google Artifact Registry, and Helm deploys to GKE, with live build logs, deploy history, and one-click rollbacks. And if your app needs a database, PandaStack auto-wires it by injecting DATABASE_URL, so your GitLab pipeline never has to manage connection strings.

Tips for fast, reliable pipelines

  • Cache dependencies keyed on the lockfile, this is the biggest speedup.
  • Use needs: to run jobs out of strict stage order where dependencies allow, shortening total pipeline time.
  • Fail fast: put quick checks (lint, type-check) early so you don't wait on a long build to learn about a typo.
  • Keep secrets masked and protected, and never echo them.
  • Pin image tags (node:20, not node:latest) for reproducible builds.

Conclusion

GitLab CI/CD gives you a flexible, declarative pipeline: stages and jobs, caching for speed, artifacts for handoff, masked variables for secrets, and environment + when: manual for safe production gating. Pair it with a deployment target that handles the build-and-ship mechanics and you get a pipeline where GitLab validates and your platform deploys.

PandaStack connects to GitLab repos for automatic git-push deploys, or accepts a deploy trigger from a gated GitLab job, with managed builds, live logs, rollbacks, and auto-wired databases. Try it on the free tier at https://dashboard.pandastack.io.

References

  • GitLab CI/CD .gitlab-ci.yml reference: https://docs.gitlab.com/ee/ci/yaml/
  • GitLab CI/CD variables: https://docs.gitlab.com/ee/ci/variables/
  • GitLab environments and deployments: https://docs.gitlab.com/ee/ci/environments/
  • GitLab rules keyword: https://docs.gitlab.com/ee/ci/yaml/#rules

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also