| | |

GitLab 19 🦊 Deploying Applications to Environments (Staging and Production)

⏱️ Estimated read: 10 min | |

A pipeline that builds and tests without deploying is a pipeline that stops at the last mile. GitLab provides a structured deployment mechanism through environments: named targets (staging, production, review apps) with their own variables, deployment history, and protection rules. The exam tests the environment keyword, the distinction between a review app and a staging environment, the behavior of when: manual for production deployments, and the interaction between environments and protected branches. The critical point is that an environment is a logical label, not a physical server. The actual deployment command is still a script. The environment is what GitLab tracks—which pipeline deployed which commit to which target, and whether the deployment succeeded.

Key point: The environment keyword attaches a deployment record to a job. The name is the environment label (e.g., staging, production). The url is the link that appears in the merge request and the environment page. The on_stop keyword pairs a stop job that tears down the environment. The action keyword distinguishes a deployment (start, the default) from a teardown (stop). The environment protection rules determine who can deploy and when.


Why environments require explicit configuration

The audit problem. A script that runs kubectl apply or ansible-playbook deploys the application, but it leaves no record of what was deployed, when, and by whom. GitLab’s environment tracking creates a deployment record for every successful job that declares environment:. The record includes the commit SHA, the pipeline ID, the user, and the timestamp. The Environments page shows the current deployment for each environment and the deployment history behind it .

The manual gate problem. A pipeline that deploys to production on every commit is a pipeline that will eventually deploy a broken commit to production. The when: manual keyword pauses the job and requires a user to click the play button. The environment protection rules go further: only specific users or roles can click the play button. This is the mechanism for enforcing deployment approvals without a separate approval application .

The review app problem. A merge request that changes the frontend needs a running environment to verify the visual result. A review app is a per-branch environment that is created when the merge request is opened and destroyed when it is closed. The environment: name: review/$CI_COMMIT_REF_SLUG syntax creates a unique environment for each branch. The on_stop keyword pairs a stop job that tears down the environment when the merge request is closed .

The environment variable problem. The database URL, the API key, and the feature flags for staging are different from those for production. Environment-scoped variables are the mechanism for this. A variable defined with environment_scope: production is only injected into jobs that declare environment: production. The same variable name can have different values for staging and production .

The trade-off. Environment tracking is a GitLab feature, not a deployment tool. The deployment command is still the script. GitLab provides the record, the gate, and the variable scoping. The actual infrastructure change is the responsibility of the script.


a. The basic environment declaration

The environment keyword declares the environment name. The url is the link that appears in the merge request and the environment page. The job runs in the deploy stage.

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

The deploy-staging job runs on the main branch, deploys to the staging environment, and records the deployment. The Environments page shows the current commit on staging, the deployment timestamp, and the user who triggered the pipeline .

The url can be a static URL or a dynamic URL. A dynamic URL uses variables: url: https://$CI_COMMIT_REF_SLUG.example.com. This is the mechanism for review apps, where each branch has its own URL.


b. Manual gates and protection rules

The when: manual keyword pauses the job. The job appears in the pipeline but does not run until a user clicks the play button. The allow_failure: false setting makes the pause a blocking gate: the pipeline cannot proceed until the job is run or the pipeline is cancelled.

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  when: manual
  allow_failure: false
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

The deploy-production job is created when the pipeline runs on the main branch. It does not run automatically. A user with the correct permissions clicks the play button to deploy. The allow_failure: false makes the pipeline status “blocked” until the manual job is run .

The environment protection rules are configured in Settings → CI/CD → Protected environments. A protected environment requires the deploying user to have a specific role (Maintainer or Owner). The production environment is the typical candidate for protection. The rule can also require an approval from a specific user or group before the deployment proceeds.


c. Review apps and the stop action

A review app is a per-branch environment. The environment: name: review/$CI_COMMIT_REF_SLUG syntax creates a unique environment for each branch. The on_stop keyword pairs a stop job that tears down the environment.

deploy-review:
  stage: deploy
  script:
    - ./deploy.sh review $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop-review
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

stop-review:
  stage: deploy
  script:
    - ./teardown.sh review $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

The deploy-review job creates the environment when the merge request is opened. The stop-review job tears it down. The action: stop keyword tells GitLab that this job is the teardown, not a deployment. The stop job is typically when: manual so that the user can close the merge request and the stop job is triggered automatically, or so that the user can trigger the teardown manually .

The on_stop keyword links the deploy job to the stop job. When the environment is stopped, GitLab runs the stop job. The stop job must have the same environment.name as the deploy job, and it must declare action: stop.


Complete Example Session

# ============================================
# PART 1: STAGING DEPLOYMENT
# ============================================

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
# ============================================
# PART 2: PRODUCTION DEPLOYMENT
# ============================================

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  when: manual
  allow_failure: false
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
# ============================================
# PART 3: CONFIGURE PROTECTED ENVIRONMENT
# ============================================

# Settings → CI/CD → Protected environments
# Environment: production
# Allowed to deploy: Maintainers
# Required approvals: 1
# ============================================
# PART 4: CONFIGURE ENVIRONMENT VARIABLES
# ============================================

# Settings → CI/CD → Variables
# Key: DB_URL
# Value: postgres://staging:5432/app
# Environment scope: staging

# Key: DB_URL
# Value: postgres://prod:5432/app
# Environment scope: production
# ============================================
# PART 5: USE ENVIRONMENT VARIABLES
# ============================================

deploy-staging:
  script:
    - ./deploy.sh staging $DB_URL
  environment:
    name: staging
