GitLab CI Environments
Learn GitLab CI environments, deployment tracking, review apps, environment URLs, protected environments, deployment approvals, scoped variables, DORA metrics, and a complete multi-environment deployment example.
In GitLab CI/CD, an environment is not just a label you attach to a deployment job. It is a first-class deployment tracking concept that helps GitLab understand where your application was deployed, when it was deployed, which job performed the deployment, and which commit reached that target. If you treat environments seriously, GitLab becomes more than a build runner. It becomes a deployment history system, a release visibility system, and a foundation for safer delivery.
That is why topics like GitLab CI environments, GitLab environment tracking, GitLab review apps, GitLab protected environments, and GitLab deployment approval matter so much in modern DevOps workflows.
What Are GitLab Environments?
A GitLab environment represents a deployment target such as:
developmentstagingproductionreview/my-feature-branch
When a CI job declares an environment, GitLab records the deployment and links it to the commit, pipeline, job, and, when available, a URL. This gives your team a clear answer to operational questions such as:
- What version is currently in staging?
- When was production last deployed?
- Which merge request created this review app?
- What deployment failed?
- How frequently are we releasing?
This is the reason GitLab environments are about deployment tracking, not only syntax.
The environment Keyword in Jobs
You define environments using the environment keyword inside a job.
deploy-staging:
stage: deploy
script:
- ./deploy-staging.sh
environment:
name: staging
url: https://staging.example.com
This does two things:
- tells GitLab that the job deploys to an environment called
staging - attaches the environment URL so users can click directly to the deployed app
name
The name field identifies the environment. Static names such as dev, staging, and production are common for long-lived environments.
url
The url field improves usability. Instead of making teammates search for the deployed site, GitLab can display a direct environment link. This is especially helpful for QA, product managers, and reviewers.
Static vs Dynamic Environments
A big part of understanding GitLab CI deployment environments is recognizing that not all environments are permanent.
Static environments
A static environment is long-lived and reused over time. Examples include:
developmentqastagingproduction
These environments usually represent stable infrastructure targets and receive repeated deployments.
Dynamic environments
A dynamic environment is created temporarily, often for a branch or merge request, and then removed when no longer needed.
Examples include:
review/feature-loginreview/mr-42preview/my-change
Dynamic environments are the foundation of GitLab review apps, which create per-merge-request previews that reviewers can click and test.
Viewing Deployment History in GitLab
One of the best benefits of environment tracking is visibility in the GitLab UI. Once jobs declare environments, GitLab can show:
- current environments and their status
- the latest deployment for each environment
- a history of previous deployments
- links to the relevant pipeline and job
- environment URLs
This makes release troubleshooting much easier. If someone asks, “What commit is in staging right now?” you do not need to dig through Slack or guess from timestamps. GitLab already knows.
For regulated or fast-moving teams, this audit trail is extremely valuable because deployment information stays close to the source code and pipeline that produced it.
Dynamic Environments for Merge Requests
Dynamic environments become especially powerful in merge request workflows. A common pattern is to create a unique environment name using the merge request IID.
review-app:
stage: deploy
script:
- ./deploy-review.sh
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://review-$CI_MERGE_REQUEST_IID.example.com
This means each merge request gets its own preview environment, such as:
review/17review/18review/19
That preview can be linked directly in the merge request, making it easy for reviewers to test behavior without checking out code locally.
Review Apps in GitLab CI/CD
GitLab review apps are one of the most developer-friendly features in GitLab CI. Instead of reviewing screenshots or abstract descriptions, product and QA teams can interact with a live deployment created just for the merge request.
A review app usually includes:
- a deployment job that creates the temporary environment
- a dynamic
environment:name - an
environment:url - a stop job that removes the environment later
Typical use cases:
- frontend UI validation
- stakeholder demos
- testing feature flags and content changes
- manual verification before merging
The experience becomes even better when the environment URL is automatically posted into the merge request widget.
The on_stop Keyword and Stop Job Pattern
Dynamic environments should not live forever. Cloud resources cost money, and stale review environments create confusion. GitLab solves this with the on_stop keyword and a paired stop job.
Deployment job
review-app:
stage: deploy
script:
- ./deploy-review.sh
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://review-$CI_MERGE_REQUEST_IID.example.com
on_stop: stop-review-app
Stop job
stop-review-app:
stage: deploy
script:
- ./destroy-review.sh
when: manual
environment:
name: review/$CI_MERGE_REQUEST_IID
action: stop
This pattern tells GitLab which job stops the environment. In many teams, the stop job is triggered manually, automatically on merge, or via cleanup policies depending on the platform.
The important point is conceptual: creating a dynamic environment should always come with a cleanup strategy.
Protected Environments and Deployment Approvals
Production is different from development. Most organizations do not want any successful pipeline to push straight to production. That is where GitLab protected environments come in.
A protected environment lets you restrict who can deploy to a specific target such as production. Depending on your GitLab tier and settings, you can require:
- only certain roles or users may deploy
- deployment approvals before the job proceeds
- controlled manual actions for sensitive releases
This creates a stronger governance layer around GitLab deployment approval workflows.
Why protected environments matter
Protected environments help prevent:
- accidental deployments from the wrong branch
- unauthorized manual triggers
- overexposure of production deployment capability
- release bypasses outside of approved processes
A good pattern is to combine:
- protected branches
- protected CI variables
- protected runners
- protected environments
Each layer reduces the chance of an unsafe production release.
Deployment Frequency and DORA Metrics
GitLab environments also support measurement. Because GitLab can track deployments to environments, it can contribute to metrics such as deployment frequency, one of the DORA metrics widely used to evaluate software delivery performance.
DORA metrics often include:
- deployment frequency
- lead time for changes
- change failure rate
- time to restore service
When your pipeline accurately records deployments to real environments, GitLab can provide better operational insight. This is another reason to model environments explicitly instead of treating deployments as anonymous scripts.
If your organization wants to improve release speed without sacrificing stability, proper environment tracking is a foundational step.
Environment-Scoped Variables
Another powerful feature is environment scoped variables. Sometimes the same application uses different values depending on where it is deployed:
- different database URLs
- different API endpoints
- different feature flags
- different credentials
Instead of hardcoding separate jobs for every small variation, you can define variables in GitLab settings and scope them to specific environments.
Examples:
DATABASE_URLscoped tostagingDATABASE_URLscoped toproductionAPI_BASE_URLscoped toreview/*
Then the same deployment logic can run across multiple targets while GitLab injects the correct value for the current environment.
This reduces duplication and helps keep sensitive configuration out of the repository.
Complete Example: Dev to Staging to Production
Here is a complete example showing environment tracking across a simple delivery flow.
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
- when: never
stages:
- test
- deploy
unit-tests:
stage: test
image: node:20
script:
- npm ci
- npm test
review-app:
stage: deploy
script:
- ./scripts/deploy-review.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://review-$CI_MERGE_REQUEST_IID.example.com
on_stop: stop-review-app
stop-review-app:
stage: deploy
script:
- ./scripts/destroy-review.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
allow_failure: true
environment:
name: review/$CI_MERGE_REQUEST_IID
action: stop
deploy-dev:
stage: deploy
script:
- ./scripts/deploy-dev.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
environment:
name: development
url: https://dev.example.com
deploy-staging:
stage: deploy
script:
- ./scripts/deploy-staging.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
environment:
name: staging
url: https://staging.example.com
deploy-production:
stage: deploy
script:
- ./scripts/deploy-production.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
environment:
name: production
url: https://app.example.com
What this pipeline does
- merge request pipelines create a review app with a unique environment name and URL
- a stop job can tear the review app down
- main branch deployments automatically push to development
- staging is a manual promotion step
- production is a separate manual promotion step with its own tracked environment
This single file now gives you:
- clear environment history
- clickable environment URLs
- review app previews
- cleaner release promotion visibility
- a better basis for approvals and operational metrics
Practical Tips for Designing Environments
- Use meaningful names. Prefer
stagingover vague labels likeenv2. - Always set URLs when possible. They make environments more useful for non-engineers.
- Pair dynamic environments with cleanup. Use
on_stopand a stop job. - Protect production. Do not make production just another ordinary job.
- Use scoped variables. They simplify safe multi-environment configuration.
- Keep promotion paths obvious. A visible dev → staging → production flow reduces mistakes.
Common Mistakes to Avoid
- treating environments as mere job names instead of tracked deployment targets
- forgetting the stop job for review apps
- reusing one environment name for many different temporary previews
- storing production values directly in YAML instead of scoped variables
- skipping protected environment controls for high-risk deployments
Final Takeaway
GitLab environments are one of the features that turn a basic CI pipeline into a real delivery platform. By using the environment keyword, setting URLs, modeling static and dynamic targets, adding review apps, wiring stop jobs with on_stop, protecting production, and scoping variables per environment, you gain far more than deployment automation. You gain visibility, safer releases, better collaboration, and stronger operational insight.
Knowledge Check
Question 1: Environment Basics
What is the main value of a GitLab environment beyond simply labeling a job?
Question 2: Review App Cleanup
Why is the on_stop pattern important for dynamic review app environments?
Question 3: Protected Environments
What is the main purpose of a protected environment in GitLab CI/CD?