How to Configure GitLab CI/CD Pipelines on RHEL 7
GitLab CI/CD turns every push to your repository into a fully automated pipeline that can build, test, and deploy your application without manual intervention. The pipeline definition lives in a file called .gitlab-ci.yml at the root of your repository, and the actual work is performed by GitLab Runner — a lightweight agent you install on the same RHEL 7 server as GitLab or on a dedicated build host. This guide covers the full .gitlab-ci.yml syntax, runner registration and executor types, variables, artifacts, caching, environment-based deployments, and the concept of review apps.
Prerequisites
- GitLab CE installed and running on RHEL 7 (see “How to Install GitLab CE on RHEL 7”)
- A GitLab project containing application source code
- Root or
sudoaccess on the runner host (can be the same machine as GitLab) - Docker installed on the runner host if you plan to use the Docker executor
- Your application’s build tools available on the runner host (JDK, Node.js, Python, etc.)
Step 1: Install GitLab Runner on RHEL 7
GitLab Runner is distributed as a standalone binary with its own yum repository. Add the repository and install the runner package:
curl -sS https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
sudo yum install -y gitlab-runner
Enable and start the runner service:
sudo systemctl enable gitlab-runner
sudo systemctl start gitlab-runner
Verify the installed version:
gitlab-runner --version
Step 2: Register the Runner with Your GitLab Instance
A runner must be registered with GitLab before it can pick up jobs. Retrieve the registration token from your GitLab project by navigating to Settings > CI/CD > Runners > Expand and copying the token under “Set up a project runner”.
Run the interactive registration command:
sudo gitlab-runner register
The command prompts for several values:
Enter the GitLab instance URL (for example, https://gitlab.com/):
https://gitlab.example.com
Enter the registration token:
GR1348941aBcDeFgHiJkLmNoPqRsT
Enter a description for the runner:
rhel7-shell-runner
Enter tags for the runner (comma-separated):
rhel7,shell,build
Enter optional maintenance note for the runner:
Select executor: shell, docker, docker-autoscaler, instance, kubernetes, custom, docker-windows, parallels, virtualbox, docker+machine, ssh:
shell
After registration, the runner appears as available in Settings > CI/CD > Runners with a green circle. Restart the runner service to ensure the new configuration takes effect:
sudo systemctl restart gitlab-runner
Step 3: Understand Executor Types
The shell executor runs jobs directly in a shell on the runner host using the system’s installed tools. It is the simplest option but means all jobs share the host environment. The Docker executor runs each job inside a fresh Docker container, providing full isolation and reproducibility. Configure the Docker executor during registration or by editing /etc/gitlab-runner/config.toml:
[runners.docker]
image = "alpine:latest"
privileged = false
volumes = ["/cache"]
shm_size = 0
For the Docker executor, install Docker on the runner host:
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo
https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker gitlab-runner
Step 4: Write Your First .gitlab-ci.yml
Create a .gitlab-ci.yml file at the root of your repository. The following example is a complete pipeline for a Node.js application:
stages:
- build
- test
- deploy
variables:
NODE_ENV: "production"
APP_PORT: "3000"
build_job:
stage: build
image: node:18-alpine
script:
- npm ci --prefer-offline
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
test_job:
stage: test
image: node:18-alpine
script:
- npm ci --prefer-offline
- npm run test -- --coverage
coverage: '/Liness*:s*(d+.?d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
deploy_production:
stage: deploy
script:
- echo "Deploying to production server..."
- rsync -avz --delete dist/ [email protected]:/var/www/myapp/
environment:
name: production
url: https://myapp.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
Step 5: Use Variables and Protect Secrets
GitLab CI/CD variables fall into two categories: those defined in .gitlab-ci.yml and those stored securely in the GitLab UI. For sensitive data such as deployment SSH keys, API tokens, and database passwords, always use the UI: navigate to Settings > CI/CD > Variables > Add variable, enable Masked to hide the value in job logs, and enable Protected to restrict it to protected branches.
Reference variables in the pipeline with the $VARIABLE_NAME syntax:
deploy_job:
stage: deploy
script:
- curl -X POST
-H "Authorization: Bearer $DEPLOY_API_TOKEN"
-d '{"ref":"main"}'
"https://api.example.com/trigger-deploy"
GitLab also exposes a rich set of predefined variables automatically:
CI_COMMIT_SHA # Full commit hash
CI_COMMIT_SHORT_SHA # 8-character short hash
CI_COMMIT_BRANCH # Branch name
CI_PIPELINE_ID # Unique pipeline ID
CI_PROJECT_NAME # Repository name
CI_REGISTRY # GitLab container registry URL
CI_REGISTRY_USER # Registry login username
CI_REGISTRY_PASSWORD # Registry login password (ephemeral token)
Step 6: Configure Artifacts
Artifacts are files generated by a job that GitLab preserves and makes available for download or for use by later pipeline stages:
build_app:
stage: build
script:
- mvn clean package -DskipTests
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- target/*.jar
- target/*.war
exclude:
- target/*-sources.jar
expire_in: 7 days
when: on_success
For test reports, use the reports key so GitLab can parse and display results in the merge request interface:
artifacts:
reports:
junit: target/surefire-reports/TEST-*.xml
Step 7: Configure Caching
Caching persists directories between pipeline runs to avoid re-downloading dependencies. Unlike artifacts, cache is not guaranteed to exist — pipelines must still work if the cache is empty:
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
key:
files:
- requirements.txt
paths:
- .cache/pip
- venv/
build:
stage: build
script:
- python -m venv venv
- source venv/bin/activate
- pip install -r requirements.txt
For Node.js projects, cache the node_modules directory keyed on package-lock.json:
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
Step 8: Define Pipeline Triggers and Rules
The rules keyword provides fine-grained control over when a job runs, replacing the older only/except syntax:
deploy_staging:
stage: deploy
script:
- ./deploy.sh staging
rules:
# Run on any push to a branch other than main
- if: $CI_COMMIT_BRANCH != "main" && $CI_PIPELINE_SOURCE == "push"
deploy_production:
stage: deploy
script:
- ./deploy.sh production
rules:
# Run only on main branch pushes
- if: $CI_COMMIT_BRANCH == "main"
# Also run if triggered manually via API
- if: $CI_PIPELINE_SOURCE == "api"
nightly_build:
stage: build
script:
- make all
rules:
# Run only when triggered by a schedule
- if: $CI_PIPELINE_SOURCE == "schedule"
Create scheduled pipelines in GitLab under CI/CD > Schedules using standard cron syntax.
Step 9: Deploy to Environments
The environment keyword links a deployment job to a named environment, enabling GitLab to track what version is deployed where and providing a rollback button in the UI:
deploy_staging:
stage: deploy
script:
- ssh [email protected] "cd /app && git pull && systemctl restart app"
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy_production:
stage: deploy
script:
- ssh [email protected] "cd /app && git pull && systemctl restart app"
environment:
name: production
url: https://myapp.example.com
when: manual
rules:
- if: $CI_COMMIT_BRANCH == "main"
Setting when: manual on the production deployment creates a one-click deploy button in the GitLab pipeline UI, preventing automatic deployments to production while still making them quick and traceable.
Step 10: Understand Review Apps
Review apps automatically deploy each merge request to a temporary, isolated environment, giving reviewers a live preview of the changes. GitLab manages the lifecycle — creating the environment when the MR opens and tearing it down when it is merged or closed.
review:
stage: deploy
script:
- export REVIEW_DOMAIN="mr-${CI_MERGE_REQUEST_IID}.review.example.com"
- ./deploy-review.sh "$REVIEW_DOMAIN"
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://mr-$CI_MERGE_REQUEST_IID.review.example.com
on_stop: stop_review
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
stop_review:
stage: deploy
script:
- ./teardown-review.sh "$CI_MERGE_REQUEST_IID"
environment:
name: review/$CI_MERGE_REQUEST_IID
action: stop
when: manual
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
The on_stop key links the cleanup job so GitLab calls it automatically when the merge request is closed.
Conclusion
GitLab CI/CD pipelines defined in .gitlab-ci.yml provide a powerful, version-controlled approach to automated software delivery. You now have a working runner registered to your RHEL 7 GitLab instance and a comprehensive understanding of stages, jobs, variables, artifacts, caching, rules-based triggers, and environment-tracked deployments. With review apps, every merge request can have its own live deployment, dramatically shortening the feedback loop between writing code and verifying it in a real environment. The next step is tuning runner capacity — consider adding additional runners on dedicated build hosts to parallelize heavy pipelines and reduce queue times.