# Receives the staging-scoped DB_URL.
# ============================================
# PART 6: REVIEW APP DEPLOYMENT
# ============================================

deploy-review:
  stage: deploy
  script:
    - ./deploy.sh review $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_COMMIT_REF_SLUG.review.example.com
    on_stop: stop-review
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
# ============================================
# PART 7: STOP REVIEW APP
# ============================================

stop-review:
  stage: deploy
  script:
    - ./teardown.sh review $CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
# ============================================
# PART 8: VIEW DEPLOYMENTS
# ============================================

# Deployments → Environments
# Shows staging, production, and review apps.
# Each environment has a deployment history.
# ============================================
# PART 9: OBSERVE THE MANUAL GATE
# ============================================

# The pipeline reaches the deploy-production job.
# The job is paused with a play button.
# Click the play button to deploy.
# The deployment is recorded with the user and timestamp.

The nine parts trace the deployment workflow: staging, production with manual gate, protected environment, environment variables, variable usage, review app, stop review app, deployment view, and manual gate observation.


Quick Reference

Environment Keywords

KeywordPurpose
nameEnvironment label
urlURL shown in MR and environment page
on_stopPair with a stop job
actionstart (default) or stop
kubernetesKubernetes namespace config

Deployment Control

KeywordEffect
when: manualPause until user clicks play
allow_failure: falseBlock pipeline until job runs
resource_groupSerialize deployments to the same resource

Environment Scopes

ScopeEffect
productionOnly production jobs
stagingOnly staging jobs
*All environments

Review App Pattern

JobPurpose
DeployCreate environment with dynamic URL
StopTeardown with action: stop

Best Practices

✅ Do This:

environment:
  name: production
  url: https://example.com
when: manual
<!-- Manual gate for production -->                  # ✅
environment:
  name: review/$CI_COMMIT_REF_SLUG
  url: https://$CI_COMMIT_REF_SLUG.example.com
  on_stop: stop-review
<!-- Dynamic review app per branch -->               # ✅
environment_scope: production
<!-- Scope variables to the environment -->          # ✅

❌ Don’t Do This:

# Deploy to production without a manual gate
<!-- No approval step -->                            # ❌
# Use the same environment name for staging and production
<!-- Deployment history becomes ambiguous -->        # ❌
# Forget the stop job for review apps
<!-- Environments accumulate indefinitely -->        # ❌

Common Pitfalls

PitfallWhy It HappensFix
Deployment not trackedNo environment keywordAdd the declaration
Production deploys automaticallyNo when: manualAdd manual gate
Environment variables emptyWrong scopeCheck environment name
Review apps not cleaned upNo on_stopAdd stop job
Manual job cannot be runUser role insufficientCheck protected environment

Real-World Examples

1. Staging Deployment

environment:
  name: staging
  url: https://staging.example.com

2. Production Deployment

environment:
  name: production
  url: https://example.com
when: manual

3. Review App

environment:
  name: review/$CI_COMMIT_REF_SLUG
  url: https://$CI_COMMIT_REF_SLUG.example.com

4. Stop Job

environment:
  name: review/$CI_COMMIT_REF_SLUG
  action: stop

5. Protected Environment

Settings → CI/CD → Protected environments
Environment: production

6. Environment Variable

Settings → CI/CD → Variables
Environment scope: production

7. Resource Group

resource_group: production

8. Manual Gate

when: manual
allow_failure: false

9. Deployment Page

Deployments → Environments

10. Stop Review App

MR → Environments widget → Stop

Visual

Environment Lifecycle

┌──────────────────────────────────────────────┐
│  Pipeline runs                               │
│  deploy-staging job                          │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Staging environment updated                 │
│  Deployment recorded                         │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  deploy-production job                       │
│  Paused (when: manual)                       │
└──────────────────┬───────────────────────────┘
                   │ user clicks play
                   ▼
┌──────────────────────────────────────────────┐
│  Production environment updated              │
│  Deployment recorded                         │
└──────────────────────────────────────────────┘

Review App Flow

┌──────────────────────────────────────────────┐
│  Merge request opened                        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  deploy-review job runs                      │
│  Creates review/$CI_COMMIT_REF_SLUG env      │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Merge request closed                        │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  stop-review job runs                        │
│  Environment torn down                       │
└──────────────────────────────────────────────┘

Summary

ItemValue
Environment keywordenvironment
Environment namename
Environment URLurl
Stop actionaction: stop
Stop pairingon_stop
Manual gatewhen: manual
Blocking gateallow_failure: false
Variable scopingenvironment_scope
Protected environmentsSettings → CI/CD

Key takeaways:

  • The environment keyword tracks deployments. It records the commit, the pipeline, the user, and the timestamp. The Environments page shows the history and the current deployment for each environment .
  • The when: manual keyword is the deployment gate. It pauses the job until a user clicks the play button. The allow_failure: false setting makes the pause a blocking gate.
  • Protected environments enforce approvals. The environment protection rules require a specific role or an approval from a specific user. This is the mechanism for preventing unauthorized production deployments .
  • Review apps are per-branch environments. The name: review/$CI_COMMIT_REF_SLUG syntax creates a unique environment for each branch. The on_stop keyword pairs the deploy job with a stop job that tears down the environment .
  • Environment-scoped variables are the configuration mechanism. A variable scoped to production is only injected into production jobs. The same variable name can have different values for staging and production.

Remember: The environment is the label. The deployment is the script. The gate is the manual job. The protection is the rule. The variable is the scope. The review app is the per-branch target. The exam tests the configuration and the distinction between the deployment job and the environment record.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!