GitLab CI/CD is built directly into the GitLab platform, making it straightforward to automate builds, tests, and deployments without installing a separate CI server. Pipelines are defined in a .gitlab-ci.yml file committed to your repository, and they execute on GitLab Runners — lightweight agents that can run on bare metal, virtual machines, or inside Docker containers. This tutorial explains the structure of a .gitlab-ci.yml file, how to register a Runner on RHEL 8, and how to build a pipeline that progresses from build through test to deployment over SSH.

Prerequisites

  • GitLab CE installed and accessible on RHEL 8 (see the GitLab CE installation tutorial)
  • A GitLab project with at least one commit in the default branch
  • A second RHEL 8 server or the same GitLab host to act as the Runner
  • Docker installed on the Runner host if using the Docker executor

Step 1 — Understand the .gitlab-ci.yml Structure

The .gitlab-ci.yml file lives at the root of your repository. It defines global settings, the list of pipeline stages, and individual jobs.

stages:
  - build
  - test
  - deploy

variables:
  APP_NAME: my-app
  DEPLOY_HOST: deploy.example.com

default:
  image: maven:3.9-eclipse-temurin-11

build-job:
  stage: build
  script:
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 hour

test-job:
  stage: test
  script:
    - mvn test
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths:
      - .m2/repository

deploy-job:
  stage: deploy
  script:
    - echo "Deploying $APP_NAME to $DEPLOY_HOST"
    - bash deploy.sh
  only:
    - main

Jobs in the same stage run in parallel by default. Jobs in later stages run only after all jobs in the previous stage succeed. The artifacts keyword passes build outputs between stages.

Step 2 — Install and Register a GitLab Runner

GitLab Runner is a separate binary that connects to your GitLab instance and picks up pipeline jobs. Install it on the server that will execute your CI jobs.

# Add the GitLab Runner repository
curl -sS https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash

# Install the runner package
sudo dnf install -y gitlab-runner

# Enable and start the service
sudo systemctl enable --now gitlab-runner

# Confirm it is running
sudo systemctl status gitlab-runner

Now register the Runner against your GitLab instance. You will need the registration token from Settings → CI/CD → Runners in your GitLab project.

sudo gitlab-runner register 
  --non-interactive 
  --url "http://gitlab.example.com" 
  --registration-token "YOUR_REGISTRATION_TOKEN" 
  --executor "shell" 
  --description "rhel8-shell-runner" 
  --tag-list "rhel8,shell" 
  --run-untagged true

Use --executor docker --docker-image maven:3.9-eclipse-temurin-11 instead for the Docker executor, which runs each job in a fresh container for better isolation.

Step 3 — Configure CI/CD Variables in the GitLab UI

Sensitive values like SSH private keys, API tokens, or database passwords should be stored as masked CI/CD variables rather than in the YAML file.

# In GitLab UI: Project → Settings → CI/CD → Variables → Add variable
#
# Example variables to add:
#   Key: SSH_PRIVATE_KEY     Type: File     Masked: yes   Protected: yes
#   Key: DEPLOY_USER         Type: Variable Masked: no    Value: deployer
#   Key: DEPLOY_HOST         Type: Variable Masked: no    Value: 192.168.1.50
#
# Reference them in .gitlab-ci.yml:
#   script:
#     - eval $(ssh-agent -s)
#     - ssh-add "$SSH_PRIVATE_KEY"
#     - ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/app && git pull && systemctl restart app"

Variables marked Protected are only injected into pipelines running on protected branches, adding an extra safeguard for production deployments.

Step 4 — Deploy to a Remote Server via SSH

A common deployment pattern is to SSH from the CI Runner into a target server and run deployment commands. Configure this in the deploy stage of your .gitlab-ci.yml.

deploy-job:
  stage: deploy
  before_script:
    - apt-get update -y && apt-get install -y openssh-client || true
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d 'r' | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
  script:
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "
        set -e
        cd /opt/$APP_NAME
        git pull origin main
        mvn clean package -DskipTests
        sudo systemctl restart $APP_NAME
      "
  only:
    - main
  environment:
    name: production
    url: http://$DEPLOY_HOST

The environment keyword creates a tracked deployment environment in GitLab, enabling rollback and deployment history visible under Deployments → Environments.

Step 5 — Trigger and Monitor Pipeline Runs

Pipelines trigger automatically on every push to the repository. You can also trigger them manually or via the GitLab API.

# Trigger pipeline via GitLab API
curl -X POST 
     --fail 
     -F token=YOUR_PIPELINE_TRIGGER_TOKEN 
     -F ref=main 
     http://gitlab.example.com/api/v4/projects/PROJECT_ID/trigger/pipeline

# Check Runner logs on the host if a job stays pending
sudo journalctl -u gitlab-runner -f

# View registered runners and their status
sudo gitlab-runner list

In the GitLab UI, navigate to CI/CD → Pipelines to see the status of each stage. Click any job to view its real-time console log, making it easy to diagnose failures.

Conclusion

You have defined a multi-stage GitLab CI/CD pipeline in .gitlab-ci.yml, registered a GitLab Runner on RHEL 8, secured credentials with masked CI/CD variables, and implemented SSH-based deployment to a target server. GitLab’s pipeline architecture scales from simple single-stage builds to complex multi-project pipelines with DAG dependencies and dynamic child pipelines. As your team grows, consider creating shared Runner pools, adding Docker-in-Docker builds for containerized applications, and integrating GitLab’s built-in container registry to store and deploy images.

Next steps: How to Install Jenkins on RHEL 8, How to Configure Jenkins Pipelines and Jenkinsfile on RHEL 8, and How to Set Up a Git Server with Gitea on RHEL 8